diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index ee906d5..a19c217 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -24,4 +24,5 @@
- Add feature to parse Python docstrings
- Add function to ensure a non-empty string.
- Add Python definition documentation generator
-- Add function to get the path segments of a path relative to another
\ No newline at end of file
+- Add function to get the path segments of a path relative to another
+- Add Python module documentation generator
\ No newline at end of file
diff --git a/src/jcloud_docsgen/core/python/_core.py b/src/jcloud_docsgen/core/python/_core.py
index 497d364..932a6b5 100644
--- a/src/jcloud_docsgen/core/python/_core.py
+++ b/src/jcloud_docsgen/core/python/_core.py
@@ -13,8 +13,8 @@
# limitations under the License.
from __future__ import annotations
-from ...utils import ExistingDirectory, assert_that_is_instance, ExistingFile, non_empty_str
-from ...exceptions import PythonArgumentStructureError
+from ...utils import ExistingDirectory, assert_that_is_instance, ExistingFile, non_empty_str, get_relative_path_segments
+from ...exceptions import PythonArgumentStructureError, InvalidNamespaceError
from .namespaces import PythonModuleNamespace, PythonPackageNamespace
from .definitions import PythonDefinition, PythonFunctionDefinition, PythonAsyncFunctionDefinition, PythonClassDefinition
from .arguments import PythonArgumentKind, PythonFunctionArgument
@@ -26,6 +26,7 @@ from enum import Enum
import docstring_parser
import pathlib
import ast
+import types
__all__ = [
'PythonDefinitionDocumentationIncludeSections',
@@ -255,6 +256,7 @@ class _NoDocstringTemplate:
many_returns = []
deprecation = None
examples = []
+ meta = []
short_description = None
long_description = None
blank_after_short_description = None
@@ -408,8 +410,8 @@ class PythonDefinitionDocumentationGenerator:
self,
definition: PythonDefinition,
include_sections: PythonDefinitionDocumentationIncludeSections = PythonDefinitionDocumentationIncludeSections(),
- *,
level: int = 1,
+ *,
allow_html: bool = True,
allow_tables: bool = False,
skip_empty_sections: bool = True,
@@ -741,12 +743,49 @@ class PythonModuleDocumentationGenerator:
:param module_path: The path of the module file.
: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(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.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]:
'''
@@ -756,10 +795,47 @@ class PythonModuleDocumentationGenerator:
:rtype: Iterator[PythonDefinition]
'''
- tree = ast.parse(self.module_path.read_text())
+ tree = self._ast
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:
'''
The class for the documentation generator.
diff --git a/tests/unit/core/python/_core/directory.with.more.than.one.dot/a.b.py b/tests/unit/core/python/_core/directory.with.more.than.one.dot/a.b.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c b/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d b/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e b/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.f b/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.f
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.f.g b/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.f.g
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.f.g.h b/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.f.g.h
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.f.g.h.py b/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.f.g.h.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.f.g.py b/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.f.g.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.f.py b/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.f.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.py b/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.py b/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.py b/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.py b/tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unit/core/python/_core/test_PythonModuleDocumentationGenerator.py b/tests/unit/core/python/_core/test_PythonModuleDocumentationGenerator.py
new file mode 100644
index 0000000..f5cf865
--- /dev/null
+++ b/tests/unit/core/python/_core/test_PythonModuleDocumentationGenerator.py
@@ -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. |
+
+
+
+
+
+
+
+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 = 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.
+
+
+
+
+
+
+
+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. '''),
+
+ (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
\ No newline at end of file
diff --git a/tests/unit/core/python/_core/test_project_dirs/pdir_2/src/pkg/module.py b/tests/unit/core/python/_core/test_project_dirs/pdir_2/src/pkg/module.py
index e69de29..050fcb4 100644
--- a/tests/unit/core/python/_core/test_project_dirs/pdir_2/src/pkg/module.py
+++ b/tests/unit/core/python/_core/test_project_dirs/pdir_2/src/pkg/module.py
@@ -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!')
\ No newline at end of file