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 class for existing files
- Add classes for Python function arguments - Add classes for Python function arguments
- Add classes for Python definitions - Add classes for Python definitions
- Add class for Python modules
+49 -11
View File
@@ -13,23 +13,61 @@
# limitations under the License. # limitations under the License.
from __future__ import annotations from __future__ import annotations
from ...utils import ExistingDirectory, assert_that_is_instance from ...utils import ExistingDirectory, assert_that_is_instance, ExistingFile
from .namespaces import PythonModuleNamespace, PythonPackageNamespace, PythonNamespace from .namespaces import PythonModuleNamespace, PythonPackageNamespace
from typing import List from .arguments import PythonASTArgumentsListParser
from .definitions import PythonDefinition, PythonFunctionDefinition, PythonAsyncFunctionDefinition, PythonClassDefinition
import pathlib import pathlib
import ast
from collections.abc import Iterator
__all__ = [ __all__ = [
'PythonDocumentationGenerator' 'PythonModuleDocumentationGenerator',
'PythonDocumentationGenerator',
] ]
# class PythonModuleDocumentationGenerator: def _collect_definitions(tree_or_node) -> Iterator[PythonDefinition]:
# ''' '''
# A documentation generator for a Python module. 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: class PythonDocumentationGenerator:
''' '''
@@ -59,12 +97,12 @@ class PythonDocumentationGenerator:
return namespace return namespace
def namespace(self) -> List[PythonPackageNamespace]: def namespace(self) -> list[PythonPackageNamespace]:
''' '''
Returns the project as a namespace. Returns the project as a namespace.
:return: The project as a namespace. :return: The project as a namespace.
:rtype: PythonPackageNamespace :rtype: list[PythonPackageNamespace]
''' '''
src_dir = self.project_directory / 'src' 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