generated from jCloud/repository-template
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
Generate API reference / generate (push) Successful in 22s
Reviewed-on: #61
This commit was merged in pull request #61.
This commit is contained in:
35 files changed
+167
-115
No files matched your search
@@ -6,6 +6,11 @@
|
||||
|
||||
- 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
|
||||
- Ignore Python namespaces with invalid names during documentation generation
|
||||
|
||||
### Changed
|
||||
|
||||
- Stricter Python namespace name validation
|
||||
|
||||
## [0.2.0] - 2026-09-28
|
||||
|
||||
|
||||
@@ -37,7 +37,11 @@ from .definitions import (
|
||||
PythonFunctionDefinition,
|
||||
)
|
||||
from .docstrings import PythonDocstring
|
||||
from .namespaces import PythonModuleNamespace, PythonPackageNamespace
|
||||
from .namespaces import (
|
||||
PythonModuleNamespace,
|
||||
PythonPackageNamespace,
|
||||
validate_namespace_name,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'PythonDefinitionDocumentationIncludeSections',
|
||||
@@ -926,9 +930,12 @@ class PythonModuleDocumentationGenerator:
|
||||
),
|
||||
**kwargs,
|
||||
) -> None:
|
||||
if module_path.name.count('.') > 1:
|
||||
if not (
|
||||
module_path.suffix == '.py'
|
||||
and validate_namespace_name(module_path.stem)
|
||||
):
|
||||
raise InvalidNamespaceError(
|
||||
'namespace identifier cannot contain more than one dot',
|
||||
'invalid module file name',
|
||||
namespace_identifier=module_path.name,
|
||||
)
|
||||
|
||||
@@ -1107,12 +1114,18 @@ class PythonDocumentationGenerator:
|
||||
)
|
||||
|
||||
for entry in directory.iterdir():
|
||||
if entry.is_dir() and (
|
||||
entry.parent / (f'{entry.stem}.py')
|
||||
).resolve() not in [e.resolve() for e in entry.parent.iterdir()]:
|
||||
namespace.add(self._namespace(entry))
|
||||
elif entry.suffix == '.py':
|
||||
namespace.add(PythonModuleNamespace(entry.stem))
|
||||
try:
|
||||
if entry.is_dir() and (
|
||||
entry.parent / (f'{entry.stem}.py')
|
||||
).resolve() not in [
|
||||
e.resolve() for e in entry.parent.iterdir()
|
||||
]:
|
||||
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
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import keyword
|
||||
from typing import TypeAlias, Union
|
||||
|
||||
from ...exceptions import (
|
||||
@@ -24,27 +25,47 @@ from ...exceptions import (
|
||||
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 name.strip():
|
||||
if not validate_namespace_name(name):
|
||||
raise InvalidNamespaceError(
|
||||
'invalid namespace',
|
||||
namespace_identifier=name,
|
||||
'invalid namespace', namespace_identifier=name
|
||||
)
|
||||
|
||||
self.name = name
|
||||
@@ -70,6 +91,8 @@ class PythonPackageNamespace(PythonNamespace):
|
||||
:type name: str
|
||||
:param sub_namespaces: The sub-namespaces.
|
||||
:type sub_namespaces: list[PythonNamespace]
|
||||
|
||||
:raises InvalidNamespaceError: If the namespace name is invalid.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+9
@@ -0,0 +1,9 @@
|
||||
# Module `pkg.a.b`
|
||||
|
||||
## Function `fn`
|
||||
|
||||
### Function signature
|
||||
|
||||
```python
|
||||
fn()
|
||||
```
|
||||
+1
@@ -0,0 +1 @@
|
||||
def fn(): ...
|
||||
+1
@@ -0,0 +1 @@
|
||||
def fn(): ...
|
||||
+1
@@ -0,0 +1 @@
|
||||
def fn(): ...
|
||||
+1
@@ -0,0 +1 @@
|
||||
def fn(): ...
|
||||
+1
@@ -0,0 +1 @@
|
||||
def fn(): ...
|
||||
+1
@@ -0,0 +1 @@
|
||||
def fn(): ...
|
||||
+1
@@ -0,0 +1 @@
|
||||
def fn(): ...
|
||||
+1
@@ -0,0 +1 @@
|
||||
def fn(): ...
|
||||
+1
@@ -0,0 +1 @@
|
||||
def fn(): ...
|
||||
Whitespace-only changes.
Whitespace-only changes.
+1
@@ -0,0 +1 @@
|
||||
def fn(): ...
|
||||
+1
@@ -0,0 +1 @@
|
||||
def fn(): ...
|
||||
@@ -25,176 +25,134 @@ from src.jcloud_docsgen.exceptions import InvalidNamespaceError
|
||||
[
|
||||
(
|
||||
pathlib.Path(
|
||||
(
|
||||
'tests/unit/core/python/_core/fil'
|
||||
'es_with_more_than_one_dot/a.b.c'
|
||||
),
|
||||
('tests/unit/core/python/_core/invalid_module_names/a.b.c'),
|
||||
),
|
||||
None,
|
||||
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(
|
||||
(
|
||||
'tests/unit/core/python/_core/file'
|
||||
's_with_more_than_one_dot/a.b.c.d'
|
||||
'tests/unit/core/python/_core/i'
|
||||
'nvalid_module_names/a.b.c.d.e'
|
||||
),
|
||||
),
|
||||
None,
|
||||
InvalidNamespaceError,
|
||||
'namespace identifier cannot contain more than one dot: a.b.c.d',
|
||||
'invalid module file name: a.b.c.d.e',
|
||||
),
|
||||
(
|
||||
pathlib.Path(
|
||||
(
|
||||
'tests/unit/core/python/_core/files'
|
||||
'_with_more_than_one_dot/a.b.c.d.e'
|
||||
'tests/unit/core/python/_core/in'
|
||||
'valid_module_names/a.b.c.d.e.f'
|
||||
),
|
||||
),
|
||||
None,
|
||||
InvalidNamespaceError,
|
||||
(
|
||||
'namespace identifier cannot cont'
|
||||
'ain more than one dot: a.b.c.d.e'
|
||||
),
|
||||
'invalid module file name: a.b.c.d.e.f',
|
||||
),
|
||||
(
|
||||
pathlib.Path(
|
||||
(
|
||||
'tests/unit/core/python/_core/files_'
|
||||
'with_more_than_one_dot/a.b.c.d.e.f'
|
||||
'tests/unit/core/python/_core/inv'
|
||||
'alid_module_names/a.b.c.d.e.f.g'
|
||||
),
|
||||
),
|
||||
None,
|
||||
InvalidNamespaceError,
|
||||
(
|
||||
'namespace identifier cannot conta'
|
||||
'in more than one dot: a.b.c.d.e.f'
|
||||
),
|
||||
'invalid module file name: a.b.c.d.e.f.g',
|
||||
),
|
||||
(
|
||||
pathlib.Path(
|
||||
(
|
||||
'tests/unit/core/python/_core/files_w'
|
||||
'ith_more_than_one_dot/a.b.c.d.e.f.g'
|
||||
'tests/unit/core/python/_core/inva'
|
||||
'lid_module_names/a.b.c.d.e.f.g.h'
|
||||
),
|
||||
),
|
||||
None,
|
||||
InvalidNamespaceError,
|
||||
(
|
||||
'namespace identifier cannot contai'
|
||||
'n more than one dot: a.b.c.d.e.f.g'
|
||||
),
|
||||
'invalid module file name: a.b.c.d.e.f.g.h',
|
||||
),
|
||||
(
|
||||
pathlib.Path(
|
||||
(
|
||||
'tests/unit/core/python/_core/files_wi'
|
||||
'th_more_than_one_dot/a.b.c.d.e.f.g.h'
|
||||
'tests/unit/core/python/_core/inval'
|
||||
'id_module_names/a.b.c.d.e.f.g.h.py'
|
||||
),
|
||||
),
|
||||
None,
|
||||
InvalidNamespaceError,
|
||||
(
|
||||
'namespace identifier cannot contain'
|
||||
' more than one dot: a.b.c.d.e.f.g.h'
|
||||
),
|
||||
'invalid module file name: a.b.c.d.e.f.g.h.py',
|
||||
),
|
||||
(
|
||||
pathlib.Path(
|
||||
(
|
||||
'tests/unit/core/python/_core/files_wit'
|
||||
'h_more_than_one_dot/a.b.c.d.e.f.g.h.py'
|
||||
'tests/unit/core/python/_core/inva'
|
||||
'lid_module_names/a.b.c.d.e.f.g.py'
|
||||
),
|
||||
),
|
||||
None,
|
||||
InvalidNamespaceError,
|
||||
(
|
||||
'namespace identifier cannot contain m'
|
||||
'ore than one dot: a.b.c.d.e.f.g.h.py'
|
||||
),
|
||||
'invalid module file name: a.b.c.d.e.f.g.py',
|
||||
),
|
||||
(
|
||||
pathlib.Path(
|
||||
(
|
||||
'tests/unit/core/python/_core/files_wi'
|
||||
'th_more_than_one_dot/a.b.c.d.e.f.g.py'
|
||||
'tests/unit/core/python/_core/in'
|
||||
'lid_module_names/a.b.c.d.e.f.py'
|
||||
),
|
||||
),
|
||||
None,
|
||||
InvalidNamespaceError,
|
||||
(
|
||||
'namespace identifier cannot contain '
|
||||
'more than one dot: a.b.c.d.e.f.g.py'
|
||||
),
|
||||
'invalid module file name: a.b.c.d.e.f.py',
|
||||
),
|
||||
(
|
||||
pathlib.Path(
|
||||
(
|
||||
'tests/unit/core/python/_core/files_w'
|
||||
'ith_more_than_one_dot/a.b.c.d.e.f.py'
|
||||
'tests/unit/core/python/_core/in'
|
||||
'valid_module_names/a.b.c.d.e.py'
|
||||
),
|
||||
),
|
||||
None,
|
||||
InvalidNamespaceError,
|
||||
(
|
||||
'namespace identifier cannot contain'
|
||||
' more than one dot: a.b.c.d.e.f.py'
|
||||
),
|
||||
'invalid module file name: a.b.c.d.e.py',
|
||||
),
|
||||
(
|
||||
pathlib.Path(
|
||||
(
|
||||
'tests/unit/core/python/_core/files_'
|
||||
'with_more_than_one_dot/a.b.c.d.e.py'
|
||||
'tests/unit/core/python/_core/i'
|
||||
'nvalid_module_names/a.b.c.d.py'
|
||||
),
|
||||
),
|
||||
None,
|
||||
InvalidNamespaceError,
|
||||
(
|
||||
'namespace identifier cannot contai'
|
||||
'n more than one dot: a.b.c.d.e.py'
|
||||
),
|
||||
'invalid module file name: a.b.c.d.py',
|
||||
),
|
||||
(
|
||||
pathlib.Path(
|
||||
(
|
||||
'tests/unit/core/python/_core/files'
|
||||
'_with_more_than_one_dot/a.b.c.d.py'
|
||||
),
|
||||
('tests/unit/core/python/_core/invalid_module_names/a.b.c.py'),
|
||||
),
|
||||
None,
|
||||
InvalidNamespaceError,
|
||||
(
|
||||
'namespace identifier cannot conta'
|
||||
'in more than one dot: a.b.c.d.py'
|
||||
),
|
||||
'invalid module file name: a.b.c.py',
|
||||
),
|
||||
(
|
||||
pathlib.Path(
|
||||
(
|
||||
'tests/unit/core/python/_core/file'
|
||||
's_with_more_than_one_dot/a.b.c.py'
|
||||
),
|
||||
('tests/unit/core/python/_core/invalid_module_names/a.b.py'),
|
||||
),
|
||||
None,
|
||||
InvalidNamespaceError,
|
||||
(
|
||||
'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',
|
||||
'invalid module file name: a.b.py',
|
||||
),
|
||||
(
|
||||
pathlib.Path(
|
||||
@@ -205,7 +163,7 @@ from src.jcloud_docsgen.exceptions import InvalidNamespaceError
|
||||
),
|
||||
None,
|
||||
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',
|
||||
[
|
||||
(PythonModuleNamespace('a'), 'a'),
|
||||
(PythonModuleNamespace('1'), '1'),
|
||||
(PythonModuleNamespace(StrSubclass('a')), StrSubclass('a')),
|
||||
],
|
||||
)
|
||||
@@ -41,11 +40,7 @@ def test_PythonModuleNamespace_name_attribute(namespace, expected_name):
|
||||
[
|
||||
(PythonModuleNamespace('a'), PythonModuleNamespace('a'), True),
|
||||
(PythonModuleNamespace('b'), PythonModuleNamespace('b'), True),
|
||||
(PythonModuleNamespace('1'), PythonModuleNamespace('1'), True),
|
||||
(PythonModuleNamespace('42'), PythonModuleNamespace('42'), True),
|
||||
(PythonModuleNamespace('a'), PythonModuleNamespace('b'), False),
|
||||
(PythonModuleNamespace('0'), PythonModuleNamespace('-0'), False),
|
||||
(PythonModuleNamespace('1'), PythonModuleNamespace('a'), False),
|
||||
(PythonModuleNamespace('a'), 'a', False),
|
||||
(PythonModuleNamespace('a'), 'b', False),
|
||||
(PythonModuleNamespace('a'), PythonPackageNamespace('a', []), False),
|
||||
@@ -61,7 +56,6 @@ def test_PythonModuleNamespace___eq__(namespace1, namespace2, expected):
|
||||
'namespace,expected',
|
||||
[
|
||||
(PythonModuleNamespace('a'), "PythonModuleNamespace('a')"),
|
||||
(PythonModuleNamespace('1'), "PythonModuleNamespace('1')"),
|
||||
(
|
||||
PythonModuleNamespace(StrSubclass('a')),
|
||||
"PythonModuleNamespace('a')",
|
||||
|
||||
@@ -24,7 +24,6 @@ class StrSubclass(str): ...
|
||||
'namespace,expected_name',
|
||||
[
|
||||
(PythonNamespace('a'), 'a'),
|
||||
(PythonNamespace('1'), '1'),
|
||||
(PythonNamespace(StrSubclass('a')), StrSubclass('a')),
|
||||
],
|
||||
)
|
||||
@@ -36,7 +35,6 @@ def test_PythonNamespace_name_attribute(namespace, expected_name):
|
||||
'namespace,expected',
|
||||
[
|
||||
(PythonNamespace('a'), "PythonNamespace('a')"),
|
||||
(PythonNamespace('1'), "PythonNamespace('1')"),
|
||||
(PythonNamespace(StrSubclass('a')), "PythonNamespace('a')"),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -38,13 +38,6 @@ class StrSubclass(str): ...
|
||||
),
|
||||
'a',
|
||||
),
|
||||
(
|
||||
PythonPackageNamespace(
|
||||
'1',
|
||||
[],
|
||||
),
|
||||
'1',
|
||||
),
|
||||
(
|
||||
PythonPackageNamespace(
|
||||
StrSubclass('a'),
|
||||
@@ -291,10 +284,6 @@ def test_PythonPackageNamespace___eq__(namespace1, namespace2, expected):
|
||||
PythonPackageNamespace('a', []),
|
||||
"PythonPackageNamespace('a', [])",
|
||||
),
|
||||
(
|
||||
PythonPackageNamespace('1', []),
|
||||
"PythonPackageNamespace('1', [])",
|
||||
),
|
||||
(
|
||||
PythonPackageNamespace(StrSubclass('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
|
||||
Reference in new issue
Block a user