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
## [0.2.2] - 2026-10-14
### 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
## [Unreleased]
### Fixed
- 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
@@ -1,45 +1,5 @@
# 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`
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.
:type name: str
:raises InvalidNamespaceError: If the namespace name is invalid.
## Class `PythonPackageNamespace`
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.
:type sub_namespaces: list[PythonNamespace]
:raises InvalidNamespaceError: If the namespace name is invalid.
## Class `PythonModuleNamespace`
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]]
name = "colorama"
@@ -235,14 +235,14 @@ version = "4.16.0"
description = "Backported and Experimental Type Hints for Python 3.9+"
optional = false
python-versions = ">=3.9"
groups = ["main", "dev"]
groups = ["dev"]
markers = "python_version == \"3.10\""
files = [
{file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"},
{file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"},
]
markers = {dev = "python_version == \"3.10\""}
[metadata]
lock-version = "2.1"
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]
name = "jcloud-docsgen"
version = "0.2.2"
version = "0.2.0"
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"
license = "Apache-2.0"
+9 -22
View File
@@ -37,11 +37,7 @@ from .definitions import (
PythonFunctionDefinition,
)
from .docstrings import PythonDocstring
from .namespaces import (
PythonModuleNamespace,
PythonPackageNamespace,
validate_namespace_name,
)
from .namespaces import PythonModuleNamespace, PythonPackageNamespace
__all__ = [
'PythonDefinitionDocumentationIncludeSections',
@@ -930,12 +926,9 @@ class PythonModuleDocumentationGenerator:
),
**kwargs,
) -> None:
if not (
module_path.suffix == '.py'
and validate_namespace_name(module_path.stem)
):
if module_path.name.count('.') > 1:
raise InvalidNamespaceError(
'invalid module file name',
'namespace identifier cannot contain more than one dot',
namespace_identifier=module_path.name,
)
@@ -1114,18 +1107,12 @@ class PythonDocumentationGenerator:
)
for entry in directory.iterdir():
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
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))
return namespace
+3 -26
View File
@@ -14,7 +14,6 @@
from __future__ import annotations
import keyword
from typing import TypeAlias, Union
from ...exceptions import (
@@ -25,47 +24,27 @@ 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 validate_namespace_name(name):
if not name.strip():
raise InvalidNamespaceError(
'invalid namespace', namespace_identifier=name
'invalid namespace',
namespace_identifier=name,
)
self.name = name
@@ -91,8 +70,6 @@ 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__(
+1
View File
@@ -14,6 +14,7 @@
import errno
__all__ = [
'NamespaceError',
'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(
'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(
(
'tests/unit/core/python/_core/i'
'nvalid_module_names/a.b.c.d.e'
'tests/unit/core/python/_core/fil'
'es_with_more_than_one_dot/a.b.c'
),
),
None,
InvalidNamespaceError,
'invalid module file name: a.b.c.d.e',
'namespace identifier cannot contain more than one dot: a.b.c',
),
(
pathlib.Path(
(
'tests/unit/core/python/_core/in'
'valid_module_names/a.b.c.d.e.f'
'tests/unit/core/python/_core/file'
's_with_more_than_one_dot/a.b.c.d'
),
),
None,
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(
(
'tests/unit/core/python/_core/inv'
'alid_module_names/a.b.c.d.e.f.g'
'tests/unit/core/python/_core/files'
'_with_more_than_one_dot/a.b.c.d.e'
),
),
None,
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(
(
'tests/unit/core/python/_core/inva'
'lid_module_names/a.b.c.d.e.f.g.h'
'tests/unit/core/python/_core/files_'
'with_more_than_one_dot/a.b.c.d.e.f'
),
),
None,
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(
(
'tests/unit/core/python/_core/inval'
'id_module_names/a.b.c.d.e.f.g.h.py'
'tests/unit/core/python/_core/files_w'
'ith_more_than_one_dot/a.b.c.d.e.f.g'
),
),
None,
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(
(
'tests/unit/core/python/_core/inva'
'lid_module_names/a.b.c.d.e.f.g.py'
'tests/unit/core/python/_core/files_wi'
'th_more_than_one_dot/a.b.c.d.e.f.g.h'
),
),
None,
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(
(
'tests/unit/core/python/_core/in'
'lid_module_names/a.b.c.d.e.f.py'
'tests/unit/core/python/_core/files_wit'
'h_more_than_one_dot/a.b.c.d.e.f.g.h.py'
),
),
None,
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(
(
'tests/unit/core/python/_core/in'
'valid_module_names/a.b.c.d.e.py'
'tests/unit/core/python/_core/files_wi'
'th_more_than_one_dot/a.b.c.d.e.f.g.py'
),
),
None,
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(
(
'tests/unit/core/python/_core/i'
'nvalid_module_names/a.b.c.d.py'
'tests/unit/core/python/_core/files_w'
'ith_more_than_one_dot/a.b.c.d.e.f.py'
),
),
None,
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(
('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,
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(
('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,
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(
@@ -163,7 +205,7 @@ from src.jcloud_docsgen.exceptions import InvalidNamespaceError
),
None,
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',
[
(PythonModuleNamespace('a'), 'a'),
(PythonModuleNamespace('1'), '1'),
(PythonModuleNamespace(StrSubclass('a')), StrSubclass('a')),
],
)
@@ -40,7 +41,11 @@ 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),
@@ -56,6 +61,7 @@ def test_PythonModuleNamespace___eq__(namespace1, namespace2, expected):
'namespace,expected',
[
(PythonModuleNamespace('a'), "PythonModuleNamespace('a')"),
(PythonModuleNamespace('1'), "PythonModuleNamespace('1')"),
(
PythonModuleNamespace(StrSubclass('a')),
"PythonModuleNamespace('a')",
@@ -24,6 +24,7 @@ class StrSubclass(str): ...
'namespace,expected_name',
[
(PythonNamespace('a'), 'a'),
(PythonNamespace('1'), '1'),
(PythonNamespace(StrSubclass('a')), StrSubclass('a')),
],
)
@@ -35,6 +36,7 @@ def test_PythonNamespace_name_attribute(namespace, expected_name):
'namespace,expected',
[
(PythonNamespace('a'), "PythonNamespace('a')"),
(PythonNamespace('1'), "PythonNamespace('1')"),
(PythonNamespace(StrSubclass('a')), "PythonNamespace('a')"),
],
)
@@ -38,6 +38,13 @@ class StrSubclass(str): ...
),
'a',
),
(
PythonPackageNamespace(
'1',
[],
),
'1',
),
(
PythonPackageNamespace(
StrSubclass('a'),
@@ -284,6 +291,10 @@ def test_PythonPackageNamespace___eq__(namespace1, namespace2, expected):
PythonPackageNamespace('a', []),
"PythonPackageNamespace('a', [])",
),
(
PythonPackageNamespace('1', []),
"PythonPackageNamespace('1', [])",
),
(
PythonPackageNamespace(StrSubclass('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