Merge pull request 'Fix(python-generator): ignore invalid namespaces' (#61) from fix/ignore-invalid-python-namespaces into main
Generate API reference / generate (push) Successful in 22s

Reviewed-on: #61
This commit was merged in pull request #61.
This commit is contained in:
jakob.scheid authored and Gitea committed 2026-10-04 17:19:13 +02:00
commit 1ccb610310
35 files changed
+167 -115

No files matched your search

+5
View File
@@ -6,6 +6,11 @@
- Python namespace name conflict when package and module have the same name - Python namespace name conflict when package and module have the same name
- Internal namespace name error which occured when generating Python documentation with `.` as the project directory - Internal namespace name error which occured when generating Python documentation with `.` as the project directory
- Ignore Python namespaces with invalid names during documentation generation
### Changed
- Stricter Python namespace name validation
## [0.2.0] - 2026-09-28 ## [0.2.0] - 2026-09-28
+22 -9
View File
@@ -37,7 +37,11 @@ from .definitions import (
PythonFunctionDefinition, PythonFunctionDefinition,
) )
from .docstrings import PythonDocstring from .docstrings import PythonDocstring
from .namespaces import PythonModuleNamespace, PythonPackageNamespace from .namespaces import (
PythonModuleNamespace,
PythonPackageNamespace,
validate_namespace_name,
)
__all__ = [ __all__ = [
'PythonDefinitionDocumentationIncludeSections', 'PythonDefinitionDocumentationIncludeSections',
@@ -926,9 +930,12 @@ class PythonModuleDocumentationGenerator:
), ),
**kwargs, **kwargs,
) -> None: ) -> None:
if module_path.name.count('.') > 1: if not (
module_path.suffix == '.py'
and validate_namespace_name(module_path.stem)
):
raise InvalidNamespaceError( raise InvalidNamespaceError(
'namespace identifier cannot contain more than one dot', 'invalid module file name',
namespace_identifier=module_path.name, namespace_identifier=module_path.name,
) )
@@ -1107,12 +1114,18 @@ class PythonDocumentationGenerator:
) )
for entry in directory.iterdir(): for entry in directory.iterdir():
if entry.is_dir() and ( try:
entry.parent / (f'{entry.stem}.py') if entry.is_dir() and (
).resolve() not in [e.resolve() for e in entry.parent.iterdir()]: entry.parent / (f'{entry.stem}.py')
namespace.add(self._namespace(entry)) ).resolve() not in [
elif entry.suffix == '.py': e.resolve() for e in entry.parent.iterdir()
namespace.add(PythonModuleNamespace(entry.stem)) ]:
namespace.add(self._namespace(entry))
elif entry.suffix == '.py':
namespace.add(PythonModuleNamespace(entry.stem))
except InvalidNamespaceError:
# the name of the directory or module is invalid; ignore.
pass
return namespace return namespace
+26 -3
View File
@@ -14,6 +14,7 @@
from __future__ import annotations from __future__ import annotations
import keyword
from typing import TypeAlias, Union from typing import TypeAlias, Union
from ...exceptions import ( from ...exceptions import (
@@ -24,27 +25,47 @@ from ...exceptions import (
from ...utils import assert_that_is_instance from ...utils import assert_that_is_instance
__all__ = [ __all__ = [
'validate_namespace_name',
'PythonNamespace', 'PythonNamespace',
'PythonPackageNamespace', 'PythonPackageNamespace',
'PythonModuleNamespace', '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: class PythonNamespace:
""" """
The base class for all Python namespaces such as modules or packages. The base class for all Python namespaces such as modules or packages.
:param name: The name of the namespace. :param name: The name of the namespace.
:type name: str :type name: str
:raises InvalidNamespaceError: If the namespace name is invalid.
""" """
def __init__(self, name: str) -> None: def __init__(self, name: str) -> None:
assert_that_is_instance(name, str) assert_that_is_instance(name, str)
if not name.strip(): if not validate_namespace_name(name):
raise InvalidNamespaceError( raise InvalidNamespaceError(
'invalid namespace', 'invalid namespace', namespace_identifier=name
namespace_identifier=name,
) )
self.name = name self.name = name
@@ -70,6 +91,8 @@ class PythonPackageNamespace(PythonNamespace):
:type name: str :type name: str
:param sub_namespaces: The sub-namespaces. :param sub_namespaces: The sub-namespaces.
:type sub_namespaces: list[PythonNamespace] :type sub_namespaces: list[PythonNamespace]
:raises InvalidNamespaceError: If the namespace name is invalid.
""" """
def __init__( def __init__(
@@ -0,0 +1,9 @@
# Module `pkg.a.b`
## Function `fn`
### Function signature
```python
fn()
```
@@ -25,176 +25,134 @@ from src.jcloud_docsgen.exceptions import InvalidNamespaceError
[ [
( (
pathlib.Path( pathlib.Path(
( ('tests/unit/core/python/_core/invalid_module_names/a.b.c'),
'tests/unit/core/python/_core/fil'
'es_with_more_than_one_dot/a.b.c'
),
), ),
None, None,
InvalidNamespaceError, InvalidNamespaceError,
'namespace identifier cannot contain more than one dot: a.b.c', 'invalid module file name: a.b.c',
),
(
pathlib.Path(
('tests/unit/core/python/_core/invalid_module_names/a.b.c.d'),
),
None,
InvalidNamespaceError,
'invalid module file name: a.b.c.d',
), ),
( (
pathlib.Path( pathlib.Path(
( (
'tests/unit/core/python/_core/file' 'tests/unit/core/python/_core/i'
's_with_more_than_one_dot/a.b.c.d' 'nvalid_module_names/a.b.c.d.e'
), ),
), ),
None, None,
InvalidNamespaceError, InvalidNamespaceError,
'namespace identifier cannot contain more than one dot: a.b.c.d', 'invalid module file name: a.b.c.d.e',
), ),
( (
pathlib.Path( pathlib.Path(
( (
'tests/unit/core/python/_core/files' 'tests/unit/core/python/_core/in'
'_with_more_than_one_dot/a.b.c.d.e' 'valid_module_names/a.b.c.d.e.f'
), ),
), ),
None, None,
InvalidNamespaceError, InvalidNamespaceError,
( 'invalid module file name: a.b.c.d.e.f',
'namespace identifier cannot cont'
'ain more than one dot: a.b.c.d.e'
),
), ),
( (
pathlib.Path( pathlib.Path(
( (
'tests/unit/core/python/_core/files_' 'tests/unit/core/python/_core/inv'
'with_more_than_one_dot/a.b.c.d.e.f' 'alid_module_names/a.b.c.d.e.f.g'
), ),
), ),
None, None,
InvalidNamespaceError, InvalidNamespaceError,
( 'invalid module file name: a.b.c.d.e.f.g',
'namespace identifier cannot conta'
'in more than one dot: a.b.c.d.e.f'
),
), ),
( (
pathlib.Path( pathlib.Path(
( (
'tests/unit/core/python/_core/files_w' 'tests/unit/core/python/_core/inva'
'ith_more_than_one_dot/a.b.c.d.e.f.g' 'lid_module_names/a.b.c.d.e.f.g.h'
), ),
), ),
None, None,
InvalidNamespaceError, InvalidNamespaceError,
( 'invalid module file name: a.b.c.d.e.f.g.h',
'namespace identifier cannot contai'
'n more than one dot: a.b.c.d.e.f.g'
),
), ),
( (
pathlib.Path( pathlib.Path(
( (
'tests/unit/core/python/_core/files_wi' 'tests/unit/core/python/_core/inval'
'th_more_than_one_dot/a.b.c.d.e.f.g.h' 'id_module_names/a.b.c.d.e.f.g.h.py'
), ),
), ),
None, None,
InvalidNamespaceError, InvalidNamespaceError,
( 'invalid module file name: a.b.c.d.e.f.g.h.py',
'namespace identifier cannot contain'
' more than one dot: a.b.c.d.e.f.g.h'
),
), ),
( (
pathlib.Path( pathlib.Path(
( (
'tests/unit/core/python/_core/files_wit' 'tests/unit/core/python/_core/inva'
'h_more_than_one_dot/a.b.c.d.e.f.g.h.py' 'lid_module_names/a.b.c.d.e.f.g.py'
), ),
), ),
None, None,
InvalidNamespaceError, InvalidNamespaceError,
( 'invalid module file name: a.b.c.d.e.f.g.py',
'namespace identifier cannot contain m'
'ore than one dot: a.b.c.d.e.f.g.h.py'
),
), ),
( (
pathlib.Path( pathlib.Path(
( (
'tests/unit/core/python/_core/files_wi' 'tests/unit/core/python/_core/in'
'th_more_than_one_dot/a.b.c.d.e.f.g.py' 'lid_module_names/a.b.c.d.e.f.py'
), ),
), ),
None, None,
InvalidNamespaceError, InvalidNamespaceError,
( 'invalid module file name: a.b.c.d.e.f.py',
'namespace identifier cannot contain '
'more than one dot: a.b.c.d.e.f.g.py'
),
), ),
( (
pathlib.Path( pathlib.Path(
( (
'tests/unit/core/python/_core/files_w' 'tests/unit/core/python/_core/in'
'ith_more_than_one_dot/a.b.c.d.e.f.py' 'valid_module_names/a.b.c.d.e.py'
), ),
), ),
None, None,
InvalidNamespaceError, InvalidNamespaceError,
( 'invalid module file name: a.b.c.d.e.py',
'namespace identifier cannot contain'
' more than one dot: a.b.c.d.e.f.py'
),
), ),
( (
pathlib.Path( pathlib.Path(
( (
'tests/unit/core/python/_core/files_' 'tests/unit/core/python/_core/i'
'with_more_than_one_dot/a.b.c.d.e.py' 'nvalid_module_names/a.b.c.d.py'
), ),
), ),
None, None,
InvalidNamespaceError, InvalidNamespaceError,
( 'invalid module file name: a.b.c.d.py',
'namespace identifier cannot contai'
'n more than one dot: a.b.c.d.e.py'
),
), ),
( (
pathlib.Path( pathlib.Path(
( ('tests/unit/core/python/_core/invalid_module_names/a.b.c.py'),
'tests/unit/core/python/_core/files'
'_with_more_than_one_dot/a.b.c.d.py'
),
), ),
None, None,
InvalidNamespaceError, InvalidNamespaceError,
( 'invalid module file name: a.b.c.py',
'namespace identifier cannot conta'
'in more than one dot: a.b.c.d.py'
),
), ),
( (
pathlib.Path( pathlib.Path(
( ('tests/unit/core/python/_core/invalid_module_names/a.b.py'),
'tests/unit/core/python/_core/file'
's_with_more_than_one_dot/a.b.c.py'
),
), ),
None, None,
InvalidNamespaceError, InvalidNamespaceError,
( 'invalid module file name: a.b.py',
'namespace identifier cannot cont'
'ain more than one dot: a.b.c.py'
),
),
(
pathlib.Path(
(
'tests/unit/core/python/_core/fil'
'es_with_more_than_one_dot/a.b.py'
),
),
None,
InvalidNamespaceError,
'namespace identifier cannot contain more than one dot: a.b.py',
), ),
( (
pathlib.Path( pathlib.Path(
@@ -205,7 +163,7 @@ from src.jcloud_docsgen.exceptions import InvalidNamespaceError
), ),
None, None,
InvalidNamespaceError, InvalidNamespaceError,
'namespace identifier cannot contain more than one dot: a.b.py', 'invalid module file name: a.b.py',
), ),
], ],
) )
@@ -28,7 +28,6 @@ class StrSubclass(str): ...
'namespace,expected_name', 'namespace,expected_name',
[ [
(PythonModuleNamespace('a'), 'a'), (PythonModuleNamespace('a'), 'a'),
(PythonModuleNamespace('1'), '1'),
(PythonModuleNamespace(StrSubclass('a')), StrSubclass('a')), (PythonModuleNamespace(StrSubclass('a')), StrSubclass('a')),
], ],
) )
@@ -41,11 +40,7 @@ def test_PythonModuleNamespace_name_attribute(namespace, expected_name):
[ [
(PythonModuleNamespace('a'), PythonModuleNamespace('a'), True), (PythonModuleNamespace('a'), PythonModuleNamespace('a'), True),
(PythonModuleNamespace('b'), PythonModuleNamespace('b'), True), (PythonModuleNamespace('b'), PythonModuleNamespace('b'), True),
(PythonModuleNamespace('1'), PythonModuleNamespace('1'), True),
(PythonModuleNamespace('42'), PythonModuleNamespace('42'), True),
(PythonModuleNamespace('a'), PythonModuleNamespace('b'), False), (PythonModuleNamespace('a'), PythonModuleNamespace('b'), False),
(PythonModuleNamespace('0'), PythonModuleNamespace('-0'), False),
(PythonModuleNamespace('1'), PythonModuleNamespace('a'), False),
(PythonModuleNamespace('a'), 'a', False), (PythonModuleNamespace('a'), 'a', False),
(PythonModuleNamespace('a'), 'b', False), (PythonModuleNamespace('a'), 'b', False),
(PythonModuleNamespace('a'), PythonPackageNamespace('a', []), False), (PythonModuleNamespace('a'), PythonPackageNamespace('a', []), False),
@@ -61,7 +56,6 @@ def test_PythonModuleNamespace___eq__(namespace1, namespace2, expected):
'namespace,expected', 'namespace,expected',
[ [
(PythonModuleNamespace('a'), "PythonModuleNamespace('a')"), (PythonModuleNamespace('a'), "PythonModuleNamespace('a')"),
(PythonModuleNamespace('1'), "PythonModuleNamespace('1')"),
( (
PythonModuleNamespace(StrSubclass('a')), PythonModuleNamespace(StrSubclass('a')),
"PythonModuleNamespace('a')", "PythonModuleNamespace('a')",
@@ -24,7 +24,6 @@ class StrSubclass(str): ...
'namespace,expected_name', 'namespace,expected_name',
[ [
(PythonNamespace('a'), 'a'), (PythonNamespace('a'), 'a'),
(PythonNamespace('1'), '1'),
(PythonNamespace(StrSubclass('a')), StrSubclass('a')), (PythonNamespace(StrSubclass('a')), StrSubclass('a')),
], ],
) )
@@ -36,7 +35,6 @@ def test_PythonNamespace_name_attribute(namespace, expected_name):
'namespace,expected', 'namespace,expected',
[ [
(PythonNamespace('a'), "PythonNamespace('a')"), (PythonNamespace('a'), "PythonNamespace('a')"),
(PythonNamespace('1'), "PythonNamespace('1')"),
(PythonNamespace(StrSubclass('a')), "PythonNamespace('a')"), (PythonNamespace(StrSubclass('a')), "PythonNamespace('a')"),
], ],
) )
@@ -38,13 +38,6 @@ class StrSubclass(str): ...
), ),
'a', 'a',
), ),
(
PythonPackageNamespace(
'1',
[],
),
'1',
),
( (
PythonPackageNamespace( PythonPackageNamespace(
StrSubclass('a'), StrSubclass('a'),
@@ -291,10 +284,6 @@ def test_PythonPackageNamespace___eq__(namespace1, namespace2, expected):
PythonPackageNamespace('a', []), PythonPackageNamespace('a', []),
"PythonPackageNamespace('a', [])", "PythonPackageNamespace('a', [])",
), ),
(
PythonPackageNamespace('1', []),
"PythonPackageNamespace('1', [])",
),
( (
PythonPackageNamespace(StrSubclass('a'), []), PythonPackageNamespace(StrSubclass('a'), []),
"PythonPackageNamespace('a', [])", "PythonPackageNamespace('a', [])",
@@ -0,0 +1,52 @@
# 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.
import pytest
from src.jcloud_docsgen.core.python.namespaces import validate_namespace_name
@pytest.mark.parametrize(
'name,expected',
[
('a', True),
('namespace', True),
('match', True),
('case', True),
('type', True),
('_', True),
('__', True),
('___', True),
('_a', True),
('_a_', True),
('_a.', False),
('a.b', False),
('a-b', False),
('a.', False),
('a-', False),
('.a', False),
('-a', False),
('.a.', False),
('-a-', False),
('.', False),
('a-.b', False),
('"', False),
('""', False),
('def', False),
('class', False),
('import', False),
],
)
def test_validate_namespace_name(name, expected):
assert validate_namespace_name(name) == expected