Add class for Python modules

This commit is contained in:
2026-04-13 09:24:36 +02:00
parent 8c3866a948
commit a79b538de8
3 changed files with 189 additions and 12 deletions
+1
View File
@@ -18,3 +18,4 @@
- Add class for existing files
- Add classes for Python function arguments
- Add classes for Python definitions
- Add class for Python modules
+49 -11
View File
@@ -13,23 +13,61 @@
# limitations under the License.
from __future__ import annotations
from ...utils import ExistingDirectory, assert_that_is_instance
from .namespaces import PythonModuleNamespace, PythonPackageNamespace, PythonNamespace
from typing import List
from ...utils import ExistingDirectory, assert_that_is_instance, ExistingFile
from .namespaces import PythonModuleNamespace, PythonPackageNamespace
from .arguments import PythonASTArgumentsListParser
from .definitions import PythonDefinition, PythonFunctionDefinition, PythonAsyncFunctionDefinition, PythonClassDefinition
import pathlib
import ast
from collections.abc import Iterator
__all__ = [
'PythonDocumentationGenerator'
'PythonModuleDocumentationGenerator',
'PythonDocumentationGenerator',
]
# class PythonModuleDocumentationGenerator:
# '''
# A documentation generator for a Python module.
def _collect_definitions(tree_or_node) -> Iterator[PythonDefinition]:
'''
Collects all definitions of the tree or node.
:param tree_or_node: The tree or node.
# '''
:return: The definitions.
:rtype: Iterator[PythonDefinition]
'''
# def __init__(self, module_path: )
for node in tree_or_node.body:
if isinstance(node, ast.FunctionDef):
yield PythonFunctionDefinition.from_node(node)
if isinstance(node, ast.AsyncFunctionDef):
yield PythonAsyncFunctionDefinition.from_node(node)
if isinstance(node, ast.ClassDef):
yield PythonClassDefinition.from_node(node)
class PythonModuleDocumentationGenerator:
'''
A documentation generator for a Python module.
:param module_path: The path of the module file.
:type module_path: ExistingFile
'''
def __init__(self, module_path: ExistingFile) -> None:
assert_that_is_instance(module_path, ExistingFile)
self.module_path = pathlib.Path(str(module_path))
def collect_definitions(self) -> Iterator[PythonDefinition]:
'''
Collects all function and class definitions of the module.
:return: All definitions of the module
:rtype: Iterator[PythonDefinition]
'''
tree = ast.parse(self.module_path.read_text())
return _collect_definitions(tree)
class PythonDocumentationGenerator:
'''
@@ -59,12 +97,12 @@ class PythonDocumentationGenerator:
return namespace
def namespace(self) -> List[PythonPackageNamespace]:
def namespace(self) -> list[PythonPackageNamespace]:
'''
Returns the project as a namespace.
:return: The project as a namespace.
:rtype: PythonPackageNamespace
:rtype: list[PythonPackageNamespace]
'''
src_dir = self.project_directory / 'src'
@@ -0,0 +1,138 @@
# Copyright 2026 jCloud Services GbR
# 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.
from src.jcloud_docsgen.core.python._core import _collect_definitions
from src.jcloud_docsgen.core.python.definitions import PythonClassDefinition, PythonFunctionDefinition
from src.jcloud_docsgen.core.python.arguments import PythonFunctionArgument, PythonArgumentKind
import pytest
import ast
def module(source_code: str) -> ast.Module:
'''
Returns an AST module of the source code.
:param source_code: The module source code.
:type source_code: str
:return: The AST module.
:rtype: ast.Module
'''
return ast.parse(source_code)
@pytest.mark.parametrize('module,expected', [
(module('def add(a, b):\n\treturn a + b'), [
PythonFunctionDefinition('add', [
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, None),
PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, None),
], None, None, [])
]),
(module('''def add(a: int | float, b: int | float) -> int | float:
"""Adds two numbers."""
return a + b
import typing
class Something:
"""A class."""
def __init__(self, value: typing.Any = 42) -> None:
self._value = value
@property
def value(self) -> typing.Any:
"""
The value.
"""
return self.get_value()
def get_value(self) -> typing.Any:
"""
Returns the value.
"""
return self._value
'''), [
PythonFunctionDefinition('add', [
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, 'int | float'),
PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, 'int | float'),
], 'int | float', 'Adds two numbers.', []),
PythonClassDefinition('Something', [], 'A class.', [], [
PythonFunctionDefinition('__init__', [
PythonFunctionArgument('self', PythonArgumentKind.NORMAL, None, None),
PythonFunctionArgument('value', PythonArgumentKind.NORMAL, '42', 'typing.Any'),
], 'None', None, []),
PythonFunctionDefinition('value', [
PythonFunctionArgument('self', PythonArgumentKind.NORMAL, None, None),
], 'typing.Any', 'The value.', ['property']),
PythonFunctionDefinition('get_value', [
PythonFunctionArgument('self', PythonArgumentKind.NORMAL, None, None),
], 'typing.Any', 'Returns the value.', []),
])
]),
(module('''from src.jcloud_docsgen.core.python import PythonDocumentationGenerator
from src.jcloud_docsgen.core.python.namespaces import PythonModuleNamespace, PythonPackageNamespace
import pytest
from src.jcloud_docsgen.utils import ExistingDirectory
@pytest.mark.parametrize('project_directory,docs_directory', [
(1, 1),
(1, None),
(42, None),
(ExistingDirectory('tests'), 1),
(ExistingDirectory('tests/'), 1),
(1, ExistingDirectory('tests/')),
(1, ExistingDirectory('tests')),
(None, ExistingDirectory('tests')),
])
def test_PythonDocumentationGenerator_type_exceptions(project_directory, docs_directory):
with pytest.raises(TypeError):
PythonDocumentationGenerator(project_directory, docs_directory)
@pytest.mark.parametrize('python_documentation_generator,expected', [
(PythonDocumentationGenerator(ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_1'), ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_1/docs')), []),
(PythonDocumentationGenerator(ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_2'), ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_2/docs')), [PythonPackageNamespace('pkg', [PythonModuleNamespace('module')])]),
(PythonDocumentationGenerator(ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_3'), ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_3/docs')), [PythonPackageNamespace('pkg', [PythonModuleNamespace('module'), PythonPackageNamespace('pkg', [PythonModuleNamespace('module2')])])]),
])
def test_PythonDocumentationGenerator_collect_modules(python_documentation_generator: PythonDocumentationGenerator, expected):
assert python_documentation_generator.namespace() == expected'''), [
PythonFunctionDefinition('test_PythonDocumentationGenerator_type_exceptions', [
PythonFunctionArgument('project_directory', PythonArgumentKind.NORMAL, None, None),
PythonFunctionArgument('docs_directory', PythonArgumentKind.NORMAL, None, None),
], None, None, ['''pytest.mark.parametrize('project_directory,docs_directory', [(1, 1), (1, None), (42, None), (ExistingDirectory('tests'), 1), (ExistingDirectory('tests/'), 1), (1, ExistingDirectory('tests/')), (1, ExistingDirectory('tests')), (None, ExistingDirectory('tests'))])''']),
PythonFunctionDefinition('test_PythonDocumentationGenerator_collect_modules', [
PythonFunctionArgument('python_documentation_generator', PythonArgumentKind.NORMAL, None, 'PythonDocumentationGenerator'),
PythonFunctionArgument('expected', PythonArgumentKind.NORMAL, None, None)
], None, None, ['''pytest.mark.parametrize('python_documentation_generator,expected', [(PythonDocumentationGenerator(ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_1'), ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_1/docs')), []), (PythonDocumentationGenerator(ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_2'), ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_2/docs')), [PythonPackageNamespace('pkg', [PythonModuleNamespace('module')])]), (PythonDocumentationGenerator(ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_3'), ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_3/docs')), [PythonPackageNamespace('pkg', [PythonModuleNamespace('module'), PythonPackageNamespace('pkg', [PythonModuleNamespace('module2')])])])])'''])
]),
(module('''class C1:
class C2:
class C3:
def function(self, value: bool, /, arg) -> bool:
def should_be_not_in_the_result(arg: int) -> object:
pass
return not value'''), [
PythonClassDefinition('C1', [], None, [], [
PythonClassDefinition('C2', [], None, [], [
PythonClassDefinition('C3', [], None, [], [
PythonFunctionDefinition('function', [
PythonFunctionArgument('self', PythonArgumentKind.POSITIONAL_ONLY, None, None),
PythonFunctionArgument('value', PythonArgumentKind.POSITIONAL_ONLY, None, 'bool'),
PythonFunctionArgument('arg', PythonArgumentKind.NORMAL, None, None),
], 'bool', None, [])
])
])
])
])
])
def test___collect_definitions(module, expected):
assert list(_collect_definitions(module)) == expected