generated from jCloud/repository-template
Added more strict validation for namespaces names to PythonNamespace than before and updated error messages. All tests depending on this were updated to the new behavior.
250 lines
7.1 KiB
Python
250 lines
7.1 KiB
Python
# Copyright 2026 jCloud
|
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
from __future__ import annotations
|
|
|
|
import keyword
|
|
from typing import TypeAlias, Union
|
|
|
|
from ...exceptions import (
|
|
InvalidNamespaceError,
|
|
NamespaceExistsError,
|
|
NamespaceNotFoundError,
|
|
)
|
|
from ...utils import assert_that_is_instance
|
|
|
|
__all__ = [
|
|
'validate_namespace_name',
|
|
'PythonNamespace',
|
|
'PythonPackageNamespace',
|
|
'PythonModuleNamespace',
|
|
]
|
|
|
|
|
|
def validate_namespace_name(name: str) -> bool:
|
|
"""
|
|
Checks whether a namespace name is valid. Namespace names are valid
|
|
if they contain no dots and are valid Python identifiers.
|
|
|
|
:param name: The namespace name to validate.
|
|
:type name: str
|
|
|
|
:returns: Whether the namespace name is valid.
|
|
:rtype: bool
|
|
"""
|
|
|
|
if not isinstance(name, str):
|
|
return False
|
|
|
|
return name.isidentifier() and not keyword.iskeyword(name)
|
|
|
|
|
|
class PythonNamespace:
|
|
"""
|
|
The base class for all Python namespaces such as modules or packages.
|
|
|
|
:param name: The name of the namespace.
|
|
:type name: str
|
|
|
|
:raises InvalidNamespaceError: If the namespace name is invalid.
|
|
"""
|
|
|
|
def __init__(self, name: str) -> None:
|
|
assert_that_is_instance(name, str)
|
|
|
|
if not validate_namespace_name(name):
|
|
raise InvalidNamespaceError(
|
|
'invalid namespace', namespace_identifier=name
|
|
)
|
|
|
|
self.name = name
|
|
|
|
def __repr__(self) -> str:
|
|
return f'{type(self).__name__}({self.name!r})'
|
|
|
|
|
|
PythonPackageNamespaceDictType: TypeAlias = dict[
|
|
str,
|
|
Union[
|
|
str,
|
|
'PythonPackageNamespaceDictType',
|
|
],
|
|
]
|
|
|
|
|
|
class PythonPackageNamespace(PythonNamespace):
|
|
"""
|
|
A subclass of ``PythonNamespace`` for Python package namespaces.
|
|
|
|
:param name: The name of the namespace.
|
|
:type name: str
|
|
:param sub_namespaces: The sub-namespaces.
|
|
:type sub_namespaces: list[PythonNamespace]
|
|
|
|
:raises InvalidNamespaceError: If the namespace name is invalid.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
name: str,
|
|
sub_namespaces: list[PythonNamespace],
|
|
) -> None:
|
|
assert_that_is_instance(sub_namespaces, list)
|
|
|
|
super().__init__(name)
|
|
|
|
self._sub_namespaces = sub_namespaces
|
|
|
|
def namespace(self, sub_namespaces: list[str]) -> PythonNamespace:
|
|
"""
|
|
Returns the namespace object with a specific identifier, such as
|
|
``['package', 'subpackage', 'module']``.
|
|
|
|
:param sub_namespaces: The identifier of the namespace.
|
|
:type sub_namespaces: list[str]
|
|
|
|
:raises InvalidNamespaceError: If the namespace identifier is
|
|
invalid.
|
|
:raises NamespaceNotFoundError: If the namespace does not exist.
|
|
|
|
:return: The namespace.
|
|
:rtype: PythonNamespace
|
|
"""
|
|
|
|
if not sub_namespaces:
|
|
raise InvalidNamespaceError('invalid namespace')
|
|
|
|
for sn in sub_namespaces:
|
|
if not sn:
|
|
raise InvalidNamespaceError('invalid namespace')
|
|
|
|
sub_namespace = self.__getitem__(sub_namespaces[0])
|
|
|
|
# package namespace
|
|
if isinstance(sub_namespace, PythonPackageNamespace):
|
|
if len(sub_namespaces) == 1:
|
|
return sub_namespace
|
|
return sub_namespace.namespace(sub_namespaces[1:])
|
|
# module namespace
|
|
else:
|
|
return sub_namespace
|
|
|
|
def _sub_namespaces_names(
|
|
self,
|
|
package: PythonPackageNamespace,
|
|
) -> PythonPackageNamespaceDictType:
|
|
"""
|
|
Returns the names of all sub-namespaces of the specified package
|
|
namespace and keeps the package structure.
|
|
|
|
:param package: The package.
|
|
:type package: PythonPackageNamespace
|
|
|
|
:return: The names if all sub-namespaces.
|
|
:rtype: PythonPackageNamespaceDictType
|
|
"""
|
|
|
|
return {
|
|
sn.name: (
|
|
self._sub_namespaces_names(sn)
|
|
if isinstance(sn, PythonPackageNamespace)
|
|
else sn.name
|
|
)
|
|
for sn in package._sub_namespaces
|
|
}
|
|
|
|
def sub_namespace_names(self) -> PythonPackageNamespaceDictType:
|
|
"""
|
|
Returns the names of all sub-namespaces and keeps the package
|
|
structure.
|
|
|
|
:return: The names if all sub-namespaces.
|
|
:rtype: PythonPackageNamespaceDictType
|
|
"""
|
|
|
|
return self._sub_namespaces_names(self)
|
|
|
|
def __eq__(self, value: PythonPackageNamespace) -> bool:
|
|
# For performance reasons, is not called self.sub_namespace_names
|
|
# if the name attribute does not match, which can be quickly
|
|
# verified
|
|
if not (isinstance(value, type(self)) and self.name == value.name):
|
|
return False
|
|
|
|
return value.sub_namespace_names() == self.sub_namespace_names()
|
|
|
|
def __repr__(self) -> str:
|
|
return type(self).__name__ + repr((self.name, self._sub_namespaces))
|
|
|
|
def __getitem__(self, name: str) -> PythonNamespace:
|
|
"""
|
|
Returns a namespace with the specified name.
|
|
|
|
:param name: The name of the namespace
|
|
:type name: str
|
|
|
|
:raises NamespaceNotFoundError: If the namespace does not exist.
|
|
|
|
:return: The namespace
|
|
:rtype: PythonNamespace
|
|
"""
|
|
|
|
for sub in self._sub_namespaces:
|
|
if sub.name == name:
|
|
return sub
|
|
|
|
raise NamespaceNotFoundError(
|
|
'no such namespace',
|
|
namespace_identifier=name,
|
|
)
|
|
|
|
def add(self, namespace: PythonNamespace) -> None:
|
|
"""
|
|
Adds a namespace.
|
|
|
|
:param namespace: The namespace to add.
|
|
:type namespace: PythonNamespace
|
|
|
|
:raises NamespaceExistsError: If a namespace with the name of the
|
|
namespace that is attempted to add
|
|
already exists.
|
|
"""
|
|
try:
|
|
self.__getitem__(namespace.name)
|
|
raise NamespaceExistsError(
|
|
'namespace already exists',
|
|
namespace_identifier=namespace.name,
|
|
)
|
|
except NamespaceNotFoundError:
|
|
self._sub_namespaces.append(namespace)
|
|
|
|
@property
|
|
def sub_namespaces(self) -> list[PythonNamespace]:
|
|
return self._sub_namespaces
|
|
|
|
|
|
class PythonModuleNamespace(PythonNamespace):
|
|
"""
|
|
A subclass of ``PythonNamespace`` for Python module namespaces.
|
|
|
|
:param name: The name of the namespace.
|
|
:type name: str
|
|
"""
|
|
|
|
def __init__(self, name: str) -> None:
|
|
super().__init__(name)
|
|
|
|
def __eq__(self, value: PythonModuleNamespace) -> bool:
|
|
return isinstance(value, type(self)) and self.name == value.name
|