From 0f8f9d3cfa2b6f0ea747195306d5f8d9fb93a65d Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Sun, 4 Oct 2026 16:39:31 +0200 Subject: [PATCH] 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. --- src/jcloud_docsgen/core/python/_core.py | 13 +- src/jcloud_docsgen/core/python/namespaces.py | 29 +++- .../a.b.c | 0 .../a.b.c.d | 0 .../a.b.c.d.e | 0 .../a.b.c.d.e.f | 0 .../a.b.c.d.e.f.g | 0 .../a.b.c.d.e.f.g.h | 0 .../a.b.c.d.e.f.g.h.py | 0 .../a.b.c.d.e.f.g.py | 0 .../a.b.c.d.e.f.py | 0 .../a.b.c.d.e.py | 0 .../a.b.c.d.py | 0 .../a.b.c.py | 0 .../a.b.py | 0 ...test_PythonModuleDocumentationGenerator.py | 126 ++++++------------ .../namespaces/test_PythonModuleNamespace.py | 6 - .../python/namespaces/test_PythonNamespace.py | 2 - .../namespaces/test_PythonPackageNamespace.py | 11 -- 19 files changed, 78 insertions(+), 109 deletions(-) rename tests/unit/core/python/_core/{files_with_more_than_one_dot => invalid_module_names}/a.b.c (100%) rename tests/unit/core/python/_core/{files_with_more_than_one_dot => invalid_module_names}/a.b.c.d (100%) rename tests/unit/core/python/_core/{files_with_more_than_one_dot => invalid_module_names}/a.b.c.d.e (100%) rename tests/unit/core/python/_core/{files_with_more_than_one_dot => invalid_module_names}/a.b.c.d.e.f (100%) rename tests/unit/core/python/_core/{files_with_more_than_one_dot => invalid_module_names}/a.b.c.d.e.f.g (100%) rename tests/unit/core/python/_core/{files_with_more_than_one_dot => invalid_module_names}/a.b.c.d.e.f.g.h (100%) rename tests/unit/core/python/_core/{files_with_more_than_one_dot => invalid_module_names}/a.b.c.d.e.f.g.h.py (100%) rename tests/unit/core/python/_core/{files_with_more_than_one_dot => invalid_module_names}/a.b.c.d.e.f.g.py (100%) rename tests/unit/core/python/_core/{files_with_more_than_one_dot => invalid_module_names}/a.b.c.d.e.f.py (100%) rename tests/unit/core/python/_core/{files_with_more_than_one_dot => invalid_module_names}/a.b.c.d.e.py (100%) rename tests/unit/core/python/_core/{files_with_more_than_one_dot => invalid_module_names}/a.b.c.d.py (100%) rename tests/unit/core/python/_core/{files_with_more_than_one_dot => invalid_module_names}/a.b.c.py (100%) rename tests/unit/core/python/_core/{files_with_more_than_one_dot => invalid_module_names}/a.b.py (100%) diff --git a/src/jcloud_docsgen/core/python/_core.py b/src/jcloud_docsgen/core/python/_core.py index 78b3f14..5e2cd6b 100644 --- a/src/jcloud_docsgen/core/python/_core.py +++ b/src/jcloud_docsgen/core/python/_core.py @@ -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, ) diff --git a/src/jcloud_docsgen/core/python/namespaces.py b/src/jcloud_docsgen/core/python/namespaces.py index 706a816..4391cff 100644 --- a/src/jcloud_docsgen/core/python/namespaces.py +++ b/src/jcloud_docsgen/core/python/namespaces.py @@ -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__( diff --git a/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c b/tests/unit/core/python/_core/invalid_module_names/a.b.c similarity index 100% rename from tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c rename to tests/unit/core/python/_core/invalid_module_names/a.b.c diff --git a/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d b/tests/unit/core/python/_core/invalid_module_names/a.b.c.d similarity index 100% rename from tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d rename to tests/unit/core/python/_core/invalid_module_names/a.b.c.d diff --git a/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e b/tests/unit/core/python/_core/invalid_module_names/a.b.c.d.e similarity index 100% rename from tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e rename to tests/unit/core/python/_core/invalid_module_names/a.b.c.d.e diff --git a/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.f b/tests/unit/core/python/_core/invalid_module_names/a.b.c.d.e.f similarity index 100% rename from tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.f rename to tests/unit/core/python/_core/invalid_module_names/a.b.c.d.e.f diff --git a/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.f.g b/tests/unit/core/python/_core/invalid_module_names/a.b.c.d.e.f.g similarity index 100% rename from tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.f.g rename to tests/unit/core/python/_core/invalid_module_names/a.b.c.d.e.f.g diff --git a/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.f.g.h b/tests/unit/core/python/_core/invalid_module_names/a.b.c.d.e.f.g.h similarity index 100% rename from tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.f.g.h rename to tests/unit/core/python/_core/invalid_module_names/a.b.c.d.e.f.g.h diff --git a/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.f.g.h.py b/tests/unit/core/python/_core/invalid_module_names/a.b.c.d.e.f.g.h.py similarity index 100% rename from tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.f.g.h.py rename to tests/unit/core/python/_core/invalid_module_names/a.b.c.d.e.f.g.h.py diff --git a/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.f.g.py b/tests/unit/core/python/_core/invalid_module_names/a.b.c.d.e.f.g.py similarity index 100% rename from tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.f.g.py rename to tests/unit/core/python/_core/invalid_module_names/a.b.c.d.e.f.g.py diff --git a/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.f.py b/tests/unit/core/python/_core/invalid_module_names/a.b.c.d.e.f.py similarity index 100% rename from tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.f.py rename to tests/unit/core/python/_core/invalid_module_names/a.b.c.d.e.f.py diff --git a/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.py b/tests/unit/core/python/_core/invalid_module_names/a.b.c.d.e.py similarity index 100% rename from tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.py rename to tests/unit/core/python/_core/invalid_module_names/a.b.c.d.e.py diff --git a/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.py b/tests/unit/core/python/_core/invalid_module_names/a.b.c.d.py similarity index 100% rename from tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.py rename to tests/unit/core/python/_core/invalid_module_names/a.b.c.d.py diff --git a/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.py b/tests/unit/core/python/_core/invalid_module_names/a.b.c.py similarity index 100% rename from tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.py rename to tests/unit/core/python/_core/invalid_module_names/a.b.c.py diff --git a/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.py b/tests/unit/core/python/_core/invalid_module_names/a.b.py similarity index 100% rename from tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.py rename to tests/unit/core/python/_core/invalid_module_names/a.b.py diff --git a/tests/unit/core/python/_core/test_PythonModuleDocumentationGenerator.py b/tests/unit/core/python/_core/test_PythonModuleDocumentationGenerator.py index 33c63b7..03921c9 100644 --- a/tests/unit/core/python/_core/test_PythonModuleDocumentationGenerator.py +++ b/tests/unit/core/python/_core/test_PythonModuleDocumentationGenerator.py @@ -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', ), ], ) diff --git a/tests/unit/core/python/namespaces/test_PythonModuleNamespace.py b/tests/unit/core/python/namespaces/test_PythonModuleNamespace.py index 53d7e0e..3f86f7f 100644 --- a/tests/unit/core/python/namespaces/test_PythonModuleNamespace.py +++ b/tests/unit/core/python/namespaces/test_PythonModuleNamespace.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')", diff --git a/tests/unit/core/python/namespaces/test_PythonNamespace.py b/tests/unit/core/python/namespaces/test_PythonNamespace.py index e4ee640..ba56d89 100644 --- a/tests/unit/core/python/namespaces/test_PythonNamespace.py +++ b/tests/unit/core/python/namespaces/test_PythonNamespace.py @@ -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')"), ], ) diff --git a/tests/unit/core/python/namespaces/test_PythonPackageNamespace.py b/tests/unit/core/python/namespaces/test_PythonPackageNamespace.py index 1087cf0..30ca226 100644 --- a/tests/unit/core/python/namespaces/test_PythonPackageNamespace.py +++ b/tests/unit/core/python/namespaces/test_PythonPackageNamespace.py @@ -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', [])",