generated from jCloud/repository-template
Add Python definition documentation generator
This commit is contained in:
@@ -23,3 +23,4 @@
|
||||
- Add class for Python docstrings
|
||||
- Add feature to parse Python docstrings
|
||||
- Add function to ensure a non-empty string.
|
||||
- Add Python definition documentation generator
|
||||
@@ -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 += '<details><summary><h' + str(self.level + 1) + ' style="display:inline">Docstring' + '</h' + str(self.level + 1) + '></summary>'
|
||||
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 += '</details>'
|
||||
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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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.'
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user