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.
This commit is contained in:
jakob.scheid committed 2026-10-04 16:39:31 +02:00
1 parent 3e77ebb7d0
commit 0f8f9d3cfa
19 files changed
+78 -109

No files matched your search

+10 -3
View File
@@ -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,
)
+26 -3
View File
@@ -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__(
@@ -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', [])",