# 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