From 9e524bec0328d3de0e65744b8fe85d249adf19d1 Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Thu, 9 Apr 2026 16:08:07 +0200 Subject: [PATCH] Change python package namespace sub namespaces to list --- src/jcloud_docsgen/core/python/_core.py | 20 +++-- src/jcloud_docsgen/core/python/namespaces.py | 85 +++++++++++++++---- .../test_PythonDocumentationGenerator.py | 7 +- .../pdir_2/src/{ => pkg}/module.py | 0 .../pdir_3/src/{ => pkg}/module.py | 0 .../pdir_3/src/pkg/{ => pkg}/module2.py | 0 .../namespaces/test_PythonPackageNamespace.py | 84 +++++++++--------- 7 files changed, 130 insertions(+), 66 deletions(-) rename tests/unit/core/python/_core/test_project_dirs/pdir_2/src/{ => pkg}/module.py (100%) rename tests/unit/core/python/_core/test_project_dirs/pdir_3/src/{ => pkg}/module.py (100%) rename tests/unit/core/python/_core/test_project_dirs/pdir_3/src/pkg/{ => pkg}/module2.py (100%) diff --git a/src/jcloud_docsgen/core/python/_core.py b/src/jcloud_docsgen/core/python/_core.py index c0014f4..0a7a44b 100644 --- a/src/jcloud_docsgen/core/python/_core.py +++ b/src/jcloud_docsgen/core/python/_core.py @@ -15,6 +15,7 @@ from __future__ import annotations from ...utils import ExistingDirectory, assert_that_is_instance from .namespaces import PythonModuleNamespace, PythonPackageNamespace, PythonNamespace +from typing import List import pathlib __all__ = [ @@ -38,18 +39,18 @@ class PythonDocumentationGenerator: self.project_directory = project_directory self.docs_directory = docs_directory - def _namespace(self, directory: pathlib.Path) -> dict[str, PythonNamespace]: - namespaces = dict() + def _namespace(self, directory: pathlib.Path) -> PythonPackageNamespace: + namespace = PythonPackageNamespace(directory.name, []) for entry in directory.iterdir(): if entry.is_dir(): - namespaces[entry.name] = PythonPackageNamespace(entry.name, self._namespace(entry)) + namespace.add(self._namespace(entry)) 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. @@ -57,4 +58,9 @@ class PythonDocumentationGenerator: :rtype: PythonPackageNamespace ''' - return PythonPackageNamespace('src', self._namespace(pathlib.Path((self.project_directory / 'src').as_posix()))) \ No newline at end of file + src_dir = pathlib.Path(str(self.project_directory / 'src')) + + return [ + self._namespace(dir) + for dir in src_dir.iterdir() + ] \ No newline at end of file diff --git a/src/jcloud_docsgen/core/python/namespaces.py b/src/jcloud_docsgen/core/python/namespaces.py index 998a3eb..511b511 100644 --- a/src/jcloud_docsgen/core/python/namespaces.py +++ b/src/jcloud_docsgen/core/python/namespaces.py @@ -13,8 +13,9 @@ # limitations under the License. from __future__ import annotations -from ...exceptions import InvalidNamespaceError, NamespaceNotFoundError +from ...exceptions import InvalidNamespaceError, NamespaceNotFoundError, NamespaceExistsError from ...utils import assert_that_is_instance +from typing import Union, TypeAlias, Dict, List __all__ = [ 'PythonNamespace', @@ -40,6 +41,8 @@ class PythonNamespace: 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. @@ -50,15 +53,27 @@ class PythonPackageNamespace(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) - 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 ``['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: @@ -68,11 +83,7 @@ class PythonPackageNamespace(PythonNamespace): if not sn: raise InvalidNamespaceError('invalid namespace') - # check whether the sub-namespace exists - 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]] + sub_namespace = self.__getitem__(sub_namespaces[0]) # package namespace if isinstance(sub_namespace, PythonPackageNamespace): @@ -82,8 +93,8 @@ class PythonPackageNamespace(PythonNamespace): # module namespace else: 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 namespace and keeps the package structure. @@ -92,21 +103,21 @@ class PythonPackageNamespace(PythonNamespace): :type package: PythonPackageNamespace :return: The names if all sub-namespaces. - :rtype: list[str] + :rtype: PythonPackageNamespaceDictType ''' return { - n: self._sub_namespaces_names(sn) if isinstance(sn, PythonPackageNamespace) else sn.name - for n, sn in package.sub_namespaces.items() + 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) -> list[str]: + 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: list[str] + :rtype: PythonPackageNamespaceDictType ''' return self._sub_namespaces_names(self) @@ -121,7 +132,47 @@ class PythonPackageNamespace(PythonNamespace): return value.sub_namespace_names() == self.sub_namespace_names() 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): ''' diff --git a/tests/unit/core/python/_core/test_PythonDocumentationGenerator.py b/tests/unit/core/python/_core/test_PythonDocumentationGenerator.py index 7eb3243..9726a53 100644 --- a/tests/unit/core/python/_core/test_PythonDocumentationGenerator.py +++ b/tests/unit/core/python/_core/test_PythonDocumentationGenerator.py @@ -21,10 +21,9 @@ def test_PythonDocumentationGenerator_type_exceptions(project_directory, docs_di PythonDocumentationGenerator(project_directory, docs_directory) @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_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_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_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('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('pkg', [PythonModuleNamespace('module'), PythonPackageNamespace('pkg', [PythonModuleNamespace('module2')])])]), ]) def test_PythonDocumentationGenerator_collect_modules(python_documentation_generator: PythonDocumentationGenerator, expected): - print('MODS:', python_documentation_generator.namespace()) assert python_documentation_generator.namespace() == expected \ No newline at end of file diff --git a/tests/unit/core/python/_core/test_project_dirs/pdir_2/src/module.py b/tests/unit/core/python/_core/test_project_dirs/pdir_2/src/pkg/module.py similarity index 100% rename from tests/unit/core/python/_core/test_project_dirs/pdir_2/src/module.py rename to tests/unit/core/python/_core/test_project_dirs/pdir_2/src/pkg/module.py diff --git a/tests/unit/core/python/_core/test_project_dirs/pdir_3/src/module.py b/tests/unit/core/python/_core/test_project_dirs/pdir_3/src/pkg/module.py similarity index 100% rename from tests/unit/core/python/_core/test_project_dirs/pdir_3/src/module.py rename to tests/unit/core/python/_core/test_project_dirs/pdir_3/src/pkg/module.py diff --git a/tests/unit/core/python/_core/test_project_dirs/pdir_3/src/pkg/module2.py b/tests/unit/core/python/_core/test_project_dirs/pdir_3/src/pkg/pkg/module2.py similarity index 100% rename from tests/unit/core/python/_core/test_project_dirs/pdir_3/src/pkg/module2.py rename to tests/unit/core/python/_core/test_project_dirs/pdir_3/src/pkg/pkg/module2.py diff --git a/tests/unit/core/python/namespaces/test_PythonPackageNamespace.py b/tests/unit/core/python/namespaces/test_PythonPackageNamespace.py index 6ac021f..71d1ad2 100644 --- a/tests/unit/core/python/namespaces/test_PythonPackageNamespace.py +++ b/tests/unit/core/python/namespaces/test_PythonPackageNamespace.py @@ -5,40 +5,40 @@ from src.jcloud_docsgen.exceptions import NamespaceNotFoundError, InvalidNamespa class StrSubclass(str): ... @pytest.mark.parametrize('namespace,expected_name', [ - (PythonPackageNamespace('a', {}), 'a'), - (PythonPackageNamespace('1', {}), '1'), - (PythonPackageNamespace(StrSubclass('a'), {}), StrSubclass('a')), + (PythonPackageNamespace('a', []), 'a'), + (PythonPackageNamespace('1', []), '1'), + (PythonPackageNamespace(StrSubclass('a'), []), StrSubclass('a')), ]) def test_PythonPackageNamespace_name_attribute(namespace, expected_name): assert namespace.name == expected_name namespace_b = PythonModuleNamespace('b') -namespace_a = PythonPackageNamespace('a', { - 'b': namespace_b -}) +namespace_a = PythonPackageNamespace('a', [ + namespace_b +]) namespace_c = PythonModuleNamespace('c') namespace_k = PythonModuleNamespace('k') -namespace_j = PythonPackageNamespace('j', {'k': namespace_k}) -namespace_i = PythonPackageNamespace('i', {'j': namespace_j}) -namespace_h = PythonPackageNamespace('h', {'i': namespace_i}) -namespace_g = PythonPackageNamespace('g', {'h': namespace_h}) -namespace_f = PythonPackageNamespace('f', {'g': namespace_g}) -namespace_e = PythonPackageNamespace('e', {'f': namespace_f}) -namespace_d = PythonPackageNamespace('d', {'e': namespace_e}) +namespace_j = PythonPackageNamespace('j', [namespace_k]) +namespace_i = PythonPackageNamespace('i', [namespace_j]) +namespace_h = PythonPackageNamespace('h', [namespace_i]) +namespace_g = PythonPackageNamespace('g', [namespace_h]) +namespace_f = PythonPackageNamespace('f', [namespace_g]) +namespace_e = PythonPackageNamespace('e', [namespace_f]) +namespace_d = PythonPackageNamespace('d', [namespace_e]) @pytest.mark.parametrize('namespace,sub_namespaces,expected', [ - (PythonPackageNamespace('name', { - 'a': namespace_a, - 'c': namespace_c - }), ['c'], namespace_c), - (PythonPackageNamespace('name', { - 'a': namespace_a, - 'c': namespace_c - }), ['a'], namespace_a), - (PythonPackageNamespace('name', { - 'a': namespace_a, - 'c': namespace_c - }), ['a', 'b'], namespace_b), + (PythonPackageNamespace('name', [ + namespace_a, + namespace_c + ]), ['c'], namespace_c), + (PythonPackageNamespace('name', [ + namespace_a, + namespace_c + ]), ['a'], namespace_a), + (PythonPackageNamespace('name', [ + namespace_a, + namespace_c + ]), ['a', 'b'], namespace_b), (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', 'k'], namespace_k), @@ -59,30 +59,38 @@ def test_PythonPackageNamespace_namespace_exceptions(namespace, sub_namespaces, assert str(exc_info.value) == expected_exception_msg @pytest.mark.parametrize('namespace,expected', [ - (PythonPackageNamespace('a', {'b': PythonModuleNamespace('b')}), {'b': 'b'}), - (PythonPackageNamespace('a', {'b': PythonModuleNamespace('b'), 'c': PythonPackageNamespace('c', {'d': PythonModuleNamespace('d')})}), {'b': 'b', 'c': {'d': 'd'}}), - (PythonPackageNamespace('a', {}), {}) + (PythonPackageNamespace('a', [PythonModuleNamespace('b')]), {'b': 'b'}), + (PythonPackageNamespace('a', [PythonModuleNamespace('b'), PythonPackageNamespace('c', [PythonModuleNamespace('d')])]), {'b': 'b', 'c': {'d': 'd'}}), + (PythonPackageNamespace('a', []), {}) ]) def test_PythonPackageNamespace_sub_namespace_names(namespace, expected): print('NAMESPACE NAMESPACE NAMESPACE NAMESPACE NAMESPACE:', namespace) assert namespace.sub_namespace_names() == expected @pytest.mark.parametrize('namespace1,namespace2,expected', [ - (PythonPackageNamespace('a', {'b': PythonModuleNamespace('b')}), PythonPackageNamespace('a', {'b': 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', {}), PythonPackageNamespace('a', {}), True), - (PythonPackageNamespace('a', {}), PythonPackageNamespace('b', {}), False), + (PythonPackageNamespace('a', [PythonModuleNamespace('b')]), PythonPackageNamespace('a', [PythonModuleNamespace('b')]), 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('b', []), False), ]) def test_PythonPackageNamespace___eq__(namespace1, namespace2, expected): assert (namespace1 == namespace2) == expected assert (namespace2 == namespace1) == expected @pytest.mark.parametrize('namespace,expected', [ - (PythonPackageNamespace('a', {}), 'PythonPackageNamespace(\'a\', {})'), - (PythonPackageNamespace('1', {}), 'PythonPackageNamespace(\'1\', {})'), - (PythonPackageNamespace(StrSubclass('a'), {}), 'PythonPackageNamespace(\'a\', {})'), - (namespace_a, 'PythonPackageNamespace(\'a\', {\'b\': 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\')})})})})})})})') + (PythonPackageNamespace('a', []), 'PythonPackageNamespace(\'a\', [])'), + (PythonPackageNamespace('1', []), 'PythonPackageNamespace(\'1\', [])'), + (PythonPackageNamespace(StrSubclass('a'), []), 'PythonPackageNamespace(\'a\', [])'), + (namespace_a, 'PythonPackageNamespace(\'a\', [PythonModuleNamespace(\'b\')])'), + (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): - assert repr(namespace) == expected \ No newline at end of file + print(namespace) + 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 \ No newline at end of file