Change python package namespace sub namespaces to list

This commit is contained in:
2026-04-09 16:08:07 +02:00
parent 512a07a773
commit 9e524bec03
7 changed files with 130 additions and 66 deletions
+13 -7
View File
@@ -15,6 +15,7 @@
from __future__ import annotations from __future__ import annotations
from ...utils import ExistingDirectory, assert_that_is_instance from ...utils import ExistingDirectory, assert_that_is_instance
from .namespaces import PythonModuleNamespace, PythonPackageNamespace, PythonNamespace from .namespaces import PythonModuleNamespace, PythonPackageNamespace, PythonNamespace
from typing import List
import pathlib import pathlib
__all__ = [ __all__ = [
@@ -38,18 +39,18 @@ class PythonDocumentationGenerator:
self.project_directory = project_directory self.project_directory = project_directory
self.docs_directory = docs_directory self.docs_directory = docs_directory
def _namespace(self, directory: pathlib.Path) -> dict[str, PythonNamespace]: def _namespace(self, directory: pathlib.Path) -> PythonPackageNamespace:
namespaces = dict() namespace = PythonPackageNamespace(directory.name, [])
for entry in directory.iterdir(): for entry in directory.iterdir():
if entry.is_dir(): if entry.is_dir():
namespaces[entry.name] = PythonPackageNamespace(entry.name, self._namespace(entry)) namespace.add(self._namespace(entry))
elif entry.suffix == '.py': elif entry.suffix == '.py':
namespaces[entry.stem] = PythonModuleNamespace(entry.stem) namespace.add(PythonModuleNamespace(entry.stem))
return namespaces return namespace
def namespace(self) -> PythonPackageNamespace: def namespace(self) -> List[PythonPackageNamespace]:
''' '''
Returns the project as a namespace. Returns the project as a namespace.
@@ -57,4 +58,9 @@ class PythonDocumentationGenerator:
:rtype: PythonPackageNamespace :rtype: PythonPackageNamespace
''' '''
return PythonPackageNamespace('src', self._namespace(pathlib.Path((self.project_directory / 'src').as_posix()))) src_dir = pathlib.Path(str(self.project_directory / 'src'))
return [
self._namespace(dir)
for dir in src_dir.iterdir()
]
+67 -16
View File
@@ -13,8 +13,9 @@
# limitations under the License. # limitations under the License.
from __future__ import annotations from __future__ import annotations
from ...exceptions import InvalidNamespaceError, NamespaceNotFoundError from ...exceptions import InvalidNamespaceError, NamespaceNotFoundError, NamespaceExistsError
from ...utils import assert_that_is_instance from ...utils import assert_that_is_instance
from typing import Union, TypeAlias, Dict, List
__all__ = [ __all__ = [
'PythonNamespace', 'PythonNamespace',
@@ -40,6 +41,8 @@ class PythonNamespace:
def __repr__(self) -> str: def __repr__(self) -> str:
return f'{type(self).__name__}({self.name!r})' return f'{type(self).__name__}({self.name!r})'
PythonPackageNamespaceDictType: TypeAlias = Dict[str, Union[str, 'PythonPackageNamespaceDictType']]
class PythonPackageNamespace(PythonNamespace): class PythonPackageNamespace(PythonNamespace):
''' '''
A subclass of ``PythonNamespace`` for python package namespaces. A subclass of ``PythonNamespace`` for python package namespaces.
@@ -50,15 +53,27 @@ class PythonPackageNamespace(PythonNamespace):
:type sub_namespaces: list[PythonNamespace] :type sub_namespaces: list[PythonNamespace]
''' '''
def __init__(self, name: str, sub_namespaces: dict[str, PythonNamespace]) -> None: def __init__(self, name: str, sub_namespaces: List[PythonNamespace]) -> None:
assert_that_is_instance(sub_namespaces, list)
super().__init__(name) super().__init__(name)
self.sub_namespaces = sub_namespaces self._sub_namespaces = sub_namespaces
def namespace(self, sub_namespaces: list[str]) -> PythonNamespace: def namespace(self, sub_namespaces: List[str]) -> PythonNamespace:
''' '''
Returns the namespace object with a specific identifier, such as Returns the namespace object with a specific identifier, such as
``['package', 'subpackage', 'module']``. ``['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: if not sub_namespaces:
@@ -68,11 +83,7 @@ class PythonPackageNamespace(PythonNamespace):
if not sn: if not sn:
raise InvalidNamespaceError('invalid namespace') raise InvalidNamespaceError('invalid namespace')
# check whether the sub-namespace exists sub_namespace = self.__getitem__(sub_namespaces[0])
if sub_namespaces[0] not in self.sub_namespaces:
raise NamespaceNotFoundError('no such namespace', namespace_identifier = sub_namespaces[0])
sub_namespace = self.sub_namespaces[sub_namespaces[0]]
# package namespace # package namespace
if isinstance(sub_namespace, PythonPackageNamespace): if isinstance(sub_namespace, PythonPackageNamespace):
@@ -83,7 +94,7 @@ class PythonPackageNamespace(PythonNamespace):
else: else:
return sub_namespace return sub_namespace
def _sub_namespaces_names(self, package: PythonPackageNamespace) -> list[str]: def _sub_namespaces_names(self, package: PythonPackageNamespace) -> PythonPackageNamespaceDictType:
''' '''
Returns the names of all sub-namespaces of the specified package Returns the names of all sub-namespaces of the specified package
namespace and keeps the package structure. namespace and keeps the package structure.
@@ -92,21 +103,21 @@ class PythonPackageNamespace(PythonNamespace):
:type package: PythonPackageNamespace :type package: PythonPackageNamespace
:return: The names if all sub-namespaces. :return: The names if all sub-namespaces.
:rtype: list[str] :rtype: PythonPackageNamespaceDictType
''' '''
return { return {
n: self._sub_namespaces_names(sn) if isinstance(sn, PythonPackageNamespace) else sn.name sn.name: self._sub_namespaces_names(sn) if isinstance(sn, PythonPackageNamespace) else sn.name
for n, sn in package.sub_namespaces.items() for sn in package._sub_namespaces
} }
def sub_namespace_names(self) -> list[str]: def sub_namespace_names(self) -> PythonPackageNamespaceDictType:
''' '''
Returns the names of all sub-namespaces and keeps the package Returns the names of all sub-namespaces and keeps the package
structure. structure.
:return: The names if all sub-namespaces. :return: The names if all sub-namespaces.
:rtype: list[str] :rtype: PythonPackageNamespaceDictType
''' '''
return self._sub_namespaces_names(self) return self._sub_namespaces_names(self)
@@ -121,7 +132,47 @@ class PythonPackageNamespace(PythonNamespace):
return value.sub_namespace_names() == self.sub_namespace_names() return value.sub_namespace_names() == self.sub_namespace_names()
def __repr__(self) -> str: def __repr__(self) -> str:
return type(self).__name__ + repr((self.name, self.sub_namespaces)) 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): class PythonModuleNamespace(PythonNamespace):
''' '''
@@ -21,10 +21,9 @@ def test_PythonDocumentationGenerator_type_exceptions(project_directory, docs_di
PythonDocumentationGenerator(project_directory, docs_directory) PythonDocumentationGenerator(project_directory, docs_directory)
@pytest.mark.parametrize('python_documentation_generator,expected', [ @pytest.mark.parametrize('python_documentation_generator,expected', [
(PythonDocumentationGenerator(ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_1'), ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_1/docs')), PythonPackageNamespace('src', {})), (PythonDocumentationGenerator(ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_1'), ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_1/docs')), []),
(PythonDocumentationGenerator(ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_2'), ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_2/docs')), PythonPackageNamespace('src', {'module': PythonModuleNamespace('module')})), (PythonDocumentationGenerator(ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_2'), ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_2/docs')), [PythonPackageNamespace('pkg', [PythonModuleNamespace('module')])]),
(PythonDocumentationGenerator(ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_3'), ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_3/docs')), PythonPackageNamespace('src', {'module': PythonModuleNamespace('module'), 'pkg': PythonPackageNamespace('pkg', {'module2': PythonModuleNamespace('module2')})})), (PythonDocumentationGenerator(ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_3'), ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_3/docs')), [PythonPackageNamespace('pkg', [PythonModuleNamespace('module'), PythonPackageNamespace('pkg', [PythonModuleNamespace('module2')])])]),
]) ])
def test_PythonDocumentationGenerator_collect_modules(python_documentation_generator: PythonDocumentationGenerator, expected): def test_PythonDocumentationGenerator_collect_modules(python_documentation_generator: PythonDocumentationGenerator, expected):
print('MODS:', python_documentation_generator.namespace())
assert python_documentation_generator.namespace() == expected assert python_documentation_generator.namespace() == expected
@@ -5,40 +5,40 @@ from src.jcloud_docsgen.exceptions import NamespaceNotFoundError, InvalidNamespa
class StrSubclass(str): ... class StrSubclass(str): ...
@pytest.mark.parametrize('namespace,expected_name', [ @pytest.mark.parametrize('namespace,expected_name', [
(PythonPackageNamespace('a', {}), 'a'), (PythonPackageNamespace('a', []), 'a'),
(PythonPackageNamespace('1', {}), '1'), (PythonPackageNamespace('1', []), '1'),
(PythonPackageNamespace(StrSubclass('a'), {}), StrSubclass('a')), (PythonPackageNamespace(StrSubclass('a'), []), StrSubclass('a')),
]) ])
def test_PythonPackageNamespace_name_attribute(namespace, expected_name): def test_PythonPackageNamespace_name_attribute(namespace, expected_name):
assert namespace.name == expected_name assert namespace.name == expected_name
namespace_b = PythonModuleNamespace('b') namespace_b = PythonModuleNamespace('b')
namespace_a = PythonPackageNamespace('a', { namespace_a = PythonPackageNamespace('a', [
'b': namespace_b namespace_b
}) ])
namespace_c = PythonModuleNamespace('c') namespace_c = PythonModuleNamespace('c')
namespace_k = PythonModuleNamespace('k') namespace_k = PythonModuleNamespace('k')
namespace_j = PythonPackageNamespace('j', {'k': namespace_k}) namespace_j = PythonPackageNamespace('j', [namespace_k])
namespace_i = PythonPackageNamespace('i', {'j': namespace_j}) namespace_i = PythonPackageNamespace('i', [namespace_j])
namespace_h = PythonPackageNamespace('h', {'i': namespace_i}) namespace_h = PythonPackageNamespace('h', [namespace_i])
namespace_g = PythonPackageNamespace('g', {'h': namespace_h}) namespace_g = PythonPackageNamespace('g', [namespace_h])
namespace_f = PythonPackageNamespace('f', {'g': namespace_g}) namespace_f = PythonPackageNamespace('f', [namespace_g])
namespace_e = PythonPackageNamespace('e', {'f': namespace_f}) namespace_e = PythonPackageNamespace('e', [namespace_f])
namespace_d = PythonPackageNamespace('d', {'e': namespace_e}) namespace_d = PythonPackageNamespace('d', [namespace_e])
@pytest.mark.parametrize('namespace,sub_namespaces,expected', [ @pytest.mark.parametrize('namespace,sub_namespaces,expected', [
(PythonPackageNamespace('name', { (PythonPackageNamespace('name', [
'a': namespace_a, namespace_a,
'c': namespace_c namespace_c
}), ['c'], namespace_c), ]), ['c'], namespace_c),
(PythonPackageNamespace('name', { (PythonPackageNamespace('name', [
'a': namespace_a, namespace_a,
'c': namespace_c namespace_c
}), ['a'], namespace_a), ]), ['a'], namespace_a),
(PythonPackageNamespace('name', { (PythonPackageNamespace('name', [
'a': namespace_a, namespace_a,
'c': namespace_c namespace_c
}), ['a', 'b'], namespace_b), ]), ['a', 'b'], namespace_b),
(namespace_d, ['e', 'f', 'g', 'h', 'i'], namespace_i), (namespace_d, ['e', 'f', 'g', 'h', 'i'], namespace_i),
(namespace_d, ['e', 'f', 'g', 'h', 'i', 'j'], namespace_j), (namespace_d, ['e', 'f', 'g', 'h', 'i', 'j'], namespace_j),
(namespace_d, ['e', 'f', 'g', 'h', 'i', 'j', 'k'], namespace_k), (namespace_d, ['e', 'f', 'g', 'h', 'i', 'j', 'k'], namespace_k),
@@ -59,30 +59,38 @@ def test_PythonPackageNamespace_namespace_exceptions(namespace, sub_namespaces,
assert str(exc_info.value) == expected_exception_msg assert str(exc_info.value) == expected_exception_msg
@pytest.mark.parametrize('namespace,expected', [ @pytest.mark.parametrize('namespace,expected', [
(PythonPackageNamespace('a', {'b': PythonModuleNamespace('b')}), {'b': 'b'}), (PythonPackageNamespace('a', [PythonModuleNamespace('b')]), {'b': 'b'}),
(PythonPackageNamespace('a', {'b': PythonModuleNamespace('b'), 'c': PythonPackageNamespace('c', {'d': PythonModuleNamespace('d')})}), {'b': 'b', 'c': {'d': 'd'}}), (PythonPackageNamespace('a', [PythonModuleNamespace('b'), PythonPackageNamespace('c', [PythonModuleNamespace('d')])]), {'b': 'b', 'c': {'d': 'd'}}),
(PythonPackageNamespace('a', {}), {}) (PythonPackageNamespace('a', []), {})
]) ])
def test_PythonPackageNamespace_sub_namespace_names(namespace, expected): def test_PythonPackageNamespace_sub_namespace_names(namespace, expected):
print('NAMESPACE NAMESPACE NAMESPACE NAMESPACE NAMESPACE:', namespace) print('NAMESPACE NAMESPACE NAMESPACE NAMESPACE NAMESPACE:', namespace)
assert namespace.sub_namespace_names() == expected assert namespace.sub_namespace_names() == expected
@pytest.mark.parametrize('namespace1,namespace2,expected', [ @pytest.mark.parametrize('namespace1,namespace2,expected', [
(PythonPackageNamespace('a', {'b': PythonModuleNamespace('b')}), PythonPackageNamespace('a', {'b': PythonModuleNamespace('b')}), True), (PythonPackageNamespace('a', [PythonModuleNamespace('b')]), PythonPackageNamespace('a', [PythonModuleNamespace('b')]), True),
(PythonPackageNamespace('a', {'b': PythonModuleNamespace('b'), 'c': PythonPackageNamespace('c', {'d': PythonModuleNamespace('d')})}), PythonPackageNamespace('a', {'b': PythonModuleNamespace('b'), 'c': PythonPackageNamespace('c', {'d': PythonModuleNamespace('d')})}), True), (PythonPackageNamespace('a', [PythonModuleNamespace('b'), PythonPackageNamespace('c', [PythonModuleNamespace('d')])]), PythonPackageNamespace('a', [PythonModuleNamespace('b'), PythonPackageNamespace('c', [PythonModuleNamespace('d')])]), True),
(PythonPackageNamespace('a', {}), PythonPackageNamespace('a', {}), True), (PythonPackageNamespace('a', []), PythonPackageNamespace('a', []), True),
(PythonPackageNamespace('a', {}), PythonPackageNamespace('b', {}), False), (PythonPackageNamespace('a', []), PythonPackageNamespace('b', []), False),
]) ])
def test_PythonPackageNamespace___eq__(namespace1, namespace2, expected): def test_PythonPackageNamespace___eq__(namespace1, namespace2, expected):
assert (namespace1 == namespace2) == expected assert (namespace1 == namespace2) == expected
assert (namespace2 == namespace1) == expected assert (namespace2 == namespace1) == expected
@pytest.mark.parametrize('namespace,expected', [ @pytest.mark.parametrize('namespace,expected', [
(PythonPackageNamespace('a', {}), 'PythonPackageNamespace(\'a\', {})'), (PythonPackageNamespace('a', []), 'PythonPackageNamespace(\'a\', [])'),
(PythonPackageNamespace('1', {}), 'PythonPackageNamespace(\'1\', {})'), (PythonPackageNamespace('1', []), 'PythonPackageNamespace(\'1\', [])'),
(PythonPackageNamespace(StrSubclass('a'), {}), 'PythonPackageNamespace(\'a\', {})'), (PythonPackageNamespace(StrSubclass('a'), []), 'PythonPackageNamespace(\'a\', [])'),
(namespace_a, 'PythonPackageNamespace(\'a\', {\'b\': PythonModuleNamespace(\'b\')})'), (namespace_a, 'PythonPackageNamespace(\'a\', [PythonModuleNamespace(\'b\')])'),
(namespace_d, 'PythonPackageNamespace(\'d\', {\'e\': PythonPackageNamespace(\'e\', {\'f\': PythonPackageNamespace(\'f\', {\'g\': PythonPackageNamespace(\'g\', {\'h\': PythonPackageNamespace(\'h\', {\'i\': PythonPackageNamespace(\'i\', {\'j\': PythonPackageNamespace(\'j\', {\'k\': PythonModuleNamespace(\'k\')})})})})})})})') (namespace_d, 'PythonPackageNamespace(\'d\', [PythonPackageNamespace(\'e\', [PythonPackageNamespace(\'f\', [PythonPackageNamespace(\'g\', [PythonPackageNamespace(\'h\', [PythonPackageNamespace(\'i\', [PythonPackageNamespace(\'j\', [PythonModuleNamespace(\'k\')])])])])])])])')
]) ])
def test_PythonPackageNamespace_string_representation(namespace, expected): def test_PythonPackageNamespace_string_representation(namespace, expected):
print(namespace)
assert repr(namespace) == expected assert repr(namespace) == expected
@pytest.mark.parametrize('namespace,new_namespace,expected', [
(PythonPackageNamespace('a', []), PythonModuleNamespace('b'), PythonPackageNamespace('a', [PythonModuleNamespace('b')]))
])
def test_PythonPackageNamespace___getitem__(namespace, new_namespace, expected):
namespace.add(new_namespace)
assert namespace == expected