diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 36c228e..203ccb9 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -22,4 +22,5 @@
- Add feature to get the signature representation of a Python function argument
- Add class for Python docstrings
- Add feature to parse Python docstrings
-- Add function to ensure a non-empty string.
\ No newline at end of file
+- Add function to ensure a non-empty string.
+- Add Python definition 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 209954a..497d364 100644
--- a/src/jcloud_docsgen/core/python/_core.py
+++ b/src/jcloud_docsgen/core/python/_core.py
@@ -13,15 +13,23 @@
# limitations under the License.
from __future__ import annotations
-from ...utils import ExistingDirectory, assert_that_is_instance, ExistingFile
+from ...utils import ExistingDirectory, assert_that_is_instance, ExistingFile, non_empty_str
+from ...exceptions import PythonArgumentStructureError
from .namespaces import PythonModuleNamespace, PythonPackageNamespace
-from .arguments import PythonASTArgumentsListParser
from .definitions import PythonDefinition, PythonFunctionDefinition, PythonAsyncFunctionDefinition, PythonClassDefinition
+from .arguments import PythonArgumentKind, PythonFunctionArgument
+from .docstrings import PythonDocstring
+from collections.abc import Iterator
+from typing import Union
+from dataclasses import dataclass
+from enum import Enum
+import docstring_parser
import pathlib
import ast
-from collections.abc import Iterator
__all__ = [
+ 'PythonDefinitionDocumentationIncludeSections',
+ 'PythonDefinitionDocumentationGenerator',
'PythonModuleDocumentationGenerator',
'PythonDocumentationGenerator',
]
@@ -44,6 +52,689 @@ def _collect_definitions(tree_or_node) -> Iterator[PythonDefinition]:
if isinstance(node, ast.ClassDef):
yield PythonClassDefinition.from_node(node)
+def _group_argument_list(argument_list: list[PythonFunctionArgument]) -> tuple[list[PythonFunctionArgument], list[PythonFunctionArgument], Union[PythonFunctionArgument, None], list[PythonFunctionArgument], Union[PythonFunctionArgument, None]]:
+ '''
+ Groups the argument list into a list of positional-only arguments, a
+ list of normal arguments, the * argument if there is one, a list of
+ keyword-only arguments and the var-keyword-argument if there is one.
+
+ :param argument_list: The argument list.
+ :type argument_list: list[PythonFunctionArgument]
+
+ :rtype: tuple[list[PythonFunctionArgument], list[PythonFunctionArgument], Union[PythonFunctionArgument, None], list[PythonFunctionArgument], Union[PythonFunctionArgument, None]]
+ '''
+
+ if argument_list:
+ if argument_list[-1].kind != PythonArgumentKind.KWARGS and any([
+ arg.kind == PythonArgumentKind.KWARGS
+ for arg in argument_list
+ ]):
+ raise PythonArgumentStructureError('arguments cannot follow var-keyword argument')
+
+ positional_only_args = []
+ normal_args = []
+ vararg = None
+ keyword_only_args = []
+ kwargs = None
+
+ for arg in argument_list:
+ if arg.kind == PythonArgumentKind.POSITIONAL_ONLY:
+ positional_only_args.append(arg)
+
+ if arg.kind == PythonArgumentKind.NORMAL:
+ normal_args.append(arg)
+
+ if arg.kind == PythonArgumentKind.VARARG:
+ if vararg is not None:
+ raise PythonArgumentStructureError('* argument may only appear once', definition = arg)
+ vararg = arg
+
+ if arg.kind == PythonArgumentKind.KEYWORD_ONLY:
+ keyword_only_args.append(arg)
+
+ if arg.kind == PythonArgumentKind.KWARGS:
+ kwargs = arg
+
+ return positional_only_args, normal_args, vararg, keyword_only_args, kwargs
+
+def _signature_argument_list(argument_list: list[PythonFunctionArgument]) -> list[str]:
+ '''
+ Returns a list of the arguments as strings for a function signature.
+
+ :param argument_list: The argument list.
+ :type argument_list: list[PythonFunctionArgument]
+
+ :return: The list of function signature argument strings.
+ :rtype: list[str]
+ '''
+
+ positional_only_args, normal_args, vararg, keyword_only_args, kwargs = _group_argument_list(argument_list)
+
+ args = []
+
+ for arg in positional_only_args:
+ args.append(arg.signature_repr())
+
+ if positional_only_args:
+ args.append('/')
+
+ for arg in normal_args:
+ args.append(arg.signature_repr())
+
+ if vararg is not None:
+ args.append(vararg.signature_repr())
+
+ if keyword_only_args and vararg is None:
+ args.append('*')
+
+ for arg in keyword_only_args:
+ args.append(arg.signature_repr())
+
+ if kwargs is not None:
+ args.append(kwargs.signature_repr())
+
+ return args
+
+def _fit_row_length(expected: int, row: tuple) -> tuple:
+ '''
+ Fits the row length to the expected length.
+
+ If the row is too long, the tail, that makes it too long, is omitted.
+ If it is too short, it is filled with ``None`` (at the end).
+
+ :param expected: The expected length.
+ :type expected: int
+ :param row: The row.
+ :type row: tuple
+ '''
+
+ if expected < 0:
+ raise ValueError('expected must be larger than or equal 0.')
+
+ if len(row) > expected:
+ return row[:expected]
+ else:
+ return row + (None,) * (expected - len(row))
+
+def _markdown_table(header: tuple, body: list[tuple], *, allow_markdown_table: bool = False, force_row_lengths: bool = False, one_line_values: bool = True, missing_value: str = '--') -> str:
+ '''
+ Generates a table.
+
+ If ``allow_markdown_table`` is ``False``, an alternative
+ representation is used.
+
+ :param headers: The table headers.
+ :type headers: tuple
+ :param body: The table body.
+ :type body: list[tuple]
+ :param allow_markdown_table: Controls whether a markdown table may be
+ used.
+ :type allow_markdown_table: bool
+ :param force_row_lengths: Controls whether an exception is raised if
+ a row does not have the correct length.
+ :type force_row_lengths: bool
+ :param one_line_values: Controls whether all newlines in all values
+ are replaced with spaces.
+ :type one_line_values: bool
+ :param missing_value: The string that is used if a value is missing
+ (only if ``force_row_lengths`` is ``False``).
+ :type missing_value: str
+ '''
+
+ if not header or not body:
+ return ''
+
+ header_len = len(header)
+
+ if allow_markdown_table:
+ table = '| ' + ' | '.join(header) + ' |\n|' + ' --- |' * len(header) + '\n'
+
+
+ for row in body:
+ if len(row) != header_len:
+ if force_row_lengths:
+ raise ValueError(f'expected {header_len} value{"s" if header_len > 1 else ""}, got {len(row)}')
+ else:
+ row = _fit_row_length(header_len, row)
+
+ table += '| ' + ' | '.join([
+ str(col if col is not None else missing_value).replace('\n', ' ')
+ if one_line_values
+ else str(col if col is not None else missing_value)
+ for col in row
+ ]) + ' |\n'
+ else:
+ table = ''
+
+ for row in body:
+ if len(row) != header_len:
+ if force_row_lengths:
+ raise ValueError(f'expected {header_len} value{"s" if header_len > 1 else ""}, got {len(row)}')
+ else:
+ row = _fit_row_length(header_len, row)
+
+ sep = '- '
+ for i, col in enumerate(row):
+ table += sep + header[i] + ': ' + (str(col if col is not None else missing_value).replace('\n', ' ') \
+ if one_line_values \
+ else str(col if col is not None else missing_value)) + '\n\n'
+ sep = ' '
+
+ return table.strip()
+
+def _is_docstring(docstring: Union[str, PythonDocstring, None]) -> bool:
+ '''
+ Checks whether the docstring is a docstring or an empty docstring.
+
+ :param docstring: The docstring.
+ :type docstring: Union[str, PythonDocstring, None]
+
+ :return: Whether there is a docstring.
+ :rtype: bool
+ '''
+
+ if docstring is None:
+ return False
+
+ if isinstance(docstring, PythonDocstring):
+ docstring = docstring.docstring
+
+ return docstring.strip() != ''
+
+@dataclass
+class _FunctionParamInfo:
+ annotation: Union[str, None]
+ description: Union[str, None]
+
+@dataclass
+class _NoDocstringTemplate:
+ description = None
+ params = []
+ raises = []
+ returns = None
+ many_returns = []
+ deprecation = None
+ examples = []
+ short_description = None
+ long_description = None
+ blank_after_short_description = None
+
+_NoDocstring = _NoDocstringTemplate()
+
+def _argument_kind_human_readable(argument_kind: PythonArgumentKind) -> str:
+ '''
+ Returns the human-readable representation of a
+ ``PythonArgumentKind``.
+
+ :param argument_kind: The argument kind.
+ :type argument_kind: PythonArgumentKind
+
+ :return: The human-readable representation.
+ :rtype: str
+ '''
+
+ return argument_kind.name.lower().replace('_', '-')
+
+def _function_params_info(arg_list: list[PythonFunctionArgument], docstring_arg_list: list[docstring_parser.DocstringParam]) -> dict[str, _FunctionParamInfo]:
+ '''
+ Returns a dictionary with the parameter info.
+
+ The keys of the dictionary are the parameter names and the values are
+ their types. If there is no type annotation for a parameter, the type
+ in the docstring is used as a fallback. If the docstring also does
+ not contain the type for a parameter, its value in the dictionary is
+ set to ``None``.
+
+ :param arg_list: The list of parameters from the function parameter
+ list.
+ :type arg_list: list[PythonFunctionArgument]
+ :param docstring_arg_list: The list of parameters from the docstring
+ parameter list.
+
+ :return: The dictionary with the parameter types.
+ :rtype: dict[str, _FunctionParamInfo]
+ '''
+
+ arg_annotations = {arg.name: _FunctionParamInfo(arg.annotation, None) for arg in arg_list}
+
+ for arg in docstring_arg_list:
+ if arg.arg_name in arg_annotations.keys():
+ arg_annotations[arg.arg_name] = _FunctionParamInfo(
+ arg_annotations[arg.arg_name].annotation or arg.type_name,
+ arg.description
+ )
+
+ return arg_annotations
+
+class PythonDefinitionDocumentationIncludeSections:
+ '''
+ A class representing the included sections in a Python definition
+ documentation.
+
+ :param signature: Whether include the signature section.
+ :type signature: bool
+ :param returns: Whether include the returns section.
+ :type returns: bool
+ :param params: Whether include the params section.
+ :type params: bool
+ :param exceptions: Whether include the exceptions section.
+ :type exceptions: bool
+ :param examples: Whether include the examples section.
+ :type examples: bool
+ :param docstring: Whether include the docstring section.
+ :type docstring: bool
+ :param bases: Whether include the bases section.
+ :type bases: bool
+ :param decorators: Whether include the decorators section.
+ :type decorators: bool
+ '''
+
+ def __init__(
+ self,
+ *,
+ signature: bool = True,
+ returns: bool = True,
+ params: bool = True,
+ exceptions: bool = True,
+ examples: bool = True,
+ docstring: bool = True,
+ bases: bool = True,
+ decorators: bool = True,
+ class_body: bool = True
+ ):
+ self.signature = signature
+ self.returns = returns
+ self.params = params
+ self.exceptions = exceptions
+ self.examples = examples
+ self.docstring = docstring
+ self.bases = bases
+ self.decorators = decorators
+ self.class_body = class_body
+
+ self._args = {
+ 'signature': self.signature,
+ 'returns': self.returns,
+ 'params': self.params,
+ 'exceptions': self.exceptions,
+ 'examples': self.examples,
+ 'docstring': self.docstring,
+ 'bases': self.bases,
+ 'decorators': self.decorators,
+ 'class_body': self.class_body,
+ }
+
+ def __repr__(self) -> str:
+ return type(self).__name__+ '(' + ', '.join(
+ [
+ key + ' = ' + repr(value)
+ for key, value in self._args.items()
+ ]
+ ) + ')'
+
+ def __eq__(self, value) -> str:
+ if not isinstance(value, PythonDefinitionDocumentationIncludeSections):
+ return False
+ return self._args == value._args
+
+class PythonDefinitionDocumentationGenerator:
+ '''
+ A documentation generator for a Python definition.
+
+ :param definition: The Python definition.
+ :type definition: PythonDefinition
+ :param include_sections: The sections that will be included.
+ :type include_sections: PythonDefinitionDocumentationIncludeSections
+ :param level: The level of the definition in the markdown content.
+ :type level: int
+ :param allow_html: Controls whether the generated documentation may
+ contain HTML.
+ :type allow_html: bool
+ :param allow_tables: Controls whether the generated documentation may
+ contain tables.
+ :type allow_tables: bool
+ :param skip_empty_sections: Controls whether sections that does not
+ have any content (such as the parameters
+ section if there are no parameters taken)
+ are skipped. If ``False``, a short text
+ (such as 'This function takes no
+ parameters') is used.
+ :type skip_empty_sections: bool
+ :param is_method: Controls whether the definition is a class method.
+ :type is_method: bool
+ '''
+
+ def __init__(
+ self,
+ definition: PythonDefinition,
+ include_sections: PythonDefinitionDocumentationIncludeSections = PythonDefinitionDocumentationIncludeSections(),
+ *,
+ level: int = 1,
+ allow_html: bool = True,
+ allow_tables: bool = False,
+ skip_empty_sections: bool = True,
+ is_method: bool = False
+ ) -> None:
+ self.definition = definition
+ self.include_sections = include_sections
+
+ self.level = level
+ self.allow_html = allow_html
+ self.allow_tables = allow_tables
+ self.skip_empty_sections = skip_empty_sections
+ self.is_method = is_method
+
+ self._docstring_cache = None
+
+ @property
+ def _docstring(self):
+ if self._docstring_cache is None:
+ self._docstring_cache = self.definition.doc.parse() if _is_docstring(self.definition.doc) else _NoDocstring
+ return self._docstring_cache
+
+ def generate_signature(self) -> str:
+ '''
+ Generates the signature of the definition. Works only if the
+ definition is a function or asynchronous function definition.
+
+ :raises TypeError: If the definition is a class definition.
+
+ :return: The signature.
+ :rtype: str
+ '''
+
+ if isinstance(self.definition, PythonClassDefinition):
+ raise TypeError('Cannot generate a signature of a class definition.')
+
+ signature = self.definition.name + '(' + ', '.join(_signature_argument_list(self.definition.args)) + ')'
+
+ if self.definition.returns is not None:
+ signature += ' -> ' + self.definition.returns
+
+ return signature.strip()
+
+ def _generate_signature_section(self) -> str:
+ '''
+ Generates the signature section.
+
+ :return: The signature section.
+ :rtype: str
+ '''
+
+ return self.level * '#' + '# ' + ('Function' if not self.is_method else 'Method') +' signature\n\n```python\n' + self.generate_signature() + '\n```'
+
+ def _generate_docstring_section(self) -> str:
+ '''
+ Generates the docstring section.
+
+ :return: The docstring section.
+ :rtype: str
+ '''
+
+ md = ''
+
+ if _is_docstring(self.definition.doc) or not self.skip_empty_sections:
+ if self.allow_html:
+ md += 'Docstring' + '
'
+ else:
+ md += self.level * '#' + '# Docstring\n\n'
+
+ if _is_docstring(self.definition.doc):
+ md += self.definition.doc.docstring
+ elif not self.skip_empty_sections:
+ md += 'There is no docstring to show.'
+
+ if _is_docstring(self.definition.doc) or not self.skip_empty_sections:
+ if self.allow_html:
+ md += ' '
+ md += '\n\n'
+
+ return md.strip()
+
+ def _generate_params_section(self) -> str:
+ '''
+ Generates the parameters section.
+
+ :return: The parameters section.
+ :rtype: str
+ '''
+
+ md = ''
+
+ if self.definition.args or not self.skip_empty_sections:
+ md += self.level * '#' + '# Parameters\n\n'
+
+ params_types = _function_params_info(self.definition.args, self._docstring.params)
+
+ if params_types:
+ md += _markdown_table(['Parameter', 'Type', 'Kind', 'Default value', 'Description'], [
+ (
+ '`' + arg.name + '`',
+ ('`' + params_types.get(arg.name).annotation + '`') if params_types.get(arg.name).annotation is not None else None,
+ _argument_kind_human_readable(arg.kind),
+ ('`' + arg.default + '`') if arg.default is not None else None,
+ non_empty_str(params_types.get(arg.name).description)
+ )
+ for arg in self.definition.args
+ ], allow_markdown_table = self.allow_tables)
+
+ md += '\n\n'
+
+ elif not self.skip_empty_sections:
+ md += 'This function takes no parameters.'
+
+ return md.strip()
+
+ def _generate_returns_section(self) -> str:
+ '''
+ Generates the returns section.
+
+ :return: The returns section.
+ :rtype: str
+ '''
+
+ md = ''
+
+ if self._docstring.returns or self.definition.returns or not self.skip_empty_sections:
+ md += self.level * '#' + '# Returns\n\n'
+
+ returns = False
+ if self._docstring.returns:
+ if self._docstring.returns.description is not None:
+ returns = True
+ md += self._docstring.returns.description + '\n\n'
+
+ rtype = self.definition.returns
+ if rtype is None and self._docstring.returns is not None:
+ rtype = self._docstring.returns.type_name
+
+ if rtype is not None:
+ returns = True
+ md += 'Return type: `' + rtype + '`'
+
+ if not self.skip_empty_sections and not returns:
+ md += 'This function does not provide any information about its return value.'
+
+ return md.strip()
+
+ def _generate_exceptions_section(self) -> str:
+ '''
+ Generates the exceptions section.
+
+ :return: The exceptions section.
+ :rtype: str
+ '''
+
+ md = ''
+
+ if self._docstring.raises or not self.skip_empty_sections:
+ md += self.level * '#' + '# Exceptions\n\n'
+
+ if self._docstring.raises:
+ md += _markdown_table(('Exception', 'Description'), [
+ (('`' + exception.type_name + '`') if exception.type_name is not None else None, non_empty_str(exception.description))
+ for exception in self._docstring.raises
+ ], allow_markdown_table = self.allow_tables)
+ elif not self.skip_empty_sections:
+ md += 'This function raises no exceptions.'
+
+ return md.strip()
+
+ def _generate_examples_section(self) -> str:
+ '''
+ Generates the examples section.
+
+ :return: The examples section.
+ :rtype: str
+ '''
+
+ md = ''
+
+ if self._docstring.examples or not self.skip_empty_sections:
+ md += self.level * '#' + '# Examples\n\n'
+
+ examples = []
+ for example in self._docstring.examples:
+ examples.append('```python\n' +\
+ (example.snippet or example.description) + \
+ '\n```')
+ md += '\n\n---\n\n'.join(examples)
+
+ if not self._docstring.examples and not self.skip_empty_sections:
+ md += 'There are no examples.'
+
+ return md.strip()
+
+ def _generate_decorators_section(self) -> str:
+ '''
+ Generates the decorators section.
+
+ :return: The decorators section.
+ :rtype: str
+ '''
+
+ md = ''
+
+ if self.definition.decorators or not self.skip_empty_sections:
+ md += self.level * '#' + '# Decorators\n\n'
+
+ for decorator in self.definition.decorators:
+ md += '```python\n' + decorator + '\n```\n\n'
+
+ if not self.definition.decorators and not self.skip_empty_sections:
+ md += 'This function or class does not have any decorators.'
+
+ return md.strip()
+
+ def _generate_bases_section(self) -> str:
+ '''
+ Generates the bases section.
+
+ :return: The bases section.
+ :rtype: str
+ '''
+
+ md = ''
+
+ if self.definition.bases or not self.skip_empty_sections:
+ md += self.level * '#' + '# Base classes\n\n'
+
+ for base in self.definition.bases:
+ md += '- `' + base + '`\n'
+
+ if not self.definition.bases and not self.skip_empty_sections:
+ md += 'This class does not have any base classes.'
+
+ return md.strip()
+
+ def _generate_body_section(self) -> str:
+ '''
+ Generates the body section.
+
+ :return: The body section.
+ :rtype: str
+ '''
+
+ md = ''
+
+ if self.definition.body or not self.skip_empty_sections:
+ md += self.level * '#' + '# Body\n\n'
+
+ for method_or_class in self.definition.body:
+ md += PythonDefinitionDocumentationGenerator(
+ method_or_class,
+ self.include_sections,
+ level = self.level + 2,
+ allow_html = self.allow_html,
+ allow_tables = self.allow_tables,
+ skip_empty_sections = self.skip_empty_sections,
+ is_method = True
+ ).generate_documentation() + '\n\n'
+
+ if not self.definition.body and not self.skip_empty_sections:
+ md += 'This class does not have a body.'
+
+ return md.strip()
+
+ def generate_documentation(self) -> str:
+ '''
+ Generates the documentation in the markdown format.
+
+ :return: The documentation.
+ :rtype: str
+ '''
+
+ md = self.level * '#' + ' '
+
+ if isinstance(self.definition, PythonAsyncFunctionDefinition):
+ md += 'Asynchronous ' + ('function' if not self.is_method else 'method')
+ elif isinstance(self.definition, PythonFunctionDefinition):
+ if self.is_method and self.definition.name == '__init__':
+ md += 'Constructor (method'
+ else:
+ md += 'Function' if not self.is_method else 'Method'
+ else:
+ md += 'Class'
+
+ md += ' `' + self.definition.name + '`'
+ if isinstance(self.definition, PythonFunctionDefinition) and not isinstance(self.definition, PythonAsyncFunctionDefinition) and self.is_method and self.definition.name == '__init__':
+ md += ')'
+ md += '\n\n'
+
+ if self._docstring.description is not None:
+ md += self._docstring.description + '\n\n'
+
+ if isinstance(self.definition, PythonFunctionDefinition):
+ if self.include_sections.signature:
+ md += self._generate_signature_section() + '\n\n'
+
+ if self.include_sections.returns:
+ md += self._generate_returns_section() + '\n\n'
+
+ if self.include_sections.params:
+ md += self._generate_params_section() + '\n\n'
+
+ if self.include_sections.exceptions:
+ md += self._generate_exceptions_section() + '\n\n'
+
+ if isinstance(self.definition, PythonClassDefinition):
+ if self.include_sections.bases:
+ md += self._generate_bases_section() + '\n\n'
+
+ if self.include_sections.class_body:
+ md += self._generate_body_section() + '\n\n'
+
+ if self.include_sections.decorators:
+ md += self._generate_decorators_section() + '\n\n'
+
+ if self.include_sections.examples:
+ md += self._generate_examples_section() + '\n\n'
+
+ if self.include_sections.docstring:
+ md += self._generate_docstring_section() + '\n\n'
+
+ return md.strip()
+
class PythonModuleDocumentationGenerator:
'''
A documentation generator for a Python module.
diff --git a/src/jcloud_docsgen/core/python/docstrings.py b/src/jcloud_docsgen/core/python/docstrings.py
index ae742ff..0370a0f 100644
--- a/src/jcloud_docsgen/core/python/docstrings.py
+++ b/src/jcloud_docsgen/core/python/docstrings.py
@@ -16,15 +16,9 @@ from enum import Enum
import docstring_parser
__all__ = [
- 'PythonDocstringStyle',
'PythonDocstring'
]
-class PythonDocstringStyle(Enum):
- SPHINX = 'sphinx'
- NUMPY = 'numpy'
- GOOGLE = 'google'
-
class PythonDocstring:
'''
Represents a Python docstring.
diff --git a/tests/unit/core/python/_core/test_PythonDefinitionDocumentationGenerator.py b/tests/unit/core/python/_core/test_PythonDefinitionDocumentationGenerator.py
new file mode 100644
index 0000000..9c777b0
--- /dev/null
+++ b/tests/unit/core/python/_core/test_PythonDefinitionDocumentationGenerator.py
@@ -0,0 +1,1413 @@
+# 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 PythonDefinitionDocumentationGenerator, PythonDefinitionDocumentationIncludeSections
+from src.jcloud_docsgen.core.python.definitions import PythonFunctionDefinition, PythonAsyncFunctionDefinition, PythonClassDefinition
+from src.jcloud_docsgen.core.python.arguments import PythonFunctionArgument, PythonArgumentKind
+from src.jcloud_docsgen.core.python.docstrings import PythonDocstring
+import pytest
+
+def _signature_section(signature: str) -> str:
+ '''
+ The definition documentation signature section of the specified
+ signature.
+
+ :param signature: The signature.
+ :type signature: str
+
+ :return: The definition documentation signature section.
+ :rtype: str
+ '''
+
+ return f'''## Function signature
+
+```python
+{signature}
+```'''
+
+@pytest.mark.parametrize('definition_documentation_generator,expected', [
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, None, [])), 'a()'),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], 'str', None, [])), 'a() -> str'),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [
+ PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, None)
+ ], 'str', None, [])), 'a(a) -> str'),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [
+ PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, 'str')
+ ], 'str', None, [])), 'a(a: str) -> str'),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [
+ PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '\'42\'', None)
+ ], 'str', None, [])), 'a(a = \'42\') -> str'),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [
+ PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '\'42\'', 'str')
+ ], 'str', None, [])), 'a(a: str = \'42\') -> str'),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [
+ PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, '\'42\'', 'str'),
+ ], 'str', None, [])), 'a(a: str = \'42\', /) -> str'),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [
+ PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, None),
+ ], 'str', None, [])), 'a(a, /) -> str'),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [
+ PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, None),
+ ], None, None, [])), 'a(a, /)'),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('__init__', [
+ PythonFunctionArgument('self', PythonArgumentKind.NORMAL, None, None),
+ PythonFunctionArgument('definition', PythonArgumentKind.NORMAL, None, 'PythonDefinition'),
+ PythonFunctionArgument('allow_html', PythonArgumentKind.KEYWORD_ONLY, 'True', 'bool'),
+ PythonFunctionArgument('level', PythonArgumentKind.KEYWORD_ONLY, '1', 'int'),
+ ], 'None', None, [])), '__init__(self, definition: PythonDefinition, *, allow_html: bool = True, level: int = 1) -> None'),
+])
+def test_PythonDefinitionDocumentationGenerator_generate_signature(definition_documentation_generator, expected):
+ assert definition_documentation_generator.generate_signature() == expected
+
+@pytest.mark.parametrize('definition_documentation_generator,expected_exception,expected_exception_msg', [
+ (PythonDefinitionDocumentationGenerator(PythonClassDefinition('A', [], None, [], [])), TypeError, 'Cannot generate a signature of a class definition.')
+])
+def test_PythonDefinitionDocumentationGenerator_generate_signature_exceptions(definition_documentation_generator, expected_exception, expected_exception_msg):
+ with pytest.raises(expected_exception) as exc_info:
+ definition_documentation_generator.generate_signature()
+ assert str(exc_info.value) == expected_exception_msg
+
+@pytest.mark.parametrize('definition_documentation_generator,expected', [
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, None, [])), 'a()'),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], 'str', None, [])), 'a() -> str'),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [
+ PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, None)
+ ], 'str', None, [])), 'a(a) -> str'),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [
+ PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, 'str')
+ ], 'str', None, [])), 'a(a: str) -> str'),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [
+ PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '\'42\'', None)
+ ], 'str', None, [])), 'a(a = \'42\') -> str'),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [
+ PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '\'42\'', 'str')
+ ], 'str', None, [])), 'a(a: str = \'42\') -> str'),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [
+ PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, '\'42\'', 'str'),
+ ], 'str', None, [])), 'a(a: str = \'42\', /) -> str'),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [
+ PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, None),
+ ], 'str', None, [])), 'a(a, /) -> str'),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [
+ PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, None),
+ ], None, None, [])), 'a(a, /)'),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('__init__', [
+ PythonFunctionArgument('self', PythonArgumentKind.NORMAL, None, None),
+ PythonFunctionArgument('definition', PythonArgumentKind.NORMAL, None, 'PythonDefinition'),
+ PythonFunctionArgument('allow_html', PythonArgumentKind.KEYWORD_ONLY, 'True', 'bool'),
+ PythonFunctionArgument('level', PythonArgumentKind.KEYWORD_ONLY, '1', 'int'),
+ ], 'None', None, [])), '__init__(self, definition: PythonDefinition, *, allow_html: bool = True, level: int = 1) -> None'),
+])
+def test_PythonDefinitionDocumentationGenerator__generate_signature_section(definition_documentation_generator, expected):
+ assert definition_documentation_generator._generate_signature_section() == _signature_section(expected)
+
+@pytest.mark.parametrize('definition_documentation_generator,expected', [
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, None, []), allow_html = False, skip_empty_sections = True), ''),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, PythonDocstring(''), []), allow_html = False, skip_empty_sections = True), ''),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, PythonDocstring('a'), []), allow_html = False, skip_empty_sections = True), '## Docstring\n\na'),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, PythonDocstring('a\nb'), []), allow_html = False, skip_empty_sections = True), '## Docstring\n\na\nb'),
+
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, None, []), allow_html = True, skip_empty_sections = True), ''),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, PythonDocstring(''), []), allow_html = True, skip_empty_sections = True), ''),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, PythonDocstring('a'), []), allow_html = True, skip_empty_sections = True), 'Docstring
a '),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, PythonDocstring('a\nb'), []), allow_html = True, skip_empty_sections = True), 'Docstring
a\nb '),
+
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, None, []), allow_html = False, skip_empty_sections = False), '## Docstring\n\nThere is no docstring to show.'),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, PythonDocstring(''), []), allow_html = False, skip_empty_sections = False), '## Docstring\n\nThere is no docstring to show.'),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, PythonDocstring('a'), []), allow_html = False, skip_empty_sections = False), '## Docstring\n\na'),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, PythonDocstring('a\nb'), []), allow_html = False, skip_empty_sections = False), '## Docstring\n\na\nb'),
+
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, None, []), allow_html = True, skip_empty_sections = False), 'Docstring
There is no docstring to show. '),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, PythonDocstring(''), []), allow_html = True, skip_empty_sections = False), 'Docstring
There is no docstring to show. '),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, PythonDocstring('a'), []), allow_html = True, skip_empty_sections = False), 'Docstring
a '),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, PythonDocstring('a\nb'), []), allow_html = True, skip_empty_sections = False), 'Docstring
a\nb '),
+
+])
+def test_PythonDefinitionDocumentationGenerator__generate_docstring_section(definition_documentation_generator, expected):
+ assert definition_documentation_generator._generate_docstring_section() == expected
+
+@pytest.mark.parametrize('definition_documentation_generator,expected', [
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, None, [])), ''),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, None, []), skip_empty_sections = False), '## Parameters\n\nThis function takes no parameters.'),
+
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [
+ PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, None)
+ ], None, None, [])), '''## Parameters
+
+- Parameter: `a`
+
+ Type: --
+
+ Kind: normal
+
+ Default value: --
+
+ Description: --'''),
+
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [
+ PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '1', None)
+ ], None, None, [])), '''## Parameters
+
+- Parameter: `a`
+
+ Type: --
+
+ Kind: normal
+
+ Default value: `1`
+
+ Description: --'''),
+
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [
+ PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, 'int')
+ ], None, None, [])), '''## Parameters
+
+- Parameter: `a`
+
+ Type: `int`
+
+ Kind: normal
+
+ Default value: --
+
+ Description: --'''),
+
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [
+ PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '1', 'int')
+ ], None, None, [])), '''## Parameters
+
+- Parameter: `a`
+
+ Type: `int`
+
+ Kind: normal
+
+ Default value: `1`
+
+ Description: --'''),
+
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [
+ PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, None),
+ PythonFunctionArgument('b', PythonArgumentKind.POSITIONAL_ONLY, None, None),
+ ], None, None, [])), '''## Parameters
+
+- Parameter: `a`
+
+ Type: --
+
+ Kind: normal
+
+ Default value: --
+
+ Description: --
+
+- Parameter: `b`
+
+ Type: --
+
+ Kind: positional-only
+
+ Default value: --
+
+ Description: --'''),
+
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [
+ PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, None),
+ PythonFunctionArgument('b', PythonArgumentKind.POSITIONAL_ONLY, None, None),
+ ], None, PythonDocstring(':param b: a parameter\n:type b: int'), [])), '''## Parameters
+
+- Parameter: `a`
+
+ Type: --
+
+ Kind: normal
+
+ Default value: --
+
+ Description: --
+
+- Parameter: `b`
+
+ Type: `int`
+
+ Kind: positional-only
+
+ Default value: --
+
+ Description: a parameter'''),
+
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [
+ PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, None),
+ PythonFunctionArgument('b', PythonArgumentKind.POSITIONAL_ONLY, None, None),
+ ], None, PythonDocstring(':param b:\n:type b: int'), [])), '''## Parameters
+
+- Parameter: `a`
+
+ Type: --
+
+ Kind: normal
+
+ Default value: --
+
+ Description: --
+
+- Parameter: `b`
+
+ Type: `int`
+
+ Kind: positional-only
+
+ Default value: --
+
+ Description: --'''),
+
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [
+ PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, None),
+ PythonFunctionArgument('b', PythonArgumentKind.POSITIONAL_ONLY, None, None),
+ ], None, PythonDocstring(':param b: \n:type b: int'), [])), '''## Parameters
+
+- Parameter: `a`
+
+ Type: --
+
+ Kind: normal
+
+ Default value: --
+
+ Description: --
+
+- Parameter: `b`
+
+ Type: `int`
+
+ Kind: positional-only
+
+ Default value: --
+
+ Description: --'''),
+
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [
+ PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, None),
+ PythonFunctionArgument('b', PythonArgumentKind.POSITIONAL_ONLY, None, None),
+ ], None, PythonDocstring(':param b: a parameter\n:type b: int'), []), allow_tables = True), '''## Parameters
+
+| Parameter | Type | Kind | Default value | Description |
+| --- | --- | --- | --- | --- |
+| `a` | -- | normal | -- | -- |
+| `b` | `int` | positional-only | -- | a parameter |'''),
+])
+def test_PythonDefinitionDocumentationGenerator__generate_params_section(definition_documentation_generator, expected):
+ assert definition_documentation_generator._generate_params_section() == expected
+
+@pytest.mark.parametrize('definition_documentation_generator,expected', [
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, None, [])), ''),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, None, []), skip_empty_sections = False), '## Exceptions\n\nThis function raises no exceptions.'),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, PythonDocstring(':raises E: a'), [])), '''## Exceptions
+
+- Exception: `E`
+
+ Description: a'''),
+
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, PythonDocstring(':raises E:\n:raises E2:'), [])), '''## Exceptions
+
+- Exception: `E`
+
+ Description: --
+
+- Exception: `E2`
+
+ Description: --'''),
+
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, PythonDocstring(':raises E:\n:raises E2: a'), [])), '''## Exceptions
+
+- Exception: `E`
+
+ Description: --
+
+- Exception: `E2`
+
+ Description: a'''),
+])
+def test_PythonDefinitionDocumentationGenerator__generate_exceptions_section(definition_documentation_generator, expected):
+ assert definition_documentation_generator._generate_exceptions_section() == expected
+
+@pytest.mark.parametrize('definition_documentation_generator,expected', [
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, None, [])), ''),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, PythonDocstring(''), [])), ''),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, PythonDocstring('Examples\n--------'), [])), ''),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, PythonDocstring('Examples\n--------\n'), [])), ''),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, PythonDocstring('Examples\n--------\n\n'), [])), ''),
+
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, None, []), skip_empty_sections = False), '## Examples\n\nThere are no examples.'),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, PythonDocstring(''), []), skip_empty_sections = False), '## Examples\n\nThere are no examples.'),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, PythonDocstring('Examples\n--------'), []), skip_empty_sections = False), '## Examples\n\nThere are no examples.'),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, PythonDocstring('Examples\n--------\n'), []), skip_empty_sections = False), '## Examples\n\nThere are no examples.'),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, PythonDocstring('Examples\n--------\n\n'), []), skip_empty_sections = False), '## Examples\n\nThere are no examples.'),
+
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, PythonDocstring('''Examples
+--------
+>>> a()
+None'''), [])), '''## Examples
+
+```python
+>>> a()
+```'''),
+
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, PythonDocstring('''Examples
+--------
+>>> a()
+None
+
+>>> a()
+None'''), [])), '''## Examples
+
+```python
+>>> a()
+```
+
+---
+
+```python
+>>> a()
+```'''),
+
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, PythonDocstring('''Examples
+--------
+>>> a()
+None
+
+>>> b()
+1'''), [])), '''## Examples
+
+```python
+>>> a()
+```
+
+---
+
+```python
+>>> b()
+```'''),
+
+])
+def test_PythonDefinitionDocumentationGenerator__generate_examples_section(definition_documentation_generator, expected):
+ assert definition_documentation_generator._generate_examples_section() == expected
+
+@pytest.mark.parametrize('definition_documentation_generator,expected', [
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], 'int', None, [])), '''## Returns
+
+Return type: `int`'''),
+
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], 'int', PythonDocstring(':return: value'), [])), '''## Returns
+
+value
+
+Return type: `int`'''),
+
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], 'int', PythonDocstring(':return: value\n:rtype: str'), [])), '''## Returns
+
+value
+
+Return type: `int`'''),
+
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, PythonDocstring(':return: value\n:rtype: str'), [])), '''## Returns
+
+value
+
+Return type: `str`'''),
+
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, PythonDocstring(':return: value'), [])), '''## Returns
+
+value'''),
+
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, PythonDocstring('a'), []), skip_empty_sections = False), '## Returns\n\nThis function does not provide any information about its return value.'),
+])
+def test_PythonDefinitionDocumentationGenerator__generate_returns_section(definition_documentation_generator, expected):
+ assert definition_documentation_generator._generate_returns_section() == expected
+
+@pytest.mark.parametrize('definition_documentation_generator,expected', [
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, None, ['a', 'b'])), '''## Decorators
+
+```python
+a
+```
+
+```python
+b
+```'''),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, None, ['a'])), '''## Decorators
+
+```python
+a
+```'''),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, None, [])), ''),
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [], None, None, []), skip_empty_sections = False), '''## Decorators
+
+This function or class does not have any decorators.'''),
+])
+def test_PythonDefinitionDocumentationGenerator__generate_decorators_section(definition_documentation_generator, expected):
+ assert definition_documentation_generator._generate_decorators_section() == expected
+
+@pytest.mark.parametrize('definition_documentation_generator,expected', [
+ (PythonDefinitionDocumentationGenerator(PythonClassDefinition('A', [], None, [], [])), ''),
+ (PythonDefinitionDocumentationGenerator(PythonClassDefinition('A', [], None, [], []), skip_empty_sections = False), '''## Base classes
+
+This class does not have any base classes.'''),
+ (PythonDefinitionDocumentationGenerator(PythonClassDefinition('A', ['a'], None, [], []), skip_empty_sections = False), '''## Base classes
+
+- `a`'''),
+ (PythonDefinitionDocumentationGenerator(PythonClassDefinition('A', ['b'], None, [], []), skip_empty_sections = False), '''## Base classes
+
+- `b`'''),
+ (PythonDefinitionDocumentationGenerator(PythonClassDefinition('A', ['a', 'b'], None, [], []), skip_empty_sections = False), '''## Base classes
+
+- `a`
+- `b`'''),
+ (PythonDefinitionDocumentationGenerator(PythonClassDefinition('A', ['b', 'a'], None, [], []), skip_empty_sections = False), '''## Base classes
+
+- `b`
+- `a`'''),
+])
+def test_PythonDefinitionDocumentationGenerator__generate_bases_section(definition_documentation_generator, expected):
+ assert definition_documentation_generator._generate_bases_section() == expected
+
+@pytest.mark.parametrize('definition_documentation_generator,expected', [
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [
+ PythonFunctionArgument('hello', PythonArgumentKind.POSITIONAL_ONLY, None, 'int'),
+ PythonFunctionArgument('world', PythonArgumentKind.NORMAL, '1', None),
+ PythonFunctionArgument('something', PythonArgumentKind.KEYWORD_ONLY, '\'something\'', 'str'),
+ PythonFunctionArgument('value', PythonArgumentKind.KWARGS, None, None),
+ ], 'bool', PythonDocstring('''A function.
+
+Very long ...
+
+:param hello: A parameter.
+:type hello: str
+:param something: Something.
+:param value: The value.
+:type value: object
+
+:raises ValueError: If the value is invalid.
+
+:return: The return value.
+:rtype: list'''), ['decorate(1, 2, 3)']), allow_html = False), '''# Function `a`
+
+A function.
+
+Very long ...
+
+## Function signature
+
+```python
+a(hello: int, /, world = 1, *, something: str = 'something', **value) -> bool
+```
+
+## Returns
+
+The return value.
+
+Return type: `bool`
+
+## Parameters
+
+- Parameter: `hello`
+
+ Type: `int`
+
+ Kind: positional-only
+
+ Default value: --
+
+ Description: A parameter.
+
+- Parameter: `world`
+
+ Type: --
+
+ Kind: normal
+
+ Default value: `1`
+
+ Description: --
+
+- Parameter: `something`
+
+ Type: `str`
+
+ Kind: keyword-only
+
+ Default value: `'something'`
+
+ Description: Something.
+
+- Parameter: `value`
+
+ Type: `object`
+
+ Kind: kwargs
+
+ Default value: --
+
+ Description: The value.
+
+## Exceptions
+
+- Exception: `ValueError`
+
+ Description: If the value is invalid.
+
+## Decorators
+
+```python
+decorate(1, 2, 3)
+```
+
+
+
+## Docstring
+
+A function.
+
+Very long ...
+
+:param hello: A parameter.
+:type hello: str
+:param something: Something.
+:param value: The value.
+:type value: object
+
+:raises ValueError: If the value is invalid.
+
+:return: The return value.
+:rtype: list'''),
+
+
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [
+ PythonFunctionArgument('hello', PythonArgumentKind.POSITIONAL_ONLY, None, 'int'),
+ PythonFunctionArgument('world', PythonArgumentKind.NORMAL, '1', None),
+ PythonFunctionArgument('something', PythonArgumentKind.KEYWORD_ONLY, '\'something\'', 'str'),
+ PythonFunctionArgument('value', PythonArgumentKind.KWARGS, None, None),
+ ], 'bool', PythonDocstring('''A function.
+
+Very long ...
+
+:param hello: A parameter.
+:type hello: str
+:param something: Something.
+:param value: The value.
+:type value: object
+
+:raises ValueError: If the value is invalid.
+
+:return: The return value.
+:rtype: list'''), ['decorate(1, 2, 3)'])), '''# Function `a`
+
+A function.
+
+Very long ...
+
+## Function signature
+
+```python
+a(hello: int, /, world = 1, *, something: str = 'something', **value) -> bool
+```
+
+## Returns
+
+The return value.
+
+Return type: `bool`
+
+## Parameters
+
+- Parameter: `hello`
+
+ Type: `int`
+
+ Kind: positional-only
+
+ Default value: --
+
+ Description: A parameter.
+
+- Parameter: `world`
+
+ Type: --
+
+ Kind: normal
+
+ Default value: `1`
+
+ Description: --
+
+- Parameter: `something`
+
+ Type: `str`
+
+ Kind: keyword-only
+
+ Default value: `'something'`
+
+ Description: Something.
+
+- Parameter: `value`
+
+ Type: `object`
+
+ Kind: kwargs
+
+ Default value: --
+
+ Description: The value.
+
+## Exceptions
+
+- Exception: `ValueError`
+
+ Description: If the value is invalid.
+
+## Decorators
+
+```python
+decorate(1, 2, 3)
+```
+
+
+
+Docstring
A function.
+
+Very long ...
+
+:param hello: A parameter.
+:type hello: str
+:param something: Something.
+:param value: The value.
+:type value: object
+
+:raises ValueError: If the value is invalid.
+
+:return: The return value.
+:rtype: list '''),
+
+
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [
+ PythonFunctionArgument('hello', PythonArgumentKind.POSITIONAL_ONLY, None, 'int'),
+ PythonFunctionArgument('world', PythonArgumentKind.NORMAL, '1', None),
+ PythonFunctionArgument('something', PythonArgumentKind.KEYWORD_ONLY, '\'something\'', 'str'),
+ PythonFunctionArgument('value', PythonArgumentKind.KWARGS, None, None),
+ ], 'bool', PythonDocstring('''A function.
+
+Very long ...
+
+:param hello: A parameter.
+:type hello: str
+:param something: Something.
+:param value: The value.
+:type value: object
+
+:raises ValueError: If the value is invalid.
+
+:return: The return value.
+:rtype: list'''), ['decorate(1, 2, 3)']), allow_html = False, allow_tables = True), '''# Function `a`
+
+A function.
+
+Very long ...
+
+## Function signature
+
+```python
+a(hello: int, /, world = 1, *, something: str = 'something', **value) -> bool
+```
+
+## Returns
+
+The return value.
+
+Return type: `bool`
+
+## Parameters
+
+| Parameter | Type | Kind | Default value | Description |
+| --- | --- | --- | --- | --- |
+| `hello` | `int` | positional-only | -- | A parameter. |
+| `world` | -- | normal | `1` | -- |
+| `something` | `str` | keyword-only | `'something'` | Something. |
+| `value` | `object` | kwargs | -- | The value. |
+
+## Exceptions
+
+| Exception | Description |
+| --- | --- |
+| `ValueError` | If the value is invalid. |
+
+## Decorators
+
+```python
+decorate(1, 2, 3)
+```
+
+
+
+## Docstring
+
+A function.
+
+Very long ...
+
+:param hello: A parameter.
+:type hello: str
+:param something: Something.
+:param value: The value.
+:type value: object
+
+:raises ValueError: If the value is invalid.
+
+:return: The return value.
+:rtype: list'''),
+
+
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [
+ PythonFunctionArgument('hello', PythonArgumentKind.POSITIONAL_ONLY, None, 'int'),
+ PythonFunctionArgument('world', PythonArgumentKind.NORMAL, '1', None),
+ PythonFunctionArgument('something', PythonArgumentKind.KEYWORD_ONLY, '\'something\'', 'str'),
+ PythonFunctionArgument('value', PythonArgumentKind.KWARGS, None, None),
+ ], 'bool', PythonDocstring('''A function.
+
+Very long ...
+
+:param hello: A parameter.
+:type hello: str
+:param something: Something.
+:param value: The value.
+:type value: object
+
+:raises ValueError: If the value is invalid.
+
+:return: The return value.
+:rtype: list'''), ['decorate(1, 2, 3)']), allow_tables = True), '''# Function `a`
+
+A function.
+
+Very long ...
+
+## Function signature
+
+```python
+a(hello: int, /, world = 1, *, something: str = 'something', **value) -> bool
+```
+
+## Returns
+
+The return value.
+
+Return type: `bool`
+
+## Parameters
+
+| Parameter | Type | Kind | Default value | Description |
+| --- | --- | --- | --- | --- |
+| `hello` | `int` | positional-only | -- | A parameter. |
+| `world` | -- | normal | `1` | -- |
+| `something` | `str` | keyword-only | `'something'` | Something. |
+| `value` | `object` | kwargs | -- | The value. |
+
+## Exceptions
+
+| Exception | Description |
+| --- | --- |
+| `ValueError` | If the value is invalid. |
+
+## Decorators
+
+```python
+decorate(1, 2, 3)
+```
+
+
+
+Docstring
A function.
+
+Very long ...
+
+:param hello: A parameter.
+:type hello: str
+:param something: Something.
+:param value: The value.
+:type value: object
+
+:raises ValueError: If the value is invalid.
+
+:return: The return value.
+:rtype: list '''),
+
+
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [
+ PythonFunctionArgument('hello', PythonArgumentKind.POSITIONAL_ONLY, None, 'int'),
+ PythonFunctionArgument('world', PythonArgumentKind.NORMAL, '1', None),
+ PythonFunctionArgument('something', PythonArgumentKind.KEYWORD_ONLY, '\'something\'', 'str'),
+ PythonFunctionArgument('value', PythonArgumentKind.KWARGS, None, None),
+ ], 'bool', PythonDocstring('''A function.
+
+Very long ...
+
+:param hello: A parameter.
+:type hello: str
+:param something: Something.
+:param value: The value.
+:type value: object
+
+:raises ValueError: If the value is invalid.
+
+:return: The return value.
+:rtype: list'''), ['decorate(1, 2, 3)']), allow_html = False, skip_empty_sections = False), '''# Function `a`
+
+A function.
+
+Very long ...
+
+## Function signature
+
+```python
+a(hello: int, /, world = 1, *, something: str = 'something', **value) -> bool
+```
+
+## Returns
+
+The return value.
+
+Return type: `bool`
+
+## Parameters
+
+- Parameter: `hello`
+
+ Type: `int`
+
+ Kind: positional-only
+
+ Default value: --
+
+ Description: A parameter.
+
+- Parameter: `world`
+
+ Type: --
+
+ Kind: normal
+
+ Default value: `1`
+
+ Description: --
+
+- Parameter: `something`
+
+ Type: `str`
+
+ Kind: keyword-only
+
+ Default value: `'something'`
+
+ Description: Something.
+
+- Parameter: `value`
+
+ Type: `object`
+
+ Kind: kwargs
+
+ Default value: --
+
+ Description: The value.
+
+## Exceptions
+
+- Exception: `ValueError`
+
+ Description: If the value is invalid.
+
+## Decorators
+
+```python
+decorate(1, 2, 3)
+```
+
+## Examples
+
+There are no examples.
+
+## Docstring
+
+A function.
+
+Very long ...
+
+:param hello: A parameter.
+:type hello: str
+:param something: Something.
+:param value: The value.
+:type value: object
+
+:raises ValueError: If the value is invalid.
+
+:return: The return value.
+:rtype: list'''),
+
+
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [
+ PythonFunctionArgument('hello', PythonArgumentKind.POSITIONAL_ONLY, None, 'int'),
+ PythonFunctionArgument('world', PythonArgumentKind.NORMAL, '1', None),
+ PythonFunctionArgument('something', PythonArgumentKind.KEYWORD_ONLY, '\'something\'', 'str'),
+ PythonFunctionArgument('value', PythonArgumentKind.KWARGS, None, None),
+ ], 'bool', PythonDocstring('''A function.
+
+Very long ...
+
+:param hello: A parameter.
+:type hello: str
+:param something: Something.
+:param value: The value.
+:type value: object
+
+:raises ValueError: If the value is invalid.
+
+:return: The return value.
+:rtype: list'''), ['decorate(1, 2, 3)']), skip_empty_sections = False), '''# Function `a`
+
+A function.
+
+Very long ...
+
+## Function signature
+
+```python
+a(hello: int, /, world = 1, *, something: str = 'something', **value) -> bool
+```
+
+## Returns
+
+The return value.
+
+Return type: `bool`
+
+## Parameters
+
+- Parameter: `hello`
+
+ Type: `int`
+
+ Kind: positional-only
+
+ Default value: --
+
+ Description: A parameter.
+
+- Parameter: `world`
+
+ Type: --
+
+ Kind: normal
+
+ Default value: `1`
+
+ Description: --
+
+- Parameter: `something`
+
+ Type: `str`
+
+ Kind: keyword-only
+
+ Default value: `'something'`
+
+ Description: Something.
+
+- Parameter: `value`
+
+ Type: `object`
+
+ Kind: kwargs
+
+ Default value: --
+
+ Description: The value.
+
+## Exceptions
+
+- Exception: `ValueError`
+
+ Description: If the value is invalid.
+
+## Decorators
+
+```python
+decorate(1, 2, 3)
+```
+
+## Examples
+
+There are no examples.
+
+Docstring
A function.
+
+Very long ...
+
+:param hello: A parameter.
+:type hello: str
+:param something: Something.
+:param value: The value.
+:type value: object
+
+:raises ValueError: If the value is invalid.
+
+:return: The return value.
+:rtype: list '''),
+
+
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [
+ PythonFunctionArgument('hello', PythonArgumentKind.POSITIONAL_ONLY, None, 'int'),
+ PythonFunctionArgument('world', PythonArgumentKind.NORMAL, '1', None),
+ PythonFunctionArgument('something', PythonArgumentKind.KEYWORD_ONLY, '\'something\'', 'str'),
+ PythonFunctionArgument('value', PythonArgumentKind.KWARGS, None, None),
+ ], 'bool', PythonDocstring('''A function.
+
+Very long ...
+
+:param hello: A parameter.
+:type hello: str
+:param something: Something.
+:param value: The value.
+:type value: object
+
+:raises ValueError: If the value is invalid.
+
+:return: The return value.
+:rtype: list'''), ['decorate(1, 2, 3)']), allow_html = False, allow_tables = True, skip_empty_sections = False), '''# Function `a`
+
+A function.
+
+Very long ...
+
+## Function signature
+
+```python
+a(hello: int, /, world = 1, *, something: str = 'something', **value) -> bool
+```
+
+## Returns
+
+The return value.
+
+Return type: `bool`
+
+## Parameters
+
+| Parameter | Type | Kind | Default value | Description |
+| --- | --- | --- | --- | --- |
+| `hello` | `int` | positional-only | -- | A parameter. |
+| `world` | -- | normal | `1` | -- |
+| `something` | `str` | keyword-only | `'something'` | Something. |
+| `value` | `object` | kwargs | -- | The value. |
+
+## Exceptions
+
+| Exception | Description |
+| --- | --- |
+| `ValueError` | If the value is invalid. |
+
+## Decorators
+
+```python
+decorate(1, 2, 3)
+```
+
+## Examples
+
+There are no examples.
+
+## Docstring
+
+A function.
+
+Very long ...
+
+:param hello: A parameter.
+:type hello: str
+:param something: Something.
+:param value: The value.
+:type value: object
+
+:raises ValueError: If the value is invalid.
+
+:return: The return value.
+:rtype: list'''),
+
+
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [
+ PythonFunctionArgument('hello', PythonArgumentKind.POSITIONAL_ONLY, None, 'int'),
+ PythonFunctionArgument('world', PythonArgumentKind.NORMAL, '1', None),
+ PythonFunctionArgument('something', PythonArgumentKind.KEYWORD_ONLY, '\'something\'', 'str'),
+ PythonFunctionArgument('value', PythonArgumentKind.KWARGS, None, None),
+ ], 'bool', PythonDocstring('''A function.
+
+Very long ...
+
+:param hello: A parameter.
+:type hello: str
+:param something: Something.
+:param value: The value.
+:type value: object
+
+:raises ValueError: If the value is invalid.
+
+:return: The return value.
+:rtype: list'''), ['decorate(1, 2, 3)']), allow_tables = True, skip_empty_sections = False), '''# Function `a`
+
+A function.
+
+Very long ...
+
+## Function signature
+
+```python
+a(hello: int, /, world = 1, *, something: str = 'something', **value) -> bool
+```
+
+## Returns
+
+The return value.
+
+Return type: `bool`
+
+## Parameters
+
+| Parameter | Type | Kind | Default value | Description |
+| --- | --- | --- | --- | --- |
+| `hello` | `int` | positional-only | -- | A parameter. |
+| `world` | -- | normal | `1` | -- |
+| `something` | `str` | keyword-only | `'something'` | Something. |
+| `value` | `object` | kwargs | -- | The value. |
+
+## Exceptions
+
+| Exception | Description |
+| --- | --- |
+| `ValueError` | If the value is invalid. |
+
+## Decorators
+
+```python
+decorate(1, 2, 3)
+```
+
+## Examples
+
+There are no examples.
+
+Docstring
A function.
+
+Very long ...
+
+:param hello: A parameter.
+:type hello: str
+:param something: Something.
+:param value: The value.
+:type value: object
+
+:raises ValueError: If the value is invalid.
+
+:return: The return value.
+:rtype: list '''),
+
+
+ (PythonDefinitionDocumentationGenerator(PythonFunctionDefinition('a', [
+ PythonFunctionArgument('hello', PythonArgumentKind.POSITIONAL_ONLY, None, 'int'),
+ PythonFunctionArgument('world', PythonArgumentKind.NORMAL, '1', None),
+ PythonFunctionArgument('something', PythonArgumentKind.KEYWORD_ONLY, '\'something\'', 'str'),
+ PythonFunctionArgument('value', PythonArgumentKind.KWARGS, None, None),
+ ], 'bool', PythonDocstring('''A function.
+
+Very long ...
+
+:param hello: A parameter.
+:type hello: str
+:param something: Something.
+:param value: The value.
+:type value: object
+
+:raises ValueError: If the value is invalid.
+
+:return: The return value.
+:rtype: list'''), ['decorate(1, 2, 3)']), PythonDefinitionDocumentationIncludeSections(
+ signature = False,
+ returns = False,
+ params = False,
+ exceptions = False,
+ examples = False,
+ docstring = False,
+ bases = False,
+ decorators = False
+), allow_tables = True, skip_empty_sections = False), '''# Function `a`
+
+A function.
+
+Very long ...'''),
+
+
+ (PythonDefinitionDocumentationGenerator(PythonClassDefinition('A', ['Base1', 'Base2'], PythonDocstring('''A class.
+
+Examples
+--------
+>>> A()
+
+>>> A()'''), ['dec', 'ora', 'tor'], [
+ PythonFunctionDefinition('__init__', [
+ PythonFunctionArgument('self', PythonArgumentKind.NORMAL, None, None),
+ ], None, None, []),
+ PythonFunctionDefinition('method', [
+ PythonFunctionArgument('self', PythonArgumentKind.NORMAL, None, None),
+ PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '1', 'int')
+ ], 'str', None, ['decorator']),
+ PythonAsyncFunctionDefinition('method2', [
+ PythonFunctionArgument('self', PythonArgumentKind.NORMAL, None, None),
+ ], None, None, []),
+ PythonClassDefinition('B', ['Base'], None, [], [
+ PythonFunctionDefinition('__init__', [
+ PythonFunctionArgument('self', PythonArgumentKind.NORMAL, None, None)
+ ], None, None, [])
+ ])
+ ]), allow_html = False, allow_tables = True, include_sections = PythonDefinitionDocumentationIncludeSections(bases = False)), '''# Class `A`
+
+A class.
+
+
+## Body
+
+### Constructor (method `__init__`)
+
+#### Method signature
+
+```python
+__init__(self)
+```
+
+
+
+#### Parameters
+
+| Parameter | Type | Kind | Default value | Description |
+| --- | --- | --- | --- | --- |
+| `self` | -- | normal | -- | -- |
+
+### Method `method`
+
+#### Method signature
+
+```python
+method(self, a: int = 1) -> str
+```
+
+#### Returns
+
+Return type: `str`
+
+#### Parameters
+
+| Parameter | Type | Kind | Default value | Description |
+| --- | --- | --- | --- | --- |
+| `self` | -- | normal | -- | -- |
+| `a` | `int` | normal | `1` | -- |
+
+
+
+#### Decorators
+
+```python
+decorator
+```
+
+### Asynchronous method `method2`
+
+#### Method signature
+
+```python
+method2(self)
+```
+
+
+
+#### Parameters
+
+| Parameter | Type | Kind | Default value | Description |
+| --- | --- | --- | --- | --- |
+| `self` | -- | normal | -- | -- |
+
+### Class `B`
+
+#### Body
+
+##### Constructor (method `__init__`)
+
+###### Method signature
+
+```python
+__init__(self)
+```
+
+
+
+###### Parameters
+
+| Parameter | Type | Kind | Default value | Description |
+| --- | --- | --- | --- | --- |
+| `self` | -- | normal | -- | -- |
+
+## Decorators
+
+```python
+dec
+```
+
+```python
+ora
+```
+
+```python
+tor
+```
+
+## Examples
+
+```python
+>>> A()
+```
+
+---
+
+```python
+>>> A()
+```
+
+## Docstring
+
+A class.
+
+Examples
+--------
+>>> A()
+
+>>> A()''')
+])
+def test_PythonDefinitionDocumentationGenerator_generate_documentation(definition_documentation_generator, expected):
+ assert definition_documentation_generator.generate_documentation() == expected
\ No newline at end of file
diff --git a/tests/unit/core/python/_core/test__argument_kind_human_readable.py b/tests/unit/core/python/_core/test__argument_kind_human_readable.py
new file mode 100644
index 0000000..cfbde3e
--- /dev/null
+++ b/tests/unit/core/python/_core/test__argument_kind_human_readable.py
@@ -0,0 +1,27 @@
+# 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 _argument_kind_human_readable
+from src.jcloud_docsgen.core.python.arguments import PythonArgumentKind
+import pytest
+
+@pytest.mark.parametrize('argument_kind,expected', [
+ (PythonArgumentKind.NORMAL, 'normal'),
+ (PythonArgumentKind.POSITIONAL_ONLY, 'positional-only'),
+ (PythonArgumentKind.KEYWORD_ONLY, 'keyword-only'),
+ (PythonArgumentKind.VARARG, 'vararg'),
+ (PythonArgumentKind.KWARGS, 'kwargs'),
+])
+def test__argument_kind_human_readable(argument_kind, expected):
+ assert _argument_kind_human_readable(argument_kind) == expected
\ No newline at end of file
diff --git a/tests/unit/core/python/_core/test__fit_row_length.py b/tests/unit/core/python/_core/test__fit_row_length.py
new file mode 100644
index 0000000..f7cffbc
--- /dev/null
+++ b/tests/unit/core/python/_core/test__fit_row_length.py
@@ -0,0 +1,56 @@
+# 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 _fit_row_length
+import pytest
+
+@pytest.mark.parametrize('expected_length,row,expected', [
+ (0, (), ()),
+ (1, (), (None,)),
+ (2, (), (None, None)),
+ (2, (1,), (1, None)),
+
+ (0, (1,), ()),
+ (1, (1, 2), (1,)),
+ (1, (1, 2), (1,)),
+ (0, (1, 2), ()),
+ (0, (1, 2, 3), ()),
+ (1, (1, 2, 3), (1,)),
+ (2, (1, 2, 3), (1, 2)),
+
+ (1, (1,), (1,)),
+ (2, (1, 1), (1, 1)),
+ (2, (1, 2), (1, 2)),
+ (3, (1, 2, 3), (1, 2, 3)),
+])
+def test__fit_row_length(expected_length, row, expected):
+ assert _fit_row_length(expected_length, row) == expected
+
+@pytest.mark.parametrize('row', [
+ (),
+ (1,),
+ (1, 2),
+ (1, 2, 3),
+ (1, 2, 3, 456),
+])
+@pytest.mark.parametrize('expected_length,expected_exception', [
+ (-1, ValueError),
+ (-2, ValueError),
+ (-3, ValueError),
+ (-42, ValueError),
+])
+def test__fit_row_length_exceptions(expected_length, row, expected_exception):
+ with pytest.raises(expected_exception) as exc_info:
+ _fit_row_length(expected_length, row)
+ assert str(exc_info.value) == 'expected must be larger than or equal 0.'
\ No newline at end of file
diff --git a/tests/unit/core/python/_core/test__function_params_info.py b/tests/unit/core/python/_core/test__function_params_info.py
new file mode 100644
index 0000000..4d2f8f6
--- /dev/null
+++ b/tests/unit/core/python/_core/test__function_params_info.py
@@ -0,0 +1,104 @@
+# 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 _function_params_info, _FunctionParamInfo
+from src.jcloud_docsgen.core.python.arguments import PythonFunctionArgument, PythonArgumentKind
+import pytest
+import docstring_parser
+
+def docstring_args_list(docstring: str) -> list[docstring_parser.DocstringParam]:
+ '''
+ Returns the parameter list of a docstring.
+
+ :param docstring: The docstring
+ :type docstring: str
+
+ :return: The parameter list.
+ :rtype: list[docstring_parser.DocstringParam]
+ '''
+
+ return docstring_parser.parse(docstring).params
+
+@pytest.mark.parametrize('arg_list,docstring,expected', [
+ ([
+ PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', 'int'),
+ ], ':param a: a\n:type a: str', {
+ 'a': _FunctionParamInfo('int', 'a')
+ }),
+ ([
+ PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', None),
+ ], ':param a: a\n:type a: str', {
+ 'a': _FunctionParamInfo('str', 'a')
+ }),
+ ([
+ PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', None),
+ PythonFunctionArgument('b', PythonArgumentKind.NORMAL, '', None),
+ ], ':param a: a\n:type a: str', {
+ 'a': _FunctionParamInfo('str', 'a'),
+ 'b': _FunctionParamInfo(None, None)
+ }),
+ ([
+ PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', None),
+ PythonFunctionArgument('b', PythonArgumentKind.NORMAL, '', 'int'),
+ ], ':param a: a\n:type a: str', {
+ 'a': _FunctionParamInfo('str', 'a'),
+ 'b': _FunctionParamInfo('int', None)
+ }),
+ ([
+ PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', None),
+ PythonFunctionArgument('b', PythonArgumentKind.NORMAL, '', 'int'),
+ ], ':param a: a\n:type a: str\n:param b: b\n:type b: str', {
+ 'a': _FunctionParamInfo('str', 'a'),
+ 'b': _FunctionParamInfo('int', 'b')
+ }),
+ ([
+ PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', None),
+ ], ':param a: a\n:type a: str\n:param b: b\n:type b: int', {
+ 'a': _FunctionParamInfo('str', 'a')
+ }),
+ ([
+ PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', None),
+ ], '', {
+ 'a': _FunctionParamInfo(None, None)
+ }),
+ ([
+ PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', 'str'),
+ ], '', {
+ 'a': _FunctionParamInfo('str', None)
+ }),
+ ([
+ PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', 'str'),
+ ], ':param a:\n:type a: int', {
+ 'a': _FunctionParamInfo('str', '')
+ }),
+ ([
+ PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', None),
+ ], ':param a:\n:type a: int', {
+ 'a': _FunctionParamInfo('int', '')
+ }),
+ ([
+ PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', None),
+ ], ':param a: a', {
+ 'a': _FunctionParamInfo(None, 'a')
+ }),
+ ([
+ PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', 'str'),
+ ], ':param a: a', {
+ 'a': _FunctionParamInfo('str', 'a')
+ }),
+ ([], ':param a: a\n:type a: str', {}),
+ ([], '', {}),
+])
+def test__function_params_types(arg_list, docstring, expected):
+ assert _function_params_info(arg_list, docstring_args_list(docstring)) == expected
\ No newline at end of file
diff --git a/tests/unit/core/python/_core/test__markdown_table.py b/tests/unit/core/python/_core/test__markdown_table.py
new file mode 100644
index 0000000..edabff7
--- /dev/null
+++ b/tests/unit/core/python/_core/test__markdown_table.py
@@ -0,0 +1,93 @@
+# 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 _markdown_table
+import pytest
+
+@pytest.mark.parametrize('header,body,allow_markdown_table,force_row_lengths,one_line_values,missing_value,expected', [
+ ([], [], False, False, True, '--', ''),
+ ([], [('a',)], False, False, True, '--', ''),
+ (['a'], [], False, False, True, '--', ''),
+ (['a'], [(1,)], True, False, True, '--', '''| a |
+| --- |
+| 1 |'''),
+ (['a'], [(1, 2)], True, False, True, '--', '''| a |
+| --- |
+| 1 |'''),
+ (['a', 'b'], [(1, 2)], True, False, True, '--', '''| a | b |
+| --- | --- |
+| 1 | 2 |'''),
+ (['a', 'b'], [(1,)], True, False, True, '--', '''| a | b |
+| --- | --- |
+| 1 | -- |'''),
+ (['a', 'b'], [()], True, False, True, '--', '''| a | b |
+| --- | --- |
+| -- | -- |'''),
+ (['a', 'b'], [(1, 2), (3, 4)], True, False, True, '--', '''| a | b |
+| --- | --- |
+| 1 | 2 |
+| 3 | 4 |'''),
+ (['a'], [('line1\nline2',)], True, False, True, '--', '''| a |
+| --- |
+| line1 line2 |'''),
+ (['a'], [('line1\nline2',)], True, False, False, '--', '''| a |
+| --- |
+| line1\nline2 |'''),
+
+ ([], [], False, False, True, '--', ''),
+ ([], [('a',)], False, False, True, '--', ''),
+ (['a'], [], False, False, True, '--', ''),
+ (['a'], [(1,)], False, False, True, '--', '''- a: 1'''),
+ (['a'], [(1, 2)], False, False, True, '--', '''- a: 1'''),
+ (['a', 'b'], [(1, 2)], False, False, True, '--', '''- a: 1
+
+ b: 2'''),
+ (['a', 'b'], [(1,)], False, False, True, '--', '''- a: 1
+
+ b: --'''),
+ (['a', 'b'], [()], False, False, True, '--', '''- a: --
+
+ b: --'''),
+ (['a', 'b'], [(1, 2), (3, 4)], False, False, True, '--', '''- a: 1
+
+ b: 2
+
+- a: 3
+
+ b: 4'''),
+ (['a'], [('line1\nline2',)], False, False, True, '--', '- a: line1 line2'),
+ (['a'], [('line1\nline2',)], False, False, False, '--', '- a: line1\nline2'),
+])
+def test__markdown_table(header, body, allow_markdown_table, force_row_lengths, one_line_values, missing_value, expected):
+ assert _markdown_table(header, body, allow_markdown_table = allow_markdown_table, force_row_lengths = force_row_lengths, one_line_values = one_line_values, missing_value = missing_value) == expected
+
+@pytest.mark.parametrize('header,body,allow_markdown_table,force_row_lengths,one_line_values,missing_value,expected_exception,expected_exception_msg', [
+ (['a'], [(1, 2)], True, True, True, '--', ValueError, 'expected 1 value, got 2'),
+ (['a'], [(1, 2, 3)], True, True, True, '--', ValueError, 'expected 1 value, got 3'),
+ (['a'], [()], True, True, True, '--', ValueError, 'expected 1 value, got 0'),
+ (['a', 'b'], [()], True, True, True, '--', ValueError, 'expected 2 values, got 0'),
+ (['a', 'b'], [(1,)], True, True, True, '--', ValueError, 'expected 2 values, got 1'),
+ (['a', 'b'], [(1, 2, 3)], True, True, True, '--', ValueError, 'expected 2 values, got 3'),
+
+ (['a'], [(1, 2)], False, True, True, '--', ValueError, 'expected 1 value, got 2'),
+ (['a'], [(1, 2, 3)], False, True, True, '--', ValueError, 'expected 1 value, got 3'),
+ (['a'], [()], False, True, True, '--', ValueError, 'expected 1 value, got 0'),
+ (['a', 'b'], [()], False, True, True, '--', ValueError, 'expected 2 values, got 0'),
+ (['a', 'b'], [(1,)], False, True, True, '--', ValueError, 'expected 2 values, got 1'),
+ (['a', 'b'], [(1, 2, 3)], False, True, True, '--', ValueError, 'expected 2 values, got 3'),
+])
+def test__markdown_table_exceptions(header, body, allow_markdown_table, force_row_lengths, one_line_values, missing_value, expected_exception, expected_exception_msg):
+ with pytest.raises(expected_exception) as exc_info:
+ _markdown_table(header, body, allow_markdown_table = allow_markdown_table, force_row_lengths = force_row_lengths, one_line_values = one_line_values, missing_value = missing_value)
+ assert str(exc_info.value) == expected_exception_msg
\ No newline at end of file
diff --git a/tests/unit/core/python/_core/test__signature_argument_list.py b/tests/unit/core/python/_core/test__signature_argument_list.py
new file mode 100644
index 0000000..86b87b0
--- /dev/null
+++ b/tests/unit/core/python/_core/test__signature_argument_list.py
@@ -0,0 +1,97 @@
+# 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 _signature_argument_list
+from src.jcloud_docsgen.core.python.arguments import PythonFunctionArgument, PythonArgumentKind
+import pytest
+
+@pytest.mark.parametrize('argument_list,expected', [
+ ([
+ PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, None)
+ ], ['a']),
+ ([
+ PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, None)
+ ], ['a', '/']),
+ ([
+ PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, None),
+ PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, None),
+ ], ['a', '/', 'b']),
+ ([
+ PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, None),
+ PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, None),
+ PythonFunctionArgument('c', PythonArgumentKind.KEYWORD_ONLY, None, None),
+ ], ['a', '/', 'b', '*', 'c']),
+ ([
+ PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, None),
+ PythonFunctionArgument('c', PythonArgumentKind.KEYWORD_ONLY, None, None),
+ ], ['b', '*', 'c']),
+ ([
+ PythonFunctionArgument('c', PythonArgumentKind.KEYWORD_ONLY, None, None),
+ ], ['*', 'c']),
+ ([
+ PythonFunctionArgument('b', PythonArgumentKind.POSITIONAL_ONLY, None, None),
+ PythonFunctionArgument('c', PythonArgumentKind.KEYWORD_ONLY, None, None),
+ ], ['b', '/', '*', 'c']),
+ ([
+ PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, None),
+ ], ['*args']),
+ ([
+ PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, None),
+ PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, None),
+ ], ['a', '*args']),
+ ([
+ PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, None),
+ PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, None),
+ ], ['a', '/', '*args']),
+ ([
+ PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, None),
+ PythonFunctionArgument('a', PythonArgumentKind.KEYWORD_ONLY, None, None),
+ ], ['*args', 'a']),
+ ([
+ PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, None),
+ PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, None),
+ ], ['*args', '**kwargs']),
+ ([
+ PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, None),
+ PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, None),
+ PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, None),
+ ], ['a', '/', '*args', '**kwargs']),
+ ([
+ PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, None),
+ PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, None),
+ PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, None),
+ ], ['a', '*args', '**kwargs']),
+ ([
+ PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, None),
+ PythonFunctionArgument('a', PythonArgumentKind.KEYWORD_ONLY, None, None),
+ PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, None),
+ ], ['*args', 'a', '**kwargs']),
+ ([
+ PythonFunctionArgument('a', PythonArgumentKind.KEYWORD_ONLY, None, None),
+ PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, None),
+ ], ['*', 'a', '**kwargs']),
+ ([
+ PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, None),
+ PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, None),
+ ], ['a', '**kwargs']),
+ ([
+ PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, None),
+ PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, None),
+ ], ['a', '/', '**kwargs']),
+ ([
+ PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '1', 'int'),
+ ], ['a: int = 1']),
+])
+def test__signature_argument_list(argument_list, expected):
+ assert _signature_argument_list(argument_list) == expected
\ No newline at end of file