Add Python module documentation generator

This commit is contained in:
2026-04-25 19:34:34 +02:00
parent ecd3f7e76b
commit a17e56fb3d
18 changed files with 1097 additions and 6 deletions
+2 -1
View File
@@ -24,4 +24,5 @@
- Add feature to parse Python docstrings - Add feature to parse Python docstrings
- Add function to ensure a non-empty string. - Add function to ensure a non-empty string.
- Add Python definition documentation generator - Add Python definition documentation generator
- Add function to get the path segments of a path relative to another - Add function to get the path segments of a path relative to another
- Add Python module documentation generator
+81 -5
View File
@@ -13,8 +13,8 @@
# limitations under the License. # limitations under the License.
from __future__ import annotations from __future__ import annotations
from ...utils import ExistingDirectory, assert_that_is_instance, ExistingFile, non_empty_str from ...utils import ExistingDirectory, assert_that_is_instance, ExistingFile, non_empty_str, get_relative_path_segments
from ...exceptions import PythonArgumentStructureError from ...exceptions import PythonArgumentStructureError, InvalidNamespaceError
from .namespaces import PythonModuleNamespace, PythonPackageNamespace from .namespaces import PythonModuleNamespace, PythonPackageNamespace
from .definitions import PythonDefinition, PythonFunctionDefinition, PythonAsyncFunctionDefinition, PythonClassDefinition from .definitions import PythonDefinition, PythonFunctionDefinition, PythonAsyncFunctionDefinition, PythonClassDefinition
from .arguments import PythonArgumentKind, PythonFunctionArgument from .arguments import PythonArgumentKind, PythonFunctionArgument
@@ -26,6 +26,7 @@ from enum import Enum
import docstring_parser import docstring_parser
import pathlib import pathlib
import ast import ast
import types
__all__ = [ __all__ = [
'PythonDefinitionDocumentationIncludeSections', 'PythonDefinitionDocumentationIncludeSections',
@@ -255,6 +256,7 @@ class _NoDocstringTemplate:
many_returns = [] many_returns = []
deprecation = None deprecation = None
examples = [] examples = []
meta = []
short_description = None short_description = None
long_description = None long_description = None
blank_after_short_description = None blank_after_short_description = None
@@ -408,8 +410,8 @@ class PythonDefinitionDocumentationGenerator:
self, self,
definition: PythonDefinition, definition: PythonDefinition,
include_sections: PythonDefinitionDocumentationIncludeSections = PythonDefinitionDocumentationIncludeSections(), include_sections: PythonDefinitionDocumentationIncludeSections = PythonDefinitionDocumentationIncludeSections(),
*,
level: int = 1, level: int = 1,
*,
allow_html: bool = True, allow_html: bool = True,
allow_tables: bool = False, allow_tables: bool = False,
skip_empty_sections: bool = True, skip_empty_sections: bool = True,
@@ -741,12 +743,49 @@ class PythonModuleDocumentationGenerator:
:param module_path: The path of the module file. :param module_path: The path of the module file.
:type module_path: ExistingFile :type module_path: ExistingFile
:param src_directory: The directory of the source code. It is
optional and only used for making the module
identifiers in the documentation relative.
:type src_directory: Union[ExistingDirectory, None]
:param include_sections: The sections that will be included.
:type include_sections: PythonDefinitionDocumentationIncludeSections
:kwargs: The options for ``PythonDefinitionDocumentationGenerator``.
''' '''
def __init__(self, module_path: ExistingFile) -> None: def __init__(
self,
module_path: ExistingFile,
src_directory: Union[ExistingDirectory, None] = None,
include_sections: PythonDefinitionDocumentationIncludeSections = PythonDefinitionDocumentationIncludeSections(),
**kwargs
) -> None:
assert_that_is_instance(module_path, ExistingFile) assert_that_is_instance(module_path, ExistingFile)
assert_that_is_instance(src_directory, (ExistingDirectory, types.NoneType))
if module_path.name.count('.') > 1:
raise InvalidNamespaceError('namespace identifier cannot contain more than one dot', namespace_identifier = module_path.name)
self.module_path = pathlib.Path(str(module_path)) self.module_path = pathlib.Path(str(module_path))
self.src_directory = pathlib.Path(str(src_directory)) if src_directory is not None else None
self.include_sections = include_sections
self.kwargs = kwargs
self._source_code_cache = None
self._ast_cache = None
@property
def _source_code(self) -> str:
if self._source_code_cache is None:
self._source_code_cache = self.module_path.read_text()
return self._source_code_cache
@property
def _ast(self) -> str:
if self._ast_cache is None:
self._ast_cache = ast.parse(self._source_code)
return self._ast_cache
def collect_definitions(self) -> Iterator[PythonDefinition]: def collect_definitions(self) -> Iterator[PythonDefinition]:
''' '''
@@ -756,10 +795,47 @@ class PythonModuleDocumentationGenerator:
:rtype: Iterator[PythonDefinition] :rtype: Iterator[PythonDefinition]
''' '''
tree = ast.parse(self.module_path.read_text()) tree = self._ast
return _collect_definitions(tree) return _collect_definitions(tree)
def generate_documentation(self) -> str:
'''
Generates the documentation in the markdown format.
:return: The documentation.
:rtype: str
'''
md = '# Module `'
if self.src_directory is not None:
module_identifier = '.'.join(get_relative_path_segments(self.module_path, self.src_directory))
if module_identifier.endswith('.py'):
md += module_identifier[:-3]
else:
md += module_identifier
else:
md += self.module_path.stem
md += '`\n\n'
docstring = ast.get_docstring(self._ast)
if docstring is not None:
md += docstring + '\n\n'
for definition in self.collect_definitions():
md += PythonDefinitionDocumentationGenerator(
definition,
self.include_sections,
2,
**self.kwargs
).generate_documentation() + '\n\n'
return md.strip()
class PythonDocumentationGenerator: class PythonDocumentationGenerator:
''' '''
The class for the documentation generator. The class for the documentation generator.
@@ -0,0 +1,964 @@
# 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 import PythonModuleDocumentationGenerator
from src.jcloud_docsgen.exceptions import InvalidNamespaceError
import pytest
from src.jcloud_docsgen.utils import ExistingFile, ExistingDirectory
@pytest.mark.parametrize('module_path,src_directory,expected_exception,expected_exception_msg', [
(1, 1, TypeError, 'expected \'ExistingFile\', got \'int\''),
(1, None, TypeError, 'expected \'ExistingFile\', got \'int\''),
(42, None, TypeError, 'expected \'ExistingFile\', got \'int\''),
(ExistingFile('tests/unit/utils/test_file'), 1, TypeError, 'expected either \'ExistingDirectory\' or \'NoneType\', got \'int\''),
(ExistingDirectory('tests/'), 1, TypeError, 'expected \'ExistingFile\', got \'ExistingDirectory\''),
(ExistingDirectory('tests'), 1, TypeError, 'expected \'ExistingFile\', got \'ExistingDirectory\''),
(1, ExistingDirectory('tests/'), TypeError, 'expected \'ExistingFile\', got \'int\''),
(1, ExistingDirectory('tests'), TypeError, 'expected \'ExistingFile\', got \'int\''),
(None, ExistingDirectory('tests'), TypeError, 'expected \'ExistingFile\', got \'NoneType\''),
(None, None, TypeError, 'expected \'ExistingFile\', got \'NoneType\''),
(1, None, TypeError, 'expected \'ExistingFile\', got \'int\''),
(ExistingFile('tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c'), None, InvalidNamespaceError, 'namespace identifier cannot contain more than one dot: a.b.c'),
(ExistingFile('tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d'), None, InvalidNamespaceError, 'namespace identifier cannot contain more than one dot: a.b.c.d'),
(ExistingFile('tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e'), None, InvalidNamespaceError, 'namespace identifier cannot contain more than one dot: a.b.c.d.e'),
(ExistingFile('tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.f'), None, InvalidNamespaceError, 'namespace identifier cannot contain more than one dot: a.b.c.d.e.f'),
(ExistingFile('tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.f.g'), None, InvalidNamespaceError, 'namespace identifier cannot contain more than one dot: a.b.c.d.e.f.g'),
(ExistingFile('tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.f.g.h'), None, InvalidNamespaceError, 'namespace identifier cannot contain more than one dot: a.b.c.d.e.f.g.h'),
(ExistingFile('tests/unit/core/python/_core/files_with_more_than_one_dot/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.py'),
(ExistingFile('tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.f.g.py'), None, InvalidNamespaceError, 'namespace identifier cannot contain more than one dot: a.b.c.d.e.f.g.py'),
(ExistingFile('tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.f.py'), None, InvalidNamespaceError, 'namespace identifier cannot contain more than one dot: a.b.c.d.e.f.py'),
(ExistingFile('tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.py'), None, InvalidNamespaceError, 'namespace identifier cannot contain more than one dot: a.b.c.d.e.py'),
(ExistingFile('tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.py'), None, InvalidNamespaceError, 'namespace identifier cannot contain more than one dot: a.b.c.d.py'),
(ExistingFile('tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.py'), None, InvalidNamespaceError, 'namespace identifier cannot contain more than one dot: a.b.c.py'),
(ExistingFile('tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.py'), None, InvalidNamespaceError, 'namespace identifier cannot contain more than one dot: a.b.py'),
(ExistingFile('tests/unit/core/python/_core/directory.with.more.than.one.dot/a.b.py'), None, InvalidNamespaceError, 'namespace identifier cannot contain more than one dot: a.b.py'),
])
def test_PythonModuleDocumentationGenerator_exceptions(module_path, src_directory, expected_exception, expected_exception_msg):
with pytest.raises(expected_exception) as exc_info:
PythonModuleDocumentationGenerator(module_path, src_directory)
assert str(exc_info.value) == expected_exception_msg
@pytest.mark.parametrize('module_documentation_generator,expected', [
(PythonModuleDocumentationGenerator(ExistingFile('tests/unit/core/python/_core/test_project_dirs/pdir_2/src/pkg/module.py'), ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_2/src/'), allow_tables = True, allow_html = False), '''# Module `pkg.module`
A test module.
## Function `add`
Adds two numbers.
### Function signature
```python
add(a: int | float, b: int | float) -> int | float
```
### Returns
The sum.
Return type: `int | float`
### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `a` | `int | float` | normal | -- | The first number. |
| `b` | `int | float` | normal | -- | The second number. |
### Docstring
Adds two numbers.
:param a: The first number.
:type a: int | float
:param b: The second number.
:type b: int | float
:return: The sum.
:rtype: int | float
## Class `Animal`
A class representing an animal.
### Body
#### Constructor (method `__init__`)
##### Method signature
```python
__init__(self, name: str) -> None
```
##### Returns
Return type: `None`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
| `name` | `str` | normal | -- | -- |
#### Method `greet`
Returns a greeting.
##### Method signature
```python
greet(self)
```
##### Returns
The greeting.
Return type: `str`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
##### Docstring
Returns a greeting.
:return: The greeting.
:rtype: str
### Docstring
A class representing an animal.
:param name: Its name.
:type name: str
## Class `Dog`
A dog.
### Base classes
- `Animal`
### Body
#### Method `bark`
Barks.
##### Method signature
```python
bark(self) -> None
```
##### Returns
Return type: `None`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
##### Docstring
Barks.
### Docstring
A dog.'''),
(PythonModuleDocumentationGenerator(ExistingFile('tests/unit/core/python/_core/test_project_dirs/pdir_2/src/pkg/module.py'), None, allow_tables = True, allow_html = False), '''# Module `module`
A test module.
## Function `add`
Adds two numbers.
### Function signature
```python
add(a: int | float, b: int | float) -> int | float
```
### Returns
The sum.
Return type: `int | float`
### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `a` | `int | float` | normal | -- | The first number. |
| `b` | `int | float` | normal | -- | The second number. |
### Docstring
Adds two numbers.
:param a: The first number.
:type a: int | float
:param b: The second number.
:type b: int | float
:return: The sum.
:rtype: int | float
## Class `Animal`
A class representing an animal.
### Body
#### Constructor (method `__init__`)
##### Method signature
```python
__init__(self, name: str) -> None
```
##### Returns
Return type: `None`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
| `name` | `str` | normal | -- | -- |
#### Method `greet`
Returns a greeting.
##### Method signature
```python
greet(self)
```
##### Returns
The greeting.
Return type: `str`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
##### Docstring
Returns a greeting.
:return: The greeting.
:rtype: str
### Docstring
A class representing an animal.
:param name: Its name.
:type name: str
## Class `Dog`
A dog.
### Base classes
- `Animal`
### Body
#### Method `bark`
Barks.
##### Method signature
```python
bark(self) -> None
```
##### Returns
Return type: `None`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
##### Docstring
Barks.
### Docstring
A dog.'''),
(PythonModuleDocumentationGenerator(ExistingFile('tests/unit/core/python/_core/test_project_dirs/pdir_2/src/pkg/module.py'), None, allow_tables = True, allow_html = True), '''# Module `module`
A test module.
## Function `add`
Adds two numbers.
### Function signature
```python
add(a: int | float, b: int | float) -> int | float
```
### Returns
The sum.
Return type: `int | float`
### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `a` | `int | float` | normal | -- | The first number. |
| `b` | `int | float` | normal | -- | The second number. |
<details><summary><h3 style="display:inline">Docstring</h3></summary>Adds two numbers.
:param a: The first number.
:type a: int | float
:param b: The second number.
:type b: int | float
:return: The sum.
:rtype: int | float</details>
## Class `Animal`
A class representing an animal.
### Body
#### Constructor (method `__init__`)
##### Method signature
```python
__init__(self, name: str) -> None
```
##### Returns
Return type: `None`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
| `name` | `str` | normal | -- | -- |
#### Method `greet`
Returns a greeting.
##### Method signature
```python
greet(self)
```
##### Returns
The greeting.
Return type: `str`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
<details><summary><h5 style="display:inline">Docstring</h5></summary>Returns a greeting.
:return: The greeting.
:rtype: str</details>
<details><summary><h3 style="display:inline">Docstring</h3></summary>A class representing an animal.
:param name: Its name.
:type name: str</details>
## Class `Dog`
A dog.
### Base classes
- `Animal`
### Body
#### Method `bark`
Barks.
##### Method signature
```python
bark(self) -> None
```
##### Returns
Return type: `None`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
<details><summary><h5 style="display:inline">Docstring</h5></summary>Barks.</details>
<details><summary><h3 style="display:inline">Docstring</h3></summary>A dog.</details>'''),
(PythonModuleDocumentationGenerator(ExistingFile('tests/unit/core/python/_core/test_project_dirs/pdir_2/src/pkg/module.py'), None, allow_tables = False, allow_html = True), '''# Module `module`
A test module.
## Function `add`
Adds two numbers.
### Function signature
```python
add(a: int | float, b: int | float) -> int | float
```
### Returns
The sum.
Return type: `int | float`
### Parameters
- Parameter: `a`
Type: `int | float`
Kind: normal
Default value: --
Description: The first number.
- Parameter: `b`
Type: `int | float`
Kind: normal
Default value: --
Description: The second number.
<details><summary><h3 style="display:inline">Docstring</h3></summary>Adds two numbers.
:param a: The first number.
:type a: int | float
:param b: The second number.
:type b: int | float
:return: The sum.
:rtype: int | float</details>
## Class `Animal`
A class representing an animal.
### Body
#### Constructor (method `__init__`)
##### Method signature
```python
__init__(self, name: str) -> None
```
##### Returns
Return type: `None`
##### Parameters
- Parameter: `self`
Type: --
Kind: normal
Default value: --
Description: --
- Parameter: `name`
Type: `str`
Kind: normal
Default value: --
Description: --
#### Method `greet`
Returns a greeting.
##### Method signature
```python
greet(self)
```
##### Returns
The greeting.
Return type: `str`
##### Parameters
- Parameter: `self`
Type: --
Kind: normal
Default value: --
Description: --
<details><summary><h5 style="display:inline">Docstring</h5></summary>Returns a greeting.
:return: The greeting.
:rtype: str</details>
<details><summary><h3 style="display:inline">Docstring</h3></summary>A class representing an animal.
:param name: Its name.
:type name: str</details>
## Class `Dog`
A dog.
### Base classes
- `Animal`
### Body
#### Method `bark`
Barks.
##### Method signature
```python
bark(self) -> None
```
##### Returns
Return type: `None`
##### Parameters
- Parameter: `self`
Type: --
Kind: normal
Default value: --
Description: --
<details><summary><h5 style="display:inline">Docstring</h5></summary>Barks.</details>
<details><summary><h3 style="display:inline">Docstring</h3></summary>A dog.</details>'''),
(PythonModuleDocumentationGenerator(ExistingFile('tests/unit/core/python/_core/test_project_dirs/pdir_2/src/pkg/module.py'), None, allow_tables = False, allow_html = False), '''# Module `module`
A test module.
## Function `add`
Adds two numbers.
### Function signature
```python
add(a: int | float, b: int | float) -> int | float
```
### Returns
The sum.
Return type: `int | float`
### Parameters
- Parameter: `a`
Type: `int | float`
Kind: normal
Default value: --
Description: The first number.
- Parameter: `b`
Type: `int | float`
Kind: normal
Default value: --
Description: The second number.
### Docstring
Adds two numbers.
:param a: The first number.
:type a: int | float
:param b: The second number.
:type b: int | float
:return: The sum.
:rtype: int | float
## Class `Animal`
A class representing an animal.
### Body
#### Constructor (method `__init__`)
##### Method signature
```python
__init__(self, name: str) -> None
```
##### Returns
Return type: `None`
##### Parameters
- Parameter: `self`
Type: --
Kind: normal
Default value: --
Description: --
- Parameter: `name`
Type: `str`
Kind: normal
Default value: --
Description: --
#### Method `greet`
Returns a greeting.
##### Method signature
```python
greet(self)
```
##### Returns
The greeting.
Return type: `str`
##### Parameters
- Parameter: `self`
Type: --
Kind: normal
Default value: --
Description: --
##### Docstring
Returns a greeting.
:return: The greeting.
:rtype: str
### Docstring
A class representing an animal.
:param name: Its name.
:type name: str
## Class `Dog`
A dog.
### Base classes
- `Animal`
### Body
#### Method `bark`
Barks.
##### Method signature
```python
bark(self) -> None
```
##### Returns
Return type: `None`
##### Parameters
- Parameter: `self`
Type: --
Kind: normal
Default value: --
Description: --
##### Docstring
Barks.
### Docstring
A dog.'''),
])
def test_PythonModuleDocumentationGenerator_exceptions(module_documentation_generator, expected):
assert module_documentation_generator.generate_documentation() == expected
@@ -0,0 +1,50 @@
'''
A test module.
'''
def add(a: int | float, b: int | float) -> int | float:
'''
Adds two numbers.
:param a: The first number.
:type a: int | float
:param b: The second number.
:type b: int | float
:return: The sum.
:rtype: int | float
'''
return a + b
class Animal:
'''
A class representing an animal.
:param name: Its name.
:type name: str
'''
def __init__(self, name: str) -> None:
self.name = name
def greet(self):
'''
Returns a greeting.
:return: The greeting.
:rtype: str
'''
return f'Hi there! My name is {self.name}'
class Dog(Animal):
'''
A dog.
'''
def bark(self) -> None:
'''
Barks.
'''
print('Woof!')