2 Commits
Author SHA1 Message Date
jakob.scheid 10328e6608 refactor(exceptions): define module exports
Added an __all__ variable to the exceptions module.
2026-10-03 18:42:56 +02:00
jakob.scheid bc802f7b38 refactor(utils): define module exports
Added an __all__ variable to the utility module.
2026-10-03 18:40:22 +02:00
39 changed files with 129 additions and 230 deletions

No files matched your search

+1 -12
View File
@@ -1,22 +1,11 @@
# Changelog # Changelog
## [0.2.2] - 2026-10-14 ## [Unreleased]
### Fixed
- Import error in Python <3.13 which prevented the tool from running and made it unusuable in Python <3.13
## [0.2.1] - 2026-10-06
### Fixed ### Fixed
- 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
@@ -1,45 +1,5 @@
# Module `jcloud_docsgen.core.python.namespaces` # Module `jcloud_docsgen.core.python.namespaces`
## Function `validate_namespace_name`
Checks whether a namespace name is valid. Namespace names are valid
if they contain no dots and are valid Python identifiers.
### Function signature
```python
validate_namespace_name(name: str) -> bool
```
### Returns
Whether the namespace name is valid.
Return type: `bool`
### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `name` | `str` | normal | -- | The namespace name to validate. |
### Docstring
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
## 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.
@@ -97,8 +57,6 @@ 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.
## Class `PythonPackageNamespace` ## Class `PythonPackageNamespace`
A subclass of ``PythonNamespace`` for Python package namespaces. A subclass of ``PythonNamespace`` for Python package namespaces.
@@ -423,8 +381,6 @@ A subclass of ``PythonNamespace`` for Python package namespaces.
: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.
## Class `PythonModuleNamespace` ## Class `PythonModuleNamespace`
A subclass of ``PythonNamespace`` for Python module namespaces. A subclass of ``PythonNamespace`` for Python module namespaces.
Generated
+4 -4
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.5.1 and should not be changed by hand. # This file is automatically @generated by Poetry 2.4.3 and should not be changed by hand.
[[package]] [[package]]
name = "colorama" name = "colorama"
@@ -235,14 +235,14 @@ version = "4.16.0"
description = "Backported and Experimental Type Hints for Python 3.9+" description = "Backported and Experimental Type Hints for Python 3.9+"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
groups = ["main", "dev"] groups = ["dev"]
markers = "python_version == \"3.10\""
files = [ files = [
{file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"}, {file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"},
{file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"}, {file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"},
] ]
markers = {dev = "python_version == \"3.10\""}
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = ">=3.10" python-versions = ">=3.10"
content-hash = "0fb3b53540e7a7fe173bf29a2031bd59f863a8dea94a29d51933de2375c3bfa2" content-hash = "24c9a2d464f6c737eeae0a66f792fa23824c3960b7cc72a22fff1408f21ec812"
+2 -2
View File
@@ -4,9 +4,9 @@ build-backend = "poetry.core.masonry.api"
[project] [project]
name = "jcloud-docsgen" name = "jcloud-docsgen"
version = "0.2.2" version = "0.2.0"
description = "A tool that generates Markdown documentation for software project source code" description = "A tool that generates Markdown documentation for software project source code"
dependencies = ["docstring-parser", "typing-extensions (>=4.16.0,<5.0.0)"] dependencies = ["docstring-parser"]
requires-python = ">=3.10" requires-python = ">=3.10"
license = "Apache-2.0" license = "Apache-2.0"
+9 -22
View File
@@ -37,11 +37,7 @@ from .definitions import (
PythonFunctionDefinition, PythonFunctionDefinition,
) )
from .docstrings import PythonDocstring from .docstrings import PythonDocstring
from .namespaces import ( from .namespaces import PythonModuleNamespace, PythonPackageNamespace
PythonModuleNamespace,
PythonPackageNamespace,
validate_namespace_name,
)
__all__ = [ __all__ = [
'PythonDefinitionDocumentationIncludeSections', 'PythonDefinitionDocumentationIncludeSections',
@@ -930,12 +926,9 @@ class PythonModuleDocumentationGenerator:
), ),
**kwargs, **kwargs,
) -> None: ) -> None:
if not ( if module_path.name.count('.') > 1:
module_path.suffix == '.py'
and validate_namespace_name(module_path.stem)
):
raise InvalidNamespaceError( raise InvalidNamespaceError(
'invalid module file name', 'namespace identifier cannot contain more than one dot',
namespace_identifier=module_path.name, namespace_identifier=module_path.name,
) )
@@ -1114,18 +1107,12 @@ class PythonDocumentationGenerator:
) )
for entry in directory.iterdir(): for entry in directory.iterdir():
try: if entry.is_dir() and (
if entry.is_dir() and ( entry.parent / (f'{entry.stem}.py')
entry.parent / (f'{entry.stem}.py') ).resolve() not in [e.resolve() for e in entry.parent.iterdir()]:
).resolve() not in [ namespace.add(self._namespace(entry))
e.resolve() for e in entry.parent.iterdir() elif entry.suffix == '.py':
]: 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
+3 -26
View File
@@ -14,7 +14,6 @@
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 (
@@ -25,47 +24,27 @@ 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 validate_namespace_name(name): if not name.strip():
raise InvalidNamespaceError( raise InvalidNamespaceError(
'invalid namespace', namespace_identifier=name 'invalid namespace',
namespace_identifier=name,
) )
self.name = name self.name = name
@@ -91,8 +70,6 @@ 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__(
+1
View File
@@ -14,6 +14,7 @@
import errno import errno
__all__ = [ __all__ = [
'NamespaceError', 'NamespaceError',
'InvalidNamespaceError', 'InvalidNamespaceError',
@@ -1,9 +0,0 @@
# Module `pkg.a.b`
## Function `fn`
### Function signature
```python
fn()
```
@@ -23,136 +23,178 @@ from src.jcloud_docsgen.exceptions import InvalidNamespaceError
@pytest.mark.parametrize( @pytest.mark.parametrize(
'module_path,src_directory,expected_exception,expected_exception_msg', 'module_path,src_directory,expected_exception,expected_exception_msg',
[ [
(
pathlib.Path(
('tests/unit/core/python/_core/invalid_module_names/a.b.c'),
),
None,
InvalidNamespaceError,
'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/i' 'tests/unit/core/python/_core/fil'
'nvalid_module_names/a.b.c.d.e' 'es_with_more_than_one_dot/a.b.c'
), ),
), ),
None, None,
InvalidNamespaceError, InvalidNamespaceError,
'invalid module file name: a.b.c.d.e', 'namespace identifier cannot contain more than one dot: a.b.c',
), ),
( (
pathlib.Path( pathlib.Path(
( (
'tests/unit/core/python/_core/in' 'tests/unit/core/python/_core/file'
'valid_module_names/a.b.c.d.e.f' 's_with_more_than_one_dot/a.b.c.d'
), ),
), ),
None, None,
InvalidNamespaceError, InvalidNamespaceError,
'invalid module file name: a.b.c.d.e.f', 'namespace identifier cannot contain more than one dot: a.b.c.d',
), ),
( (
pathlib.Path( pathlib.Path(
( (
'tests/unit/core/python/_core/inv' 'tests/unit/core/python/_core/files'
'alid_module_names/a.b.c.d.e.f.g' '_with_more_than_one_dot/a.b.c.d.e'
), ),
), ),
None, None,
InvalidNamespaceError, InvalidNamespaceError,
'invalid module file name: a.b.c.d.e.f.g', (
'namespace identifier cannot cont'
'ain more than one dot: a.b.c.d.e'
),
), ),
( (
pathlib.Path( pathlib.Path(
( (
'tests/unit/core/python/_core/inva' 'tests/unit/core/python/_core/files_'
'lid_module_names/a.b.c.d.e.f.g.h' 'with_more_than_one_dot/a.b.c.d.e.f'
), ),
), ),
None, None,
InvalidNamespaceError, InvalidNamespaceError,
'invalid module file name: a.b.c.d.e.f.g.h', (
'namespace identifier cannot conta'
'in more than one dot: a.b.c.d.e.f'
),
), ),
( (
pathlib.Path( pathlib.Path(
( (
'tests/unit/core/python/_core/inval' 'tests/unit/core/python/_core/files_w'
'id_module_names/a.b.c.d.e.f.g.h.py' 'ith_more_than_one_dot/a.b.c.d.e.f.g'
), ),
), ),
None, None,
InvalidNamespaceError, InvalidNamespaceError,
'invalid module file name: a.b.c.d.e.f.g.h.py', (
'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/inva' 'tests/unit/core/python/_core/files_wi'
'lid_module_names/a.b.c.d.e.f.g.py' 'th_more_than_one_dot/a.b.c.d.e.f.g.h'
), ),
), ),
None, None,
InvalidNamespaceError, InvalidNamespaceError,
'invalid module file name: a.b.c.d.e.f.g.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/in' 'tests/unit/core/python/_core/files_wit'
'lid_module_names/a.b.c.d.e.f.py' 'h_more_than_one_dot/a.b.c.d.e.f.g.h.py'
), ),
), ),
None, None,
InvalidNamespaceError, InvalidNamespaceError,
'invalid module file name: a.b.c.d.e.f.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/in' 'tests/unit/core/python/_core/files_wi'
'valid_module_names/a.b.c.d.e.py' 'th_more_than_one_dot/a.b.c.d.e.f.g.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.g.py'
),
), ),
( (
pathlib.Path( pathlib.Path(
( (
'tests/unit/core/python/_core/i' 'tests/unit/core/python/_core/files_w'
'nvalid_module_names/a.b.c.d.py' 'ith_more_than_one_dot/a.b.c.d.e.f.py'
), ),
), ),
None, None,
InvalidNamespaceError, InvalidNamespaceError,
'invalid module file name: a.b.c.d.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/invalid_module_names/a.b.c.py'), (
'tests/unit/core/python/_core/files_'
'with_more_than_one_dot/a.b.c.d.e.py'
),
), ),
None, None,
InvalidNamespaceError, InvalidNamespaceError,
'invalid module file name: a.b.c.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.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.py', (
'namespace identifier cannot conta'
'in more than one dot: a.b.c.d.py'
),
),
(
pathlib.Path(
(
'tests/unit/core/python/_core/file'
's_with_more_than_one_dot/a.b.c.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',
), ),
( (
pathlib.Path( pathlib.Path(
@@ -163,7 +205,7 @@ from src.jcloud_docsgen.exceptions import InvalidNamespaceError
), ),
None, None,
InvalidNamespaceError, InvalidNamespaceError,
'invalid module file name: a.b.py', 'namespace identifier cannot contain more than one dot: a.b.py',
), ),
], ],
) )
@@ -28,6 +28,7 @@ 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')),
], ],
) )
@@ -40,7 +41,11 @@ 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),
@@ -56,6 +61,7 @@ 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,6 +24,7 @@ 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')),
], ],
) )
@@ -35,6 +36,7 @@ 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,6 +38,13 @@ class StrSubclass(str): ...
), ),
'a', 'a',
), ),
(
PythonPackageNamespace(
'1',
[],
),
'1',
),
( (
PythonPackageNamespace( PythonPackageNamespace(
StrSubclass('a'), StrSubclass('a'),
@@ -284,6 +291,10 @@ 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', [])",
@@ -1,52 +0,0 @@
# 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