Files
jcloud-docsgen/src/jcloud_docsgen/core/python/namespaces.py
T
jakob.scheid 0f8f9d3cfa feat(python-namespaces)!: add validation for namespace names
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.
2026-10-04 16:39:31 +02:00

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