From 3fd51d126f70d5b38e42785ea24fa5e7b0d55b4a Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Sun, 19 Apr 2026 19:12:33 +0200 Subject: [PATCH] Add Python definition documentation generator --- docs/CHANGELOG.md | 3 +- src/jcloud_docsgen/core/python/_core.py | 697 +++++++- src/jcloud_docsgen/core/python/docstrings.py | 6 - ..._PythonDefinitionDocumentationGenerator.py | 1413 +++++++++++++++++ .../test__argument_kind_human_readable.py | 27 + .../core/python/_core/test__fit_row_length.py | 56 + .../_core/test__function_params_info.py | 104 ++ .../core/python/_core/test__markdown_table.py | 93 ++ .../_core/test__signature_argument_list.py | 97 ++ 9 files changed, 2486 insertions(+), 10 deletions(-) create mode 100644 tests/unit/core/python/_core/test_PythonDefinitionDocumentationGenerator.py create mode 100644 tests/unit/core/python/_core/test__argument_kind_human_readable.py create mode 100644 tests/unit/core/python/_core/test__fit_row_length.py create mode 100644 tests/unit/core/python/_core/test__function_params_info.py create mode 100644 tests/unit/core/python/_core/test__markdown_table.py create mode 100644 tests/unit/core/python/_core/test__signature_argument_list.py 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