style: format source code using Ruff

This commit is contained in:
jakob.scheid committed 2026-09-21 20:10:04 +02:00
1 parent d9887f34c9
commit aa4a139878
40 files changed
+1719 -1328

No files matched your search

+40 -37
View File
@@ -12,9 +12,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
'''
"""
The command line interface
'''
"""
import argparse
import pathlib
@@ -31,80 +31,81 @@ __all__ = [
_LANG_PYTHON = 'python'
_PROGRAM_NAME = 'jcloud-docsgen'
def existing_directory(path: str) -> pathlib.Path:
_path = path
path = pathlib.Path(_path)
if not path.exists():
raise argparse.ArgumentTypeError(
f'{_path}: no such file or directory'
)
raise argparse.ArgumentTypeError(f'{_path}: no such file or directory')
if not path.is_dir():
raise argparse.ArgumentTypeError(f'{_path}: not a directory')
return path
def error(message: str, exit_code: int = 1):
'''
"""
Shows an error message and returns the exit code.
:param message: The error message to show.
:type message: str
:param exit_code: The exit code.
:type exit_code: int
'''
"""
print(f'{_PROGRAM_NAME}: error: {message}', file = sys.stderr)
print(f'{_PROGRAM_NAME}: error: {message}', file=sys.stderr)
return exit_code
def create_argument_parser() -> argparse.ArgumentParser:
'''
"""
Prepares a parser to parse the command-line arguments using
argparse.
:return: The parser.
:rtype: argparse.ArgumentParser
'''
"""
common_parser = argparse.ArgumentParser(add_help = False)
common_parser = argparse.ArgumentParser(add_help=False)
common_parser.add_argument(
'project_directory',
type = existing_directory,
nargs = '?',
default = pathlib.Path('.'),
help = 'The directory of the project',
type=existing_directory,
nargs='?',
default=pathlib.Path('.'),
help='The directory of the project',
)
common_parser.add_argument(
'-o',
'--output-dir',
type = pathlib.Path,
help = 'the output directory the documentation will be written to',
type=pathlib.Path,
help='the output directory the documentation will be written to',
)
argument_parser = argparse.ArgumentParser()
subparsers = argument_parser.add_subparsers(
dest = 'language',
required = True,
dest='language',
required=True,
)
python_parser = subparsers.add_parser(
_LANG_PYTHON,
parents = [common_parser],
help = 'generate documentation for Python project',
parents=[common_parser],
help='generate documentation for Python project',
)
python_parser.add_argument(
'-s',
'--sections',
nargs = '+',
choices = VALID_PYTHON_SECTIONS,
help = 'the sections to be included in the documentation',
nargs='+',
choices=VALID_PYTHON_SECTIONS,
help='the sections to be included in the documentation',
)
python_parser.add_argument(
'-S',
'--sections-exclude',
nargs = '+',
default = [],
choices = VALID_PYTHON_SECTIONS,
help = (
nargs='+',
default=[],
choices=VALID_PYTHON_SECTIONS,
help=(
'the sections to be excluded in the documentation. '
'If a section shall be included AND excluded, it is excluded.'
),
@@ -112,8 +113,8 @@ def create_argument_parser() -> argparse.ArgumentParser:
python_parser.add_argument(
'-H',
'--allow-html',
action = 'store_true',
help = (
action='store_true',
help=(
'indicates whether HTML is allowed in the documentation. '
'By default, HTML is disabled.'
),
@@ -121,8 +122,8 @@ def create_argument_parser() -> argparse.ArgumentParser:
python_parser.add_argument(
'-T',
'--allow-tables',
action = 'store_true',
help = (
action='store_true',
help=(
'indicates whether Markdown tables are allowed in the '
'documentation. By default, Markdown tables are disabled. '
'Warning: Markdown tables are not CommonMark.'
@@ -131,7 +132,8 @@ def create_argument_parser() -> argparse.ArgumentParser:
return argument_parser
def main(argv = None) -> int:
def main(argv=None) -> int:
argument_parser = create_argument_parser()
args = argument_parser.parse_args(argv)
@@ -141,13 +143,14 @@ def main(argv = None) -> int:
try:
if args.language == _LANG_PYTHON:
from .languages.python import generate
generate(
args.project_directory,
args.output_dir,
include_sections = args.sections,
exclude_sections = args.sections_exclude,
allow_html = args.allow_html,
allow_tables = args.allow_tables,
include_sections=args.sections,
exclude_sections=args.sections_exclude,
allow_html=args.allow_html,
allow_tables=args.allow_tables,
)
except FileNotFoundError as exc:
return error(f'{exc.filename}: no such file or directory')
+24 -21
View File
@@ -29,11 +29,12 @@ VALID_SECTIONS = list(
PythonDefinitionDocumentationIncludeSections()._args.keys()
)
def parse_sections_list(
include_sections: Optional[list[str]] = None,
exclude_sections: list[str] = [],
) -> PythonDefinitionDocumentationIncludeSections:
'''
"""
Returns a Python definition documentation sections list from the
sections list to be included and the sections list to be excluded.
These lists both consist of the sections as strings which contain
@@ -46,7 +47,7 @@ def parse_sections_list(
:return: The Python definition documentation sections object.
:rtype: PythonDefinitionDocumentationIncludeSections
'''
"""
if include_sections is None:
include_sections = VALID_SECTIONS[:]
@@ -56,17 +57,18 @@ def parse_sections_list(
del include_sections[include_sections.index(section)]
return PythonDefinitionDocumentationIncludeSections(
signature = 'signature' in include_sections,
returns = 'returns' in include_sections,
params = 'params' in include_sections,
exceptions = 'exceptions' in include_sections,
examples = 'examples' in include_sections,
docstring = 'docstring' in include_sections,
bases = 'bases' in include_sections,
decorators = 'decorators' in include_sections,
class_body = 'class_body' in include_sections,
signature='signature' in include_sections,
returns='returns' in include_sections,
params='params' in include_sections,
exceptions='exceptions' in include_sections,
examples='examples' in include_sections,
docstring='docstring' in include_sections,
bases='bases' in include_sections,
decorators='decorators' in include_sections,
class_body='class_body' in include_sections,
)
def _generate(
project_directory: pathlib.Path,
output_directory: pathlib.Path,
@@ -77,7 +79,7 @@ def _generate(
allow_html: bool = False,
allow_tables: bool = False,
) -> None:
'''
"""
Generates the documentation for a Python project.
:param project_directory: The project directory.
@@ -92,17 +94,18 @@ def _generate(
:type allow_html: bool
:param allow_tables: Indicates whether Markdown tables are allowed.
Warning: Markdown tables are not CommonMark.
'''
"""
docs_generator = PythonDocumentationGenerator(
project_directory,
output_directory,
include_sections = include_sections,
allow_html = allow_html,
allow_tables = allow_tables,
include_sections=include_sections,
allow_html=allow_html,
allow_tables=allow_tables,
)
docs_generator.generate_documentation()
def generate(
project_directory: pathlib.Path,
output_directory: pathlib.Path,
@@ -112,7 +115,7 @@ def generate(
allow_html: bool = False,
allow_tables: bool = False,
) -> None:
'''
"""
Generates the documentation for a Python project.
:param project_directory: The project directory.
@@ -130,14 +133,14 @@ def generate(
:type allow_html: bool
:param allow_tables: Indicates whether Markdown tables are allowed.
Warning: Markdown tables are not CommonMark.
'''
"""
sections = parse_sections_list(include_sections, exclude_sections)
_generate(
project_directory,
output_directory,
include_sections = sections,
allow_html = allow_html,
allow_tables = allow_tables,
include_sections=sections,
allow_html=allow_html,
allow_tables=allow_tables,
)
+164 -135
View File
@@ -40,15 +40,16 @@ __all__ = [
'PythonDocumentationGenerator',
]
def _collect_definitions(tree_or_node) -> Iterator[PythonDefinition]:
'''
"""
Collects all definitions of the tree or node.
:param tree_or_node: The tree or node.
:return: The definitions.
:rtype: Iterator[PythonDefinition]
'''
"""
for node in tree_or_node.body:
if isinstance(node, ast.FunctionDef):
@@ -58,6 +59,7 @@ def _collect_definitions(tree_or_node) -> Iterator[PythonDefinition]:
if isinstance(node, ast.ClassDef):
yield PythonClassDefinition.from_node(node)
_Arguments: TypeAlias = tuple[
list[PythonFunctionArgument],
list[PythonFunctionArgument],
@@ -66,10 +68,11 @@ _Arguments: TypeAlias = tuple[
Union[PythonFunctionArgument, None],
]
def _group_argument_list(
argument_list: list[PythonFunctionArgument],
) -> _Arguments:
'''
"""
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.
@@ -78,13 +81,12 @@ def _group_argument_list(
:type argument_list: list[PythonFunctionArgument]
:rtype: _Arguments
'''
"""
if argument_list:
if argument_list[-1].kind != PythonArgumentKind.KWARGS and any([
arg.kind == PythonArgumentKind.KWARGS
for arg in 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',
)
@@ -106,7 +108,7 @@ def _group_argument_list(
if vararg is not None:
raise PythonArgumentStructureError(
'* argument may only appear once',
definition = arg,
definition=arg,
)
vararg = arg
@@ -124,10 +126,11 @@ def _group_argument_list(
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.
@@ -135,7 +138,7 @@ def _signature_argument_list(
:return: The list of function signature argument strings.
:rtype: list[str]
'''
"""
(
positional_only_args,
@@ -170,8 +173,9 @@ def _signature_argument_list(
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.
@@ -181,7 +185,7 @@ def _fit_row_length(expected: int, row: tuple) -> tuple:
:type expected: int
:param row: The row.
:type row: tuple
'''
"""
if expected < 0:
raise ValueError('expected must be larger than or equal 0.')
@@ -191,6 +195,7 @@ def _fit_row_length(expected: int, row: tuple) -> tuple:
else:
return row + (None,) * (expected - len(row))
def _markdown_table(
header: tuple,
body: list[tuple],
@@ -200,7 +205,7 @@ def _markdown_table(
one_line_values: bool = True,
missing_value: str = '--',
) -> str:
'''
"""
Generates a table.
If ``allow_markdown_table`` is ``False``, an alternative
@@ -222,7 +227,7 @@ def _markdown_table(
: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 ''
@@ -231,37 +236,34 @@ def _markdown_table(
if allow_markdown_table:
table = (
'| ' \
+ ' | '.join(header) \
+ ' |\n|' \
+ ' --- |' * len(header) \
+ '\n'
'| ' + ' | '.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 ""
"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', ' ')
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'
]
)
+ ' |\n'
)
else:
table = ''
@@ -270,9 +272,7 @@ def _markdown_table(
if force_row_lengths:
raise ValueError(
f'expected {header_len} value{
"s"
if header_len > 1
else ""
"s" if header_len > 1 else ""
}, got {len(row)}',
)
else:
@@ -281,29 +281,25 @@ def _markdown_table(
sep = '- '
for i, col in enumerate(row):
table += (
sep \
+ header[i] \
+ ': ' \
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
str(col if col is not None else missing_value).replace(
'\n', ' '
)
) + '\n\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.
@@ -311,7 +307,7 @@ def _is_docstring(docstring: Union[str, PythonDocstring, None]) -> bool:
:return: Whether there is a docstring.
:rtype: bool
'''
"""
if docstring is None:
return False
@@ -321,11 +317,13 @@ def _is_docstring(docstring: Union[str, PythonDocstring, None]) -> bool:
return docstring.strip() != ''
@dataclass
class _FunctionParamInfo:
annotation: Union[str, None]
description: Union[str, None]
@dataclass
class _NoDocstringTemplate:
description = None
@@ -340,10 +338,12 @@ class _NoDocstringTemplate:
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``.
@@ -352,15 +352,16 @@ def _argument_kind_human_readable(argument_kind: PythonArgumentKind) -> str:
: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
@@ -377,11 +378,10 @@ def _function_params_info(
:return: The dictionary with the parameter types.
:rtype: dict[str, _FunctionParamInfo]
'''
"""
arg_annotations = {
arg.name: _FunctionParamInfo(arg.annotation, None)
for arg in arg_list
arg.name: _FunctionParamInfo(arg.annotation, None) for arg in arg_list
}
for arg in docstring_arg_list:
@@ -393,8 +393,9 @@ def _function_params_info(
return arg_annotations
class PythonDefinitionDocumentationIncludeSections:
'''
"""
A class representing the included sections in a Python definition
documentation.
@@ -414,7 +415,7 @@ class PythonDefinitionDocumentationIncludeSections:
:type bases: bool
:param decorators: Whether include the decorators section.
:type decorators: bool
'''
"""
def __init__(
self,
@@ -452,12 +453,17 @@ class PythonDefinitionDocumentationIncludeSections:
}
def __repr__(self) -> str:
return type(self).__name__+ '(' + ', '.join(
return (
type(self).__name__
+ '('
+ ', '.join(
[
key + ' = ' + repr(value)
for key, value in self._args.items()
],
) + ')'
)
+ ')'
)
def __eq__(self, value) -> str:
if not isinstance(
@@ -467,8 +473,9 @@ class PythonDefinitionDocumentationIncludeSections:
return False
return self._args == value._args
class PythonDefinitionDocumentationGenerator:
'''
"""
A documentation generator for a Python definition.
:param definition: The Python definition.
@@ -492,7 +499,7 @@ class PythonDefinitionDocumentationGenerator:
:type skip_empty_sections: bool
:param is_method: Controls whether the definition is a class method.
:type is_method: bool
'''
"""
def __init__(
self,
@@ -521,13 +528,15 @@ class PythonDefinitionDocumentationGenerator:
@property
def _docstring(self):
if self._docstring_cache is None:
self._docstring_cache = self.definition.doc.parse() \
if _is_docstring(self.definition.doc) \
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.
@@ -535,18 +544,23 @@ class PythonDefinitionDocumentationGenerator:
: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 = (
self.definition.name
+ '('
+ ', '.join(
_signature_argument_list(
self.definition.args,
),
) + ')'
)
+ ')'
)
if self.definition.returns is not None:
signature += ' -> ' + self.definition.returns
@@ -554,38 +568,38 @@ class PythonDefinitionDocumentationGenerator:
return signature.strip()
def _generate_signature_section(self) -> str:
'''
"""
Generates the signature section.
:return: The signature section.
:rtype: str
'''
"""
definition_type = 'Function' if not self.is_method else 'Method'
heading = self.level * '#' + '# ' + (definition_type) +' signature'
return f'''{heading}
heading = self.level * '#' + '# ' + (definition_type) + ' signature'
return f"""{heading}
```python
{self.generate_signature()}
```'''
```"""
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 += f'<details><summary><h{str(
self.level + 1
)} style="display:inline">Docstring</h{str(
self.level + 1
)}></summary>'
md += f'<details><summary><h{
str(self.level + 1)
} style="display:inline">Docstring</h{
str(self.level + 1)
}></summary>'
else:
md += self.level * '#' + '# Docstring\n\n'
@@ -602,12 +616,12 @@ class PythonDefinitionDocumentationGenerator:
return md.strip()
def _generate_params_section(self) -> str:
'''
"""
Generates the parameters section.
:return: The parameters section.
:rtype: str
'''
"""
md = ''
@@ -636,7 +650,7 @@ class PythonDefinitionDocumentationGenerator:
)
for arg in self.definition.args
],
allow_markdown_table = self.allow_tables,
allow_markdown_table=self.allow_tables,
)
md += '\n\n'
@@ -647,12 +661,12 @@ class PythonDefinitionDocumentationGenerator:
return md.strip()
def _generate_returns_section(self) -> str:
'''
"""
Generates the returns section.
:return: The returns section.
:rtype: str
'''
"""
md = ''
@@ -686,12 +700,12 @@ class PythonDefinitionDocumentationGenerator:
return md.strip()
def _generate_exceptions_section(self) -> str:
'''
"""
Generates the exceptions section.
:return: The exceptions section.
:rtype: str
'''
"""
md = ''
@@ -699,26 +713,31 @@ class PythonDefinitionDocumentationGenerator:
md += self.level * '#' + '# Exceptions\n\n'
if self._docstring.raises:
md += _markdown_table(('Exception', 'Description'), [
md += _markdown_table(
('Exception', 'Description'),
[
(
('`' + exception.type_name + '`')
if exception.type_name is not None
else None, non_empty_str(exception.description)
else None,
non_empty_str(exception.description),
)
for exception in self._docstring.raises
], allow_markdown_table = self.allow_tables)
],
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 = ''
@@ -727,9 +746,11 @@ class PythonDefinitionDocumentationGenerator:
examples = []
for example in self._docstring.examples:
examples.append('```python\n' +\
(example.snippet or example.description) + \
'\n```')
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:
@@ -738,12 +759,12 @@ class PythonDefinitionDocumentationGenerator:
return md.strip()
def _generate_decorators_section(self) -> str:
'''
"""
Generates the decorators section.
:return: The decorators section.
:rtype: str
'''
"""
md = ''
@@ -759,12 +780,12 @@ class PythonDefinitionDocumentationGenerator:
return md.strip()
def _generate_bases_section(self) -> str:
'''
"""
Generates the bases section.
:return: The bases section.
:rtype: str
'''
"""
md = ''
@@ -780,12 +801,12 @@ class PythonDefinitionDocumentationGenerator:
return md.strip()
def _generate_body_section(self) -> str:
'''
"""
Generates the body section.
:return: The body section.
:rtype: str
'''
"""
md = ''
@@ -793,15 +814,18 @@ class PythonDefinitionDocumentationGenerator:
md += self.level * '#' + '# Body\n\n'
for method_or_class in self.definition.body:
md += PythonDefinitionDocumentationGenerator(
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'
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.'
@@ -809,12 +833,12 @@ class PythonDefinitionDocumentationGenerator:
return md.strip()
def generate_documentation(self) -> str:
'''
"""
Generates the documentation in the markdown format.
:return: The documentation.
:rtype: str
'''
"""
md = self.level * '#' + ' '
@@ -874,8 +898,9 @@ class PythonDefinitionDocumentationGenerator:
return md.strip()
class PythonModuleDocumentationGenerator:
'''
"""
A documentation generator for a Python module.
:param module_path: The path of the module file.
@@ -887,7 +912,7 @@ class PythonModuleDocumentationGenerator:
:param include_sections: The sections that will be included.
:type include_sections: PythonDefinitionDocumentationIncludeSections
:param kwargs: The options for ``PythonDefinitionDocumentationGenerator``.
'''
"""
def __init__(
self,
@@ -901,7 +926,7 @@ class PythonModuleDocumentationGenerator:
if module_path.name.count('.') > 1:
raise InvalidNamespaceError(
'namespace identifier cannot contain more than one dot',
namespace_identifier = module_path.name,
namespace_identifier=module_path.name,
)
self.module_path = pathlib.Path(str(module_path))
@@ -930,25 +955,24 @@ class PythonModuleDocumentationGenerator:
return self._ast_cache
def collect_definitions(self) -> Iterator[PythonDefinition]:
'''
"""
Collects all function and class definitions of the module.
:return: All definitions of the module
:rtype: Iterator[PythonDefinition]
'''
"""
tree = self._ast
return _collect_definitions(tree)
def generate_documentation(self) -> str:
'''
"""
Generates the documentation in the markdown format.
:return: The documentation.
:rtype: str
'''
"""
md = '# Module `'
@@ -973,15 +997,19 @@ class PythonModuleDocumentationGenerator:
md += docstring + '\n\n'
for definition in self.collect_definitions():
md += PythonDefinitionDocumentationGenerator(
md += (
PythonDefinitionDocumentationGenerator(
definition,
self.include_sections,
2,
**self.kwargs,
).generate_documentation() + '\n\n'
).generate_documentation()
+ '\n\n'
)
return md.strip()
def _generate_package_namespace_documentation(
namespace: PythonPackageNamespace,
documentation_directory: pathlib.Path,
@@ -991,8 +1019,8 @@ def _generate_package_namespace_documentation(
PythonDefinitionDocumentationIncludeSections()
),
kwargs: dict = {},
) -> None:
'''
) -> None:
"""
Generates the documentation for a package namespace and writes it to
the documentation directory.
@@ -1007,19 +1035,20 @@ def _generate_package_namespace_documentation(
:param include_sections: The sections to be included in the documentation.
:type include_sections: PythonDefinitionDocumentationIncludeSections
:param kwargs: The options for the documentation generator.
'''
"""
documentation_directory = pathlib.Path(str(documentation_directory))
for sub_namespace in namespace.sub_namespaces:
if isinstance(sub_namespace, PythonPackageNamespace):
(
documentation_directory / sub_namespace.name
).mkdir(mode = 0o700, exist_ok = True)
(documentation_directory / sub_namespace.name).mkdir(
mode=0o700, exist_ok=True
)
_generate_package_namespace_documentation(
sub_namespace,
documentation_directory / sub_namespace.name,
root_directory, dir / sub_namespace.name,
root_directory,
dir / sub_namespace.name,
include_sections,
kwargs,
)
@@ -1035,8 +1064,9 @@ def _generate_package_namespace_documentation(
).generate_documentation()
)
class PythonDocumentationGenerator:
'''
"""
The class for the documentation generator.
:param project_directory: The directory of the project.
@@ -1046,7 +1076,7 @@ class PythonDocumentationGenerator:
:param include_sections: The sections to be included in the documentation.
:type include_sections: PythonDefinitionDocumentationIncludeSections
:param kwargs: The options for ``PythonDefinitionDocumentationGenerator``.
'''
"""
def __init__(
self,
@@ -1074,25 +1104,24 @@ class PythonDocumentationGenerator:
return namespace
def namespace(self) -> list[PythonPackageNamespace]:
'''
"""
Returns the project as a namespace.
:return: The project as a namespace.
:rtype: list[PythonPackageNamespace]
'''
"""
src_dir = self.project_directory / 'src'
return [
self._namespace(dir)
for dir in src_dir.iterdir() if dir.is_dir()
self._namespace(dir) for dir in src_dir.iterdir() if dir.is_dir()
]
def generate_documentation(self) -> None:
'''
"""
Generates the full project documentation and writes it to the
documentation directory.
'''
"""
_generate_package_namespace_documentation(
PythonPackageNamespace('src', self.namespace()),
+35 -31
View File
@@ -31,6 +31,7 @@ __all__ = [
'PythonASTArgumentsListParser',
]
class PythonArgumentKind(Enum):
NORMAL = 0
POSITIONAL_ONLY = 1
@@ -38,8 +39,9 @@ class PythonArgumentKind(Enum):
VARARG = 3
KWARGS = 4
class PythonFunctionArgument:
'''
"""
Represents an argument of a Python function.
:param name: The name of the argument.
@@ -50,7 +52,7 @@ class PythonFunctionArgument:
:type default: Union[str, None]
:param annotation: The type annotation of the argument.
:type annotation: Union[str, None]
'''
"""
def __init__(
self,
@@ -68,7 +70,7 @@ class PythonFunctionArgument:
if not name or not name.isidentifier() or keyword.iskeyword(name):
raise InvalidPythonIdentifierError(
'invalid identifier',
identifier = name,
identifier=name,
)
# check whether the annotation is a valid annotation
@@ -77,7 +79,7 @@ class PythonFunctionArgument:
if annotation is not None and not annotation:
raise InvalidPythonAnnotationError(
'invalid annotation',
annotation = annotation,
annotation=annotation,
)
self.name = name
@@ -85,13 +87,17 @@ class PythonFunctionArgument:
self.default = default
self.annotation = annotation
if self.kind in (
if (
self.kind
in (
PythonArgumentKind.VARARG,
PythonArgumentKind.KWARGS,
) and self.default is not None:
)
and self.default is not None
):
raise PythonArgumentStructureError(
'* or ** arguments cannot have default values',
definition = self.signature_repr(),
definition=self.signature_repr(),
)
def __eq__(self, value: PythonFunctionArgument) -> bool:
@@ -105,20 +111,22 @@ class PythonFunctionArgument:
)
def __repr__(self) -> str:
return type(self).__name__ + repr((
return type(self).__name__ + repr(
(
self.name,
self.kind,
self.default,
self.annotation,
))
)
)
def signature_repr(self) -> str:
'''
"""
Returns the function signature representation of the argument.
:return: The string representation of the argument.
:rtype: str
'''
"""
if self.kind == PythonArgumentKind.KWARGS:
str_repr = '**'
@@ -137,48 +145,44 @@ class PythonFunctionArgument:
return str_repr
class PythonASTArgumentsListParser:
'''
"""
A parser for making ``PythonFunctionArgument`` objects from an
``ast.arguments`` arguments list.
:param ast_arguments_list: The ``ast.arguments`` arguments list.
:type ast_arguments_list: ast.arguments
'''
"""
def __init__(self, ast_arguments_list: ast.arguments) -> None:
self.ast_arguments_list = ast_arguments_list
def to_argument_list(self) -> list[PythonFunctionArgument]:
'''
"""
Converts the AST arguments list to a list of Python function
argument objects.
:return: The list of Python function argument objects.
:rtype: list[PythonFunctionArgument]
'''
"""
defaults = (
[
None
for _ in range(len(
self.ast_arguments_list.posonlyargs +
self.ast_arguments_list.args +
self.ast_arguments_list.kwonlyargs
) - len(
self.ast_arguments_list.defaults
) - len(
self.ast_arguments_list.kw_defaults
))
for _ in range(
len(
self.ast_arguments_list.posonlyargs
+ self.ast_arguments_list.args
+ self.ast_arguments_list.kwonlyargs
)
- len(self.ast_arguments_list.defaults)
- len(self.ast_arguments_list.kw_defaults)
)
]
+ [ast.unparse(d) for d in self.ast_arguments_list.defaults]
+ [
ast.unparse(d)
for d in self.ast_arguments_list.defaults
]
+ [
ast.unparse(d)
if d is not None
else None
ast.unparse(d) if d is not None else None
for d in self.ast_arguments_list.kw_defaults
]
)
+23 -22
View File
@@ -29,8 +29,9 @@ __all__ = [
'PythonClassDefinition',
]
def _python_docstring(doc: Union[str, None]) -> Union[PythonDocstring, None]:
'''
"""
Returns the PythonDocstring object if the docstring is a string and
not ``None``.
@@ -39,12 +40,13 @@ def _python_docstring(doc: Union[str, None]) -> Union[PythonDocstring, None]:
:return: The PythonDocstring object or ``None``.
:rtype: Union[PythonDocstring, None]
'''
"""
return PythonDocstring(doc) if doc is not None else None
class PythonDefinition:
'''
"""
A base class for Python definition (a class, function or asynchronous
function definition) classes.
@@ -57,7 +59,7 @@ class PythonDefinition:
:raises InvalidPythonIdentifierError: If the name is not a valid
Python identifier.
'''
"""
def __init__(
self,
@@ -68,7 +70,7 @@ class PythonDefinition:
if keyword.iskeyword(name) or not name.isidentifier() or not name:
raise InvalidPythonIdentifierError(
'invalid identifier',
identifier = name,
identifier=name,
)
assert_that_is_instance(doc, (PythonDocstring, types.NoneType))
@@ -89,12 +91,12 @@ class PythonDefinition:
@classmethod
def from_node(cls, node: ast.stmt) -> PythonDefinition:
'''
"""
From an AST node.
:param node: The AST node.
:type node: ast.stmt
'''
"""
return cls(
node.name,
@@ -107,8 +109,9 @@ class PythonDefinition:
return False
return self._arg_list() == value._arg_list()
class PythonFunctionDefinition(PythonDefinition):
'''
"""
A PythonDefinition subclass representing a Python function
definition.
@@ -125,7 +128,7 @@ class PythonFunctionDefinition(PythonDefinition):
:raises InvalidPythonIdentifierError: If the name is not a valid
Python identifier.
'''
"""
def __init__(
self,
@@ -150,12 +153,12 @@ class PythonFunctionDefinition(PythonDefinition):
@classmethod
def from_node(cls, node: ast.stmt) -> PythonFunctionDefinition:
'''
"""
From an AST node.
:param node: The AST node.
:type node: ast.stmt
'''
"""
return cls(
node.name,
@@ -165,8 +168,9 @@ class PythonFunctionDefinition(PythonDefinition):
[ast.unparse(d) for d in node.decorator_list],
)
class PythonAsyncFunctionDefinition(PythonFunctionDefinition):
'''
"""
A PythonDefinition subclass representing an asynchronous Python function
definition.
@@ -183,10 +187,11 @@ class PythonAsyncFunctionDefinition(PythonFunctionDefinition):
:raises InvalidPythonIdentifierError: If the name is not a valid
Python identifier.
'''
"""
class PythonClassDefinition(PythonDefinition):
'''
"""
A PythonDefinition subclass representing a Python class definition.
:param name: The name of the definition.
@@ -202,7 +207,7 @@ class PythonClassDefinition(PythonDefinition):
:raises InvalidPythonIdentifierError: If the name is not a valid
Python identifier.
'''
"""
def __init__(
self,
@@ -227,12 +232,12 @@ class PythonClassDefinition(PythonDefinition):
@classmethod
def from_node(cls, node: ast.stmt) -> PythonClassDefinition:
'''
"""
From an AST node.
:param node: The AST node.
:type node: ast.stmt
'''
"""
return cls(
node.name,
@@ -242,16 +247,12 @@ class PythonClassDefinition(PythonDefinition):
[
PythonFunctionDefinition.from_node(expr)
if isinstance(expr, ast.FunctionDef)
else PythonAsyncFunctionDefinition.from_node(expr)
if isinstance(expr, ast.AsyncFunctionDef)
else PythonClassDefinition.from_node(expr)
for expr in node.body
if isinstance(
expr,
(ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)
expr, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)
)
],
)
+6 -7
View File
@@ -15,17 +15,16 @@
from enum import Enum
import docstring_parser
__all__ = [
'PythonDocstring'
]
__all__ = ['PythonDocstring']
class PythonDocstring:
'''
"""
Represents a Python docstring.
:param docstring: The docstring as a string.
:type docstring: str
'''
"""
def __init__(self, docstring: str) -> None:
self.docstring = docstring
@@ -39,11 +38,11 @@ class PythonDocstring:
return self.docstring == value.docstring
def parse(self) -> docstring_parser.Docstring:
'''
"""
Parses the docstring.
:return: The parsed docstring.
:rtype: docstring_parser.Docstring
'''
"""
return docstring_parser.parse(self.docstring)
+24 -19
View File
@@ -27,20 +27,22 @@ __all__ = [
'PythonModuleNamespace',
]
class PythonNamespace:
'''
"""
The base class for all Python namespaces such as modules or packages.
:param name: The name of the namespace.
:type name: str
'''
"""
def __init__(self, name: str) -> None:
assert_that_is_instance(name, str)
if not name.strip():
raise InvalidNamespaceError(
'invalid namespace',
namespace_identifier = name,
namespace_identifier=name,
)
self.name = name
@@ -48,6 +50,7 @@ class PythonNamespace:
def __repr__(self) -> str:
return f'{type(self).__name__}({self.name!r})'
PythonPackageNamespaceDictType: TypeAlias = Dict[
str,
Union[
@@ -56,15 +59,16 @@ PythonPackageNamespaceDictType: TypeAlias = Dict[
],
]
class PythonPackageNamespace(PythonNamespace):
'''
"""
A subclass of ``PythonNamespace`` for Python package namespaces.
:param name: The name of the namespace.
:type name: str
:param sub_namespaces: The sub-namespaces.
:type sub_namespaces: list[PythonNamespace]
'''
"""
def __init__(
self,
@@ -78,7 +82,7 @@ class PythonPackageNamespace(PythonNamespace):
self._sub_namespaces = sub_namespaces
def namespace(self, sub_namespaces: List[str]) -> PythonNamespace:
'''
"""
Returns the namespace object with a specific identifier, such as
``['package', 'subpackage', 'module']``.
@@ -91,7 +95,7 @@ class PythonPackageNamespace(PythonNamespace):
:return: The namespace.
:rtype: PythonNamespace
'''
"""
if not sub_namespaces:
raise InvalidNamespaceError('invalid namespace')
@@ -115,7 +119,7 @@ class PythonPackageNamespace(PythonNamespace):
self,
package: PythonPackageNamespace,
) -> PythonPackageNamespaceDictType:
'''
"""
Returns the names of all sub-namespaces of the specified package
namespace and keeps the package structure.
@@ -124,7 +128,7 @@ class PythonPackageNamespace(PythonNamespace):
:return: The names if all sub-namespaces.
:rtype: PythonPackageNamespaceDictType
'''
"""
return {
sn.name: (
@@ -136,13 +140,13 @@ class PythonPackageNamespace(PythonNamespace):
}
def sub_namespace_names(self) -> PythonPackageNamespaceDictType:
'''
"""
Returns the names of all sub-namespaces and keeps the package
structure.
:return: The names if all sub-namespaces.
:rtype: PythonPackageNamespaceDictType
'''
"""
return self._sub_namespaces_names(self)
@@ -159,7 +163,7 @@ class PythonPackageNamespace(PythonNamespace):
return type(self).__name__ + repr((self.name, self._sub_namespaces))
def __getitem__(self, name: str) -> PythonNamespace:
'''
"""
Returns a namespace with the specified name.
:param name: The name of the namespace
@@ -169,7 +173,7 @@ class PythonPackageNamespace(PythonNamespace):
:return: The namespace
:rtype: PythonNamespace
'''
"""
for sub in self._sub_namespaces:
if sub.name == name:
@@ -177,11 +181,11 @@ class PythonPackageNamespace(PythonNamespace):
raise NamespaceNotFoundError(
'no such namespace',
namespace_identifier = name,
namespace_identifier=name,
)
def add(self, namespace: PythonNamespace) -> None:
'''
"""
Adds a namespace.
:param namespace: The namespace to add.
@@ -190,12 +194,12 @@ class PythonPackageNamespace(PythonNamespace):
:raises NamespaceExistsError: If a namespace with the name of the
namespace that is attempted to add
already exists.
'''
"""
try:
self.__getitem__(namespace.name)
raise NamespaceExistsError(
'namespace already exists',
namespace_identifier = namespace.name,
namespace_identifier=namespace.name,
)
except NamespaceNotFoundError:
self._sub_namespaces.append(namespace)
@@ -204,13 +208,14 @@ class PythonPackageNamespace(PythonNamespace):
def sub_namespaces(self) -> List[PythonNamespace]:
return self._sub_namespaces
class PythonModuleNamespace(PythonNamespace):
'''
"""
A subclass of ``PythonNamespace`` for Python module namespaces.
:param name: The name of the namespace.
:type name: str
'''
"""
def __init__(self, name: str) -> None:
super().__init__(name)
+32 -40
View File
@@ -12,10 +12,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
class NamespaceError(ValueError):
'''
"""
Base class for namespace errors.
'''
"""
def __init__(self, *args: object, namespace_identifier: str = '') -> None:
super().__init__(*args)
self.namespace_identifier = namespace_identifier
@@ -25,23 +27,24 @@ class NamespaceError(ValueError):
return ''
else:
return f'{self.args[0]}{
": "
if self.namespace_identifier and self.args[0]
else ""
}{
self.namespace_identifier
if self.args[0]
else ""
}'
": " if self.namespace_identifier and self.args[0] else ""
}{self.namespace_identifier if self.args[0] else ""}'
class InvalidNamespaceError(NamespaceError): ...
class NamespaceNotFoundError(NamespaceError): ...
class NamespaceExistsError(NamespaceError): ...
class PythonIdentifierError(ValueError):
'''
"""
Base class for Python identifier errors.
'''
"""
def __init__(self, *args: object, identifier: str = '') -> None:
super().__init__(*args)
self.identifier = identifier
@@ -51,21 +54,18 @@ class PythonIdentifierError(ValueError):
return ''
else:
return f'{self.args[0]}{
": "
if self.identifier and self.args[0]
else ""
}{
self.identifier
if self.args[0]
else ""
}'
": " if self.identifier and self.args[0] else ""
}{self.identifier if self.args[0] else ""}'
class InvalidPythonIdentifierError(PythonIdentifierError): ...
class PythonAnnotationError(ValueError):
'''
"""
Base class for Python annotation errors.
'''
"""
def __init__(self, *args: object, annotation: str = '') -> None:
super().__init__(*args)
self.annotation = annotation
@@ -75,21 +75,18 @@ class PythonAnnotationError(ValueError):
return ''
else:
return f'{self.args[0]}{
": "
if self.annotation and self.args[0]
else ""
}{
self.annotation
if self.args[0]
else ""
}'
": " if self.annotation and self.args[0] else ""
}{self.annotation if self.args[0] else ""}'
class InvalidPythonAnnotationError(PythonAnnotationError): ...
class PythonDefinitionError(ValueError):
'''
"""
Base class for Python definition errors.
'''
"""
def __init__(self, *args: object, definition: str = '') -> None:
super().__init__(*args)
self.definition = definition
@@ -99,13 +96,8 @@ class PythonDefinitionError(ValueError):
return ''
else:
return f'{self.args[0]}{
": "
if self.definition and self.args[0]
else ""
}{
self.definition
if self.args[0]
else ""
}'
": " if self.definition and self.args[0] else ""
}{self.definition if self.args[0] else ""}'
class PythonArgumentStructureError(PythonDefinitionError): ...
+47 -28
View File
@@ -12,9 +12,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
'''
"""
The utilities for jcloud_docsgen
'''
"""
import pathlib
import os
@@ -27,15 +27,17 @@ try:
except ImportError:
from typing_extensions import deprecated
@deprecated('Use pathlib.Path instead.')
class ExistingDirectory(pathlib.Path):
'''
"""
pathlib.Path subclass that is a directory.
ExistingDirectory represents a filesystem path but it also checks
whether the path exists and is not a file but a directory. It
inherits from pathlib.Path.
'''
"""
def __init__(self, path, *args, **kwargs) -> None:
if not os.path.exists(path):
raise FileNotFoundError(
@@ -47,15 +49,17 @@ class ExistingDirectory(pathlib.Path):
raise NotADirectoryError(errno.ENOTDIR, 'not a directory', path)
super().__init__(*(path, *args), **kwargs)
@deprecated('Use pathlib.Path instead.')
class ExistingFile(pathlib.Path):
'''
"""
pathlib.Path subclass that is a file.
ExistingFile represents a filesystem path but it also checks whether
the path exists and is not a directory but a file. It inherits from
pathlib.Path.
'''
"""
def __init__(self, path, *args, **kwargs) -> None:
if not os.path.exists(path):
raise FileNotFoundError(
@@ -67,8 +71,9 @@ class ExistingFile(pathlib.Path):
raise IsADirectoryError(errno.EISDIR, 'is a directory', path)
super().__init__(*(path, *args), **kwargs)
def _quote(string: str, quotation_mark: str) -> str:
'''
"""
Quotes a string.
:param string: The string to quote
@@ -78,16 +83,17 @@ def _quote(string: str, quotation_mark: str) -> str:
:return: The quoted string
:rtype: str
'''
"""
return quotation_mark + string + quotation_mark
def human_readable_list(
ls: list,
final_separator: str = 'and',
quotation_mark: str = '',
) -> str:
'''
"""
Generates a human-readable list from a list.
:param ls: The list.
@@ -101,7 +107,7 @@ def human_readable_list(
:return: The human-readable list.
:rtype: str
'''
"""
# empty list
if not ls:
@@ -111,18 +117,26 @@ def human_readable_list(
if len(ls) == 1:
return _quote(str(ls[0]), quotation_mark)
return ', '.join(
return (
', '.join(
[
_quote(
str(obj),
quotation_mark
str(obj), quotation_mark
) # quote the string representation of the object
for obj in ls[:-1] # do not include the last element of the list
for obj in ls[
:-1
] # do not include the last element of the list
],
) + ' ' + final_separator + ' ' + _quote(str(ls[-1]), quotation_mark)
)
+ ' '
+ final_separator
+ ' '
+ _quote(str(ls[-1]), quotation_mark)
)
def _list_type_names(types: list[type]) -> list[str]:
'''
"""
Converts a list of types into a list of their names (``__name__``
attribute)
@@ -131,10 +145,11 @@ def _list_type_names(types: list[type]) -> list[str]:
:return: The list of the names of the types
:rtype: list[str]
'''
"""
return [tp.__name__ for tp in types]
def assert_that_is_instance(
obj: object,
class_or_tuple: Union[
@@ -145,8 +160,8 @@ def assert_that_is_instance(
) -> None:
if not isinstance(class_or_tuple, Union[type, types.UnionType, tuple]):
raise TypeError(
f'class_or_tuple: expected \'Union[type, types.UnionType, '
f'tuple[type, ...]]\', \'got {type(class_or_tuple).__name__}\'',
f"class_or_tuple: expected 'Union[type, types.UnionType, "
f"tuple[type, ...]]', 'got {type(class_or_tuple).__name__}'",
)
if not isinstance(obj, class_or_tuple):
if isinstance(class_or_tuple, (tuple, types.UnionType)):
@@ -157,18 +172,21 @@ def assert_that_is_instance(
else:
exception_message_expected = ''
exception_message_expected += human_readable_list(
_list_type_names(class_or_tuple), 'or', '\'',
_list_type_names(class_or_tuple),
'or',
"'",
)
else:
exception_message_expected = '\'' + class_or_tuple.__name__ + '\''
exception_message_expected = "'" + class_or_tuple.__name__ + "'"
raise TypeError(
f'expected {exception_message_expected}, got \'{type(
obj
).__name__}\'',
f"expected {exception_message_expected}, got '{
type(obj).__name__
}'",
)
def non_empty_str(value: Union[str, None]) -> Union[str, None]:
'''
"""
Returns the value or ``None``.
Returns the value if it is not empty and not `Ǹone``; otherwise,
@@ -181,7 +199,7 @@ def non_empty_str(value: Union[str, None]) -> Union[str, None]:
:return: The value or ``None``.
:rtype: Union[str, None]
'''
"""
if value is None:
return None
@@ -191,11 +209,12 @@ def non_empty_str(value: Union[str, None]) -> Union[str, None]:
return value
def get_relative_path_segments(
path: pathlib.Path,
root: pathlib.Path,
) -> list[str]:
'''
"""
Returns the path segments relative to ``root``.
:param path: The path.
@@ -205,7 +224,7 @@ def get_relative_path_segments(
:return: The path segments relative to ``root``.
:rtype: list[str]
'''
"""
return list(
path.resolve()
+9 -7
View File
@@ -15,6 +15,7 @@
import subprocess
import sys
def test_cli():
result = subprocess.run(
[
@@ -23,12 +24,13 @@ def test_cli():
'jcloud_docsgen',
'--help',
],
capture_output = True,
text = True,
capture_output=True,
text=True,
)
stdout = result.stdout.lower()
assert \
'usage:' in stdout and \
'positional arguments' in stdout and \
'options' in stdout and \
'--help' in stdout
assert (
'usage:' in stdout
and 'positional arguments' in stdout
and 'options' in stdout
and '--help' in stdout
)
@@ -1,5 +1,5 @@
def f(a: int = 42) -> str:
'''
"""
Does something.
:param a: A parameter.
@@ -7,5 +7,5 @@ def f(a: int = 42) -> str:
:return: A string.
:rtype: str
'''
"""
...
+12 -4
View File
@@ -15,20 +15,28 @@
import pytest
from jcloud_docsgen.cli.cli import create_argument_parser
@pytest.mark.parametrize('lang', [
@pytest.mark.parametrize(
'lang',
[
'javascript',
'Python',
'PYTHON',
])
],
)
def test_language_error(lang):
parser = create_argument_parser()
with pytest.raises(SystemExit):
parser.parse_args([lang])
@pytest.mark.parametrize('lang', [
@pytest.mark.parametrize(
'lang',
[
'python',
])
],
)
def test_language_success(lang):
parser = create_argument_parser()
parser.parse_args([lang])
+85 -68
View File
@@ -17,68 +17,71 @@ import pytest
from jcloud_docsgen.cli.cli import create_argument_parser, main as cli_main
from jcloud_docsgen.cli.languages.python import parse_sections_list
from jcloud_docsgen.core.python import (
PythonDefinitionDocumentationIncludeSections
PythonDefinitionDocumentationIncludeSections,
)
@pytest.mark.parametrize('include,exclude,expected', [
@pytest.mark.parametrize(
'include,exclude,expected',
[
(
[],
[],
PythonDefinitionDocumentationIncludeSections(
signature = False,
returns = False,
params = False,
exceptions = False,
examples = False,
docstring = False,
bases = False,
decorators = False,
class_body = False,
signature=False,
returns=False,
params=False,
exceptions=False,
examples=False,
docstring=False,
bases=False,
decorators=False,
class_body=False,
),
),
(
[],
['signature'],
PythonDefinitionDocumentationIncludeSections(
signature = False,
returns = False,
params = False,
exceptions = False,
examples = False,
docstring = False,
bases = False,
decorators = False,
class_body = False,
signature=False,
returns=False,
params=False,
exceptions=False,
examples=False,
docstring=False,
bases=False,
decorators=False,
class_body=False,
),
),
(
['signature'],
['signature'],
PythonDefinitionDocumentationIncludeSections(
signature = False,
returns = False,
params = False,
exceptions = False,
examples = False,
docstring = False,
bases = False,
decorators = False,
class_body = False,
signature=False,
returns=False,
params=False,
exceptions=False,
examples=False,
docstring=False,
bases=False,
decorators=False,
class_body=False,
),
),
(
['signature'],
[],
PythonDefinitionDocumentationIncludeSections(
signature = True,
returns = False,
params = False,
exceptions = False,
examples = False,
docstring = False,
bases = False,
decorators = False,
class_body = False,
signature=True,
returns=False,
params=False,
exceptions=False,
examples=False,
docstring=False,
bases=False,
decorators=False,
class_body=False,
),
),
(
@@ -90,74 +93,87 @@ from jcloud_docsgen.core.python import (
['signature', 'exceptions', 'bases', 'class_body'],
['signature', 'bases'],
PythonDefinitionDocumentationIncludeSections(
signature = False,
returns = False,
params = False,
exceptions = True,
examples = False,
docstring = False,
bases = False,
decorators = False,
class_body = True,
signature=False,
returns=False,
params=False,
exceptions=True,
examples=False,
docstring=False,
bases=False,
decorators=False,
class_body=True,
),
),
(
None,
['params', 'docstring'],
PythonDefinitionDocumentationIncludeSections(
signature = True,
returns = True,
params = False,
exceptions = True,
examples = True,
docstring = False,
bases = True,
decorators = True,
class_body = True,
signature=True,
returns=True,
params=False,
exceptions=True,
examples=True,
docstring=False,
bases=True,
decorators=True,
class_body=True,
),
)
])
),
],
)
def test_parse_sections_list(include, exclude, expected):
assert parse_sections_list(include, exclude) == expected
def test_sections_error():
parser = create_argument_parser()
with pytest.raises(SystemExit):
parser.parse_args([
parser.parse_args(
[
'python',
'-s',
'param', # correct: `params`
])
]
)
with pytest.raises(SystemExit):
parser.parse_args([
parser.parse_args(
[
'python',
'-s',
'params',
'docstrng', # correct: `docstring`
])
]
)
def test_sections_success():
parser = create_argument_parser()
parser.parse_args([
parser.parse_args(
[
'python',
'-s',
'params',
])
parser.parse_args([
]
)
parser.parse_args(
[
'python',
'-s',
'params',
'docstring',
])
]
)
def test_docs_generation(expectdir):
with expectdir('python_project') as output_dir:
output_dir: pathlib.Path
input_dir = pathlib.Path(__file__).parent / 'python_project/project'
cli_main([
cli_main(
[
'python',
'-s',
'params',
@@ -165,4 +181,5 @@ def test_docs_generation(expectdir):
'-HTo',
str(output_dir),
str(input_dir),
])
]
)
File diff suppressed because it is too large. Load diff
@@ -20,7 +20,10 @@ from src.jcloud_docsgen.core.python.namespaces import (
import pytest
import pathlib
@pytest.mark.parametrize('python_documentation_generator,expected', [
@pytest.mark.parametrize(
'python_documentation_generator,expected',
[
(
PythonDocumentationGenerator(
pathlib.Path(
@@ -44,9 +47,7 @@ import pathlib
[
PythonPackageNamespace(
'pkg',
[
PythonModuleNamespace('module')
],
[PythonModuleNamespace('module')],
),
],
),
@@ -67,23 +68,25 @@ import pathlib
PythonPackageNamespace(
'pkg',
[
PythonModuleNamespace(
'module2'
),
PythonModuleNamespace('module2'),
],
),
],
),
],
),
])
],
)
def test_PythonDocumentationGenerator_collect_modules(
python_documentation_generator: PythonDocumentationGenerator,
expected,
):
assert python_documentation_generator.namespace() == expected
@pytest.mark.parametrize('dir', [
@pytest.mark.parametrize(
'dir',
[
d.name
for d in pathlib.Path(
(
@@ -91,7 +94,8 @@ def test_PythonDocumentationGenerator_collect_modules(
'ntationGenerator/test_generate_documentation/'
),
).iterdir()
])
],
)
def test_generate_documentation(dir, expectdir):
with expectdir(
'test_PythonDocumentationGenerator/test_generate_documentation/' + dir
@@ -104,12 +108,12 @@ def test_generate_documentation(dir, expectdir):
'tests/unit/core/python/_core/test_PythonDocume'
'ntationGenerator/test_generate_documentation/'
),
) /
dir /
'project',
)
/ dir
/ 'project',
),
),
pathlib.Path(str(d)),
allow_tables = True,
allow_html = False,
allow_tables=True,
allow_html=False,
).generate_documentation() # simplify the tests
@@ -1,8 +1,10 @@
class UserNotFoundError(Exception):
"""User does not exist."""
pass
class InvalidEmailError(Exception):
"""Email address is invalid."""
pass
@@ -7,6 +7,7 @@ class User:
"""
Represents a user.
"""
id: str
name: str
email: str
@@ -13,23 +13,16 @@ class UserService:
self.storage = storage
def create_user(
self,
user_id: str,
name: str,
email: str,
age: int | None = None
self, user_id: str, name: str, email: str, age: int | None = None
) -> User:
"""
Creates and stores a new user.
"""
if not is_valid_email(email):
raise InvalidEmailError(f"Invalid email: {email}")
raise InvalidEmailError(f'Invalid email: {email}')
user = User(
id=user_id,
name=normalize_name(name),
email=email,
age=age
id=user_id, name=normalize_name(name), email=email, age=age
)
self.storage.add(user)
@@ -5,7 +5,7 @@ def is_valid_email(email: str) -> bool:
"""
Checks whether an email address is valid.
"""
pattern = r"^[\w\.-]+@[\w\.-]+\.\w+$"
pattern = r'^[\w\.-]+@[\w\.-]+\.\w+$'
return re.match(pattern, email) is not None
@@ -13,4 +13,4 @@ def normalize_name(name: str) -> str:
"""
Removes extra spaces and formats the name.
"""
return " ".join(name.strip().split()).title()
return ' '.join(name.strip().split()).title()
@@ -17,6 +17,7 @@ from src.jcloud_docsgen.exceptions import InvalidNamespaceError
import pathlib
import pytest
@pytest.mark.parametrize(
'module_path,src_directory,expected_exception,expected_exception_msg',
[
@@ -216,7 +217,10 @@ def test_PythonModuleDocumentationGenerator_exceptions(
PythonModuleDocumentationGenerator(module_path, src_directory)
assert str(exc_info.value) == expected_exception_msg
@pytest.mark.parametrize('module_documentation_generator,expected', [
@pytest.mark.parametrize(
'module_documentation_generator,expected',
[
(
PythonModuleDocumentationGenerator(
pathlib.Path(
@@ -228,10 +232,10 @@ def test_PythonModuleDocumentationGenerator_exceptions(
pathlib.Path(
'tests/unit/core/python/_core/test_project_dirs/pdir_2/src/'
),
allow_tables = True,
allow_html = False,
allow_tables=True,
allow_html=False,
),
'''# Module `pkg.module`
"""# Module `pkg.module`
A test module.
@@ -398,9 +402,8 @@ Barks.
### Docstring
A dog.'''
A dog.""",
),
(
PythonModuleDocumentationGenerator(
pathlib.Path(
@@ -410,10 +413,10 @@ A dog.'''
),
),
None,
allow_tables = True,
allow_html = False,
allow_tables=True,
allow_html=False,
),
'''# Module `module`
"""# Module `module`
A test module.
@@ -580,9 +583,8 @@ Barks.
### Docstring
A dog.'''
A dog.""",
),
(
PythonModuleDocumentationGenerator(
pathlib.Path(
@@ -592,11 +594,11 @@ A dog.'''
),
),
None,
allow_tables = True,
allow_html = True,
allow_tables=True,
allow_html=True,
),
(
'''# Module `module`
"""# Module `module`
A test module.
@@ -630,8 +632,8 @@ Return type: `int | float`
<details><summary><h3 style="display:inline">Docstring</h3></summary>Adds '''
'''two numbers.
<details><summary><h3 style="display:inline">Docstring</h3></summary>Adds """
"""two numbers.
:param a: The first number.
:type a: int | float
@@ -698,8 +700,8 @@ Return type: `str`
<details><summary><h5 style="display:inline">Docstring</h5></summary>'''
'''Returns a greeting.
<details><summary><h5 style="display:inline">Docstring</h5></summary>"""
"""Returns a greeting.
:return: The greeting.
:rtype: str</details>
@@ -708,8 +710,8 @@ Return type: `str`
<details><summary><h3 style="display:inline">Docstring</h3></summary>A '''
'''class representing an animal.
<details><summary><h3 style="display:inline">Docstring</h3></summary>A """
"""class representing an animal.
:param name: Its name.
:type name: str</details>
@@ -750,18 +752,17 @@ Return type: `None`
<details><summary><h5 style="display:inline">Docstring</h5></summary>Barks.'''
'''</details>
<details><summary><h5 style="display:inline">Docstring</h5></summary>Barks."""
"""</details>
<details><summary><h3 style="display:inline">Docstring</h3></summary>A dog.'''
<details><summary><h3 style="display:inline">Docstring</h3></summary>A dog."""
'</details>'
),
),
(
PythonModuleDocumentationGenerator(
pathlib.Path(
@@ -771,11 +772,11 @@ Return type: `None`
),
),
None,
allow_tables = False,
allow_html = True,
allow_tables=False,
allow_html=True,
),
(
'''# Module `module`
"""# Module `module`
A test module.
@@ -824,8 +825,8 @@ Return type: `int | float`
<details><summary><h3 style="display:inline">Docstring</h3></summary>Adds '''
'''two numbers.
<details><summary><h3 style="display:inline">Docstring</h3></summary>Adds """
"""two numbers.
:param a: The first number.
:type a: int | float
@@ -913,8 +914,8 @@ Return type: `str`
<details><summary><h5 style="display:inline">Docstring</h5></summary>'''
'''Returns a greeting.
<details><summary><h5 style="display:inline">Docstring</h5></summary>"""
"""Returns a greeting.
:return: The greeting.
:rtype: str</details>
@@ -923,8 +924,8 @@ Return type: `str`
<details><summary><h3 style="display:inline">Docstring</h3></summary>A '''
'''class representing an animal.
<details><summary><h3 style="display:inline">Docstring</h3></summary>A """
"""class representing an animal.
:param name: Its name.
:type name: str</details>
@@ -971,18 +972,17 @@ Return type: `None`
<details><summary><h5 style="display:inline">Docstring</h5></summary>Barks.'''
'''</details>
<details><summary><h5 style="display:inline">Docstring</h5></summary>Barks."""
"""</details>
<details><summary><h3 style="display:inline">Docstring</h3></summary>A '''
<details><summary><h3 style="display:inline">Docstring</h3></summary>A """
'dog.</details>'
),
),
(
PythonModuleDocumentationGenerator(
pathlib.Path(
@@ -992,10 +992,10 @@ Return type: `None`
),
),
None,
allow_tables = False,
allow_html = False,
allow_tables=False,
allow_html=False,
),
'''# Module `module`
"""# Module `module`
A test module.
@@ -1204,9 +1204,10 @@ Barks.
### Docstring
A dog.'''
A dog.""",
),
])
],
)
def test_PythonModuleDocumentationGenerator_exceptions(
module_documentation_generator,
expected,
@@ -16,12 +16,16 @@ 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', [
@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
@@ -25,8 +25,9 @@ from src.jcloud_docsgen.core.python.docstrings import PythonDocstring
import pytest
import ast
def module(source_code: str) -> ast.Module:
'''
"""
Returns an AST module of the source code.
:param source_code: The module source code.
@@ -34,11 +35,14 @@ def module(source_code: str) -> ast.Module:
:return: The AST module.
:rtype: ast.Module
'''
"""
return ast.parse(source_code)
@pytest.mark.parametrize('module,expected', [
@pytest.mark.parametrize(
'module,expected',
[
(
module('def add(a, b):\n\treturn a + b'),
[
@@ -169,7 +173,7 @@ class Something:
],
),
(
module('''from src.jcloud_docsgen.core.python import (
module("""from src.jcloud_docsgen.core.python import (
PythonDocumentationGenerator
)
from src.jcloud_docsgen.core.python.namespaces import (
@@ -235,7 +239,7 @@ def test_PythonDocumentationGenerator_collect_modules(
python_documentation_generator: PythonDocumentationGenerator,
expected
):
assert python_documentation_generator.namespace() == expected'''),
assert python_documentation_generator.namespace() == expected"""),
[
PythonFunctionDefinition(
'test_PythonDocumentationGenerator_type_exceptions',
@@ -251,18 +255,18 @@ def test_PythonDocumentationGenerator_collect_modules(
PythonArgumentKind.NORMAL,
None,
None,
)
),
],
None,
None,
[
(
'''pytest.mark.parametrize('project_directo'''
'''ry,docs_directory', [(1, 1), (1, None), '''
'''(42, None), (pathlib.Path('tests'), 1), '''
'''(pathlib.Path('tests/'), 1), (1, pathlib'''
'''.Path('tests/')), (1, pathlib.Path('test'''
'''s')), (None, pathlib.Path('tests'))])'''
"""pytest.mark.parametrize('project_directo"""
"""ry,docs_directory', [(1, 1), (1, None), """
"""(42, None), (pathlib.Path('tests'), 1), """
"""(pathlib.Path('tests/'), 1), (1, pathlib"""
""".Path('tests/')), (1, pathlib.Path('test"""
"""s')), (None, pathlib.Path('tests'))])"""
),
],
),
@@ -280,40 +284,42 @@ def test_PythonDocumentationGenerator_collect_modules(
PythonArgumentKind.NORMAL,
None,
None,
)
),
],
None,
None,
[(
'''pytest.mark.parametrize('python_documentation_gener'''
'''ator,expected', [(PythonDocumentationGenerator(path'''
'''lib.Path('tests/unit/core/python/_core/test_project'''
'''_dirs/pdir_1'), pathlib.Path('tests/unit/core/pytho'''
'''n/_core/test_project_dirs/pdir_1/docs')), []), (Pyt'''
'''honDocumentationGenerator(pathlib.Path('tests/unit/'''
'''core/python/_core/test_project_dirs/pdir_2'), pathl'''
'''ib.Path('tests/unit/core/python/_core/test_project_'''
'''dirs/pdir_2/docs')), [PythonPackageNamespace('pkg','''
''' [PythonModuleNamespace('module')])]), (PythonDocum'''
'''entationGenerator(pathlib.Path('tests/unit/core/pyt'''
'''hon/_core/test_project_dirs/pdir_3'), pathlib.Path('''
''''tests/unit/core/python/_core/test_project_dirs/pdi'''
'''r_3/docs')), [PythonPackageNamespace('pkg', [Python'''
'''ModuleNamespace(\'module'), PythonPackageNamespace('''
'''\'pkg\', [PythonModuleNamespace(\'module2')])])])])'''
)],
[
(
"""pytest.mark.parametrize('python_documentation_gener"""
"""ator,expected', [(PythonDocumentationGenerator(path"""
"""lib.Path('tests/unit/core/python/_core/test_project"""
"""_dirs/pdir_1'), pathlib.Path('tests/unit/core/pytho"""
"""n/_core/test_project_dirs/pdir_1/docs')), []), (Pyt"""
"""honDocumentationGenerator(pathlib.Path('tests/unit/"""
"""core/python/_core/test_project_dirs/pdir_2'), pathl"""
"""ib.Path('tests/unit/core/python/_core/test_project_"""
"""dirs/pdir_2/docs')), [PythonPackageNamespace('pkg',"""
""" [PythonModuleNamespace('module')])]), (PythonDocum"""
"""entationGenerator(pathlib.Path('tests/unit/core/pyt"""
"""hon/_core/test_project_dirs/pdir_3'), pathlib.Path("""
"""'tests/unit/core/python/_core/test_project_dirs/pdi"""
"""r_3/docs')), [PythonPackageNamespace('pkg', [Python"""
"""ModuleNamespace(\'module'), PythonPackageNamespace("""
"""\'pkg\', [PythonModuleNamespace(\'module2')])])])])"""
)
],
),
],
),
(
module(
'''class C1:
"""class C1:
class C2:
class C3:
def function(self, value: bool, /, arg) -> bool:
def should_be_not_in_the_result(arg: int) -> object:
pass
return not value'''
return not value"""
),
[
PythonClassDefinition(
@@ -354,7 +360,7 @@ def test_PythonDocumentationGenerator_collect_modules(
PythonArgumentKind.NORMAL,
None,
None,
)
),
],
'bool',
None,
@@ -368,6 +374,7 @@ def test_PythonDocumentationGenerator_collect_modules(
),
],
),
])
],
)
def test___collect_definitions(module, expected):
assert list(_collect_definitions(module)) == expected
@@ -15,12 +15,14 @@
from src.jcloud_docsgen.core.python._core import _fit_row_length
import pytest
@pytest.mark.parametrize('expected_length,row,expected', [
@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,)),
@@ -28,28 +30,35 @@ import pytest
(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', [
@pytest.mark.parametrize(
'row',
[
(),
(1,),
(1, 2),
(1, 2, 3),
(1, 2, 3, 456),
])
@pytest.mark.parametrize('expected_length,expected_exception', [
],
)
@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)
@@ -23,10 +23,11 @@ from src.jcloud_docsgen.core.python.arguments import (
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
@@ -34,82 +35,164 @@ def docstring_args_list(
: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', {
@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', {
},
),
(
[
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', {
},
),
(
[
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', {
},
),
(
[
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', {
},
),
(
[
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', {
},
),
(
[
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),
], '', {
},
),
(
[
PythonFunctionArgument(
'a', PythonArgumentKind.NORMAL, '', None
),
],
'',
{
'a': _FunctionParamInfo(None, None),
}),
([
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', 'str'),
], '', {
},
),
(
[
PythonFunctionArgument(
'a', PythonArgumentKind.NORMAL, '', 'str'
),
],
'',
{
'a': _FunctionParamInfo('str', None),
}),
([
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', 'str'),
], ':param a:\n:type a: int', {
},
),
(
[
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', {
},
),
(
[
PythonFunctionArgument(
'a', PythonArgumentKind.NORMAL, '', None
),
],
':param a:\n:type a: int',
{
'a': _FunctionParamInfo('int', ''),
}),
([
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', None),
], ':param a: a', {
},
),
(
[
PythonFunctionArgument(
'a', PythonArgumentKind.NORMAL, '', None
),
],
':param a: a',
{
'a': _FunctionParamInfo(None, 'a'),
}),
([
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', 'str'),
], ':param a: 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
assert (
_function_params_info(arg_list, docstring_args_list(docstring))
== expected
)
@@ -15,6 +15,7 @@
from src.jcloud_docsgen.core.python._core import _markdown_table
import pytest
@pytest.mark.parametrize(
(
'header,body,allow_markdown_table,force_row_le'
@@ -55,9 +56,9 @@ import pytest
False,
True,
'--',
'''| a |
"""| a |
| --- |
| 1 |''',
| 1 |""",
),
(
['a'],
@@ -66,9 +67,9 @@ import pytest
False,
True,
'--',
'''| a |
"""| a |
| --- |
| 1 |''',
| 1 |""",
),
(
['a', 'b'],
@@ -77,9 +78,9 @@ import pytest
False,
True,
'--',
'''| a | b |
"""| a | b |
| --- | --- |
| 1 | 2 |''',
| 1 | 2 |""",
),
(
['a', 'b'],
@@ -88,9 +89,9 @@ import pytest
False,
True,
'--',
'''| a | b |
"""| a | b |
| --- | --- |
| 1 | -- |''',
| 1 | -- |""",
),
(
['a', 'b'],
@@ -99,9 +100,9 @@ import pytest
False,
True,
'--',
'''| a | b |
"""| a | b |
| --- | --- |
| -- | -- |''',
| -- | -- |""",
),
(
['a', 'b'],
@@ -110,10 +111,10 @@ import pytest
False,
True,
'--',
'''| a | b |
"""| a | b |
| --- | --- |
| 1 | 2 |
| 3 | 4 |''',
| 3 | 4 |""",
),
(
['a'],
@@ -122,9 +123,9 @@ import pytest
False,
True,
'--',
'''| a |
"""| a |
| --- |
| line1 line2 |''',
| line1 line2 |""",
),
(
['a'],
@@ -133,11 +134,10 @@ import pytest
False,
False,
'--',
'''| a |
"""| a |
| --- |
| line1\nline2 |''',
| line1\nline2 |""",
),
(
[],
[],
@@ -190,9 +190,9 @@ import pytest
False,
True,
'--',
'''- a: 1
"""- a: 1
b: 2''',
b: 2""",
),
(
['a', 'b'],
@@ -201,9 +201,9 @@ import pytest
False,
True,
'--',
'''- a: 1
"""- a: 1
b: --''',
b: --""",
),
(
['a', 'b'],
@@ -212,9 +212,9 @@ import pytest
False,
True,
'--',
'''- a: --
"""- a: --
b: --''',
b: --""",
),
(
['a', 'b'],
@@ -223,13 +223,13 @@ import pytest
False,
True,
'--',
'''- a: 1
"""- a: 1
b: 2
- a: 3
b: 4''',
b: 4""",
),
(
['a'],
@@ -260,14 +260,18 @@ def test__markdown_table(
missing_value,
expected,
):
assert _markdown_table(
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
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(
(
@@ -335,7 +339,6 @@ def test__markdown_table(
ValueError,
'expected 2 values, got 3',
),
(
['a'],
[(1, 2)],
@@ -412,9 +415,9 @@ def test__markdown_table_exceptions(
_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,
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
@@ -19,24 +19,34 @@ from src.jcloud_docsgen.core.python.arguments import (
)
import pytest
@pytest.mark.parametrize('argument_list,expected', [
([
@pytest.mark.parametrize(
'argument_list,expected',
[
(
[
PythonFunctionArgument(
'a',
PythonArgumentKind.NORMAL,
None,
None,
),
], ['a']),
([
],
['a'],
),
(
[
PythonFunctionArgument(
'a',
PythonArgumentKind.POSITIONAL_ONLY,
None,
None,
),
], ['a', '/']),
([
],
['a', '/'],
),
(
[
PythonFunctionArgument(
'a',
PythonArgumentKind.POSITIONAL_ONLY,
@@ -49,8 +59,11 @@ import pytest
None,
None,
),
], ['a', '/', 'b']),
([
],
['a', '/', 'b'],
),
(
[
PythonFunctionArgument(
'a',
PythonArgumentKind.POSITIONAL_ONLY,
@@ -69,8 +82,11 @@ import pytest
None,
None,
),
], ['a', '/', 'b', '*', 'c']),
([
],
['a', '/', 'b', '*', 'c'],
),
(
[
PythonFunctionArgument(
'b',
PythonArgumentKind.NORMAL,
@@ -83,16 +99,22 @@ import pytest
None,
None,
),
], ['b', '*', 'c']),
([
],
['b', '*', 'c'],
),
(
[
PythonFunctionArgument(
'c',
PythonArgumentKind.KEYWORD_ONLY,
None,
None,
),
], ['*', 'c']),
([
],
['*', 'c'],
),
(
[
PythonFunctionArgument(
'b',
PythonArgumentKind.POSITIONAL_ONLY,
@@ -105,16 +127,22 @@ import pytest
None,
None,
),
], ['b', '/', '*', 'c']),
([
],
['b', '/', '*', 'c'],
),
(
[
PythonFunctionArgument(
'args',
PythonArgumentKind.VARARG,
None,
None,
)
], ['*args']),
([
],
['*args'],
),
(
[
PythonFunctionArgument(
'a',
PythonArgumentKind.NORMAL,
@@ -127,10 +155,14 @@ import pytest
None,
None,
),
], ['a', '*args']),
([
],
['a', '*args'],
),
(
[
PythonFunctionArgument(
'a', PythonArgumentKind.POSITIONAL_ONLY,
'a',
PythonArgumentKind.POSITIONAL_ONLY,
None,
None,
),
@@ -140,8 +172,11 @@ import pytest
None,
None,
),
], ['a', '/', '*args']),
([
],
['a', '/', '*args'],
),
(
[
PythonFunctionArgument(
'args',
PythonArgumentKind.VARARG,
@@ -154,8 +189,11 @@ import pytest
None,
None,
),
], ['*args', 'a']),
([
],
['*args', 'a'],
),
(
[
PythonFunctionArgument(
'args',
PythonArgumentKind.VARARG,
@@ -168,8 +206,11 @@ import pytest
None,
None,
),
], ['*args', '**kwargs']),
([
],
['*args', '**kwargs'],
),
(
[
PythonFunctionArgument(
'a',
PythonArgumentKind.POSITIONAL_ONLY,
@@ -188,8 +229,11 @@ import pytest
None,
None,
),
], ['a', '/', '*args', '**kwargs']),
([
],
['a', '/', '*args', '**kwargs'],
),
(
[
PythonFunctionArgument(
'a',
PythonArgumentKind.NORMAL,
@@ -208,8 +252,11 @@ import pytest
None,
None,
),
], ['a', '*args', '**kwargs']),
([
],
['a', '*args', '**kwargs'],
),
(
[
PythonFunctionArgument(
'args',
PythonArgumentKind.VARARG,
@@ -228,8 +275,11 @@ import pytest
None,
None,
),
], ['*args', 'a', '**kwargs']),
([
],
['*args', 'a', '**kwargs'],
),
(
[
PythonFunctionArgument(
'a',
PythonArgumentKind.KEYWORD_ONLY,
@@ -242,8 +292,11 @@ import pytest
None,
None,
),
], ['*', 'a', '**kwargs']),
([
],
['*', 'a', '**kwargs'],
),
(
[
PythonFunctionArgument(
'a',
PythonArgumentKind.NORMAL,
@@ -256,8 +309,11 @@ import pytest
None,
None,
),
], ['a', '**kwargs']),
([
],
['a', '**kwargs'],
),
(
[
PythonFunctionArgument(
'a',
PythonArgumentKind.POSITIONAL_ONLY,
@@ -270,15 +326,21 @@ import pytest
None,
None,
),
], ['a', '/', '**kwargs']),
([
],
['a', '/', '**kwargs'],
),
(
[
PythonFunctionArgument(
'a',
PythonArgumentKind.NORMAL,
'1',
'int',
),
], ['a: int = 1']),
])
],
['a: int = 1'],
),
],
)
def test__signature_argument_list(argument_list, expected):
assert _signature_argument_list(argument_list) == expected
@@ -1,9 +1,10 @@
'''
"""
A test module.
'''
"""
def add(a: int | float, b: int | float) -> int | float:
'''
"""
Adds two numbers.
:param a: The first number.
@@ -13,38 +14,41 @@ def add(a: int | float, b: int | float) -> int | float:
:return: The sum.
:rtype: int | float
'''
"""
return a + b
class Animal:
'''
"""
A class representing an animal.
:param name: Its name.
:type name: str
'''
"""
def __init__(self, name: str) -> None:
self.name = name
def greet(self):
'''
"""
Returns a greeting.
:return: The greeting.
:rtype: str
'''
"""
return f'Hi there! My name is {self.name}'
class Dog(Animal):
'''
"""
A dog.
'''
"""
def bark(self) -> None:
'''
"""
Barks.
'''
"""
print('Woof!')
@@ -20,8 +20,9 @@ from src.jcloud_docsgen.core.python.arguments import (
import pytest
import ast
def argument_list(argument_list: str) -> ast.arguments:
'''
"""
Returns an AST argument list from the argument list source code
:param argument_list: The argument list.
@@ -29,11 +30,14 @@ def argument_list(argument_list: str) -> ast.arguments:
:return: The AST argument list.
:rtype: ast.arguments
'''
"""
return ast.parse('def func(' + argument_list + '): ...').body[0].args
@pytest.mark.parametrize('ast_arguments_list,expected', [
@pytest.mark.parametrize(
'ast_arguments_list,expected',
[
(
argument_list('a'),
[
@@ -679,8 +683,9 @@ def argument_list(argument_list: str) -> ast.arguments:
),
(
argument_list(
'a = 1, b = \'a\', /, *args, c = None, d = True, **kwargs',
), [
"a = 1, b = 'a', /, *args, c = None, d = True, **kwargs",
),
[
PythonFunctionArgument(
'a',
PythonArgumentKind.POSITIONAL_ONLY,
@@ -690,7 +695,7 @@ def argument_list(argument_list: str) -> ast.arguments:
PythonFunctionArgument(
'b',
PythonArgumentKind.POSITIONAL_ONLY,
'\'a\'',
"'a'",
None,
),
PythonFunctionArgument(
@@ -720,7 +725,9 @@ def argument_list(argument_list: str) -> ast.arguments:
],
),
(
argument_list('a, b = \'a\', /, *args, c = None, d = True, **kwargs'),
argument_list(
"a, b = 'a', /, *args, c = None, d = True, **kwargs"
),
[
PythonFunctionArgument(
'a',
@@ -731,7 +738,7 @@ def argument_list(argument_list: str) -> ast.arguments:
PythonFunctionArgument(
'b',
PythonArgumentKind.POSITIONAL_ONLY,
'\'a\'',
"'a'",
None,
),
PythonFunctionArgument(
@@ -741,10 +748,7 @@ def argument_list(argument_list: str) -> ast.arguments:
None,
),
PythonFunctionArgument(
'c',
PythonArgumentKind.KEYWORD_ONLY,
'None',
None
'c', PythonArgumentKind.KEYWORD_ONLY, 'None', None
),
PythonFunctionArgument(
'd',
@@ -884,7 +888,8 @@ def argument_list(argument_list: str) -> ast.arguments:
],
),
(
argument_list('a, b , /, *args, c = None, d, **kwargs'), [
argument_list('a, b , /, *args, c = None, d, **kwargs'),
[
PythonFunctionArgument(
'a',
PythonArgumentKind.POSITIONAL_ONLY,
@@ -934,11 +939,13 @@ def argument_list(argument_list: str) -> ast.arguments:
),
],
),
])
],
)
def test_PythonASTArgumentsListParser_to_argument_list(
ast_arguments_list,
expected,
):
assert PythonASTArgumentsListParser(
ast_arguments_list
).to_argument_list() == expected
assert (
PythonASTArgumentsListParser(ast_arguments_list).to_argument_list()
== expected
)
@@ -23,6 +23,7 @@ from src.jcloud_docsgen.exceptions import (
)
import pytest
@pytest.mark.parametrize(
'name,kind,default,annotation,expected_exception,expected_exception_msg',
[
@@ -32,7 +33,7 @@ import pytest
None,
None,
TypeError,
'expected \'str\', got \'NoneType\'',
"expected 'str', got 'NoneType'",
),
(
1,
@@ -40,7 +41,7 @@ import pytest
None,
None,
TypeError,
'expected \'str\', got \'int\'',
"expected 'str', got 'int'",
),
(
42,
@@ -48,7 +49,7 @@ import pytest
None,
None,
TypeError,
'expected \'str\', got \'int\'',
"expected 'str', got 'int'",
),
(
4.2,
@@ -56,7 +57,7 @@ import pytest
None,
None,
TypeError,
'expected \'str\', got \'float\'',
"expected 'str', got 'float'",
),
(
4.2,
@@ -64,7 +65,7 @@ import pytest
None,
None,
TypeError,
'expected \'str\', got \'float\'',
"expected 'str', got 'float'",
),
(
'a',
@@ -72,7 +73,7 @@ import pytest
1,
None,
TypeError,
'expected either \'str\' or \'NoneType\', got \'int\'',
"expected either 'str' or 'NoneType', got 'int'",
),
(
'a',
@@ -80,7 +81,7 @@ import pytest
None,
1,
TypeError,
'expected either \'str\' or \'NoneType\', got \'int\'',
"expected either 'str' or 'NoneType', got 'int'",
),
(
'a',
@@ -88,7 +89,7 @@ import pytest
None,
None,
TypeError,
'expected \'PythonArgumentKind\', got \'int\'',
"expected 'PythonArgumentKind', got 'int'",
),
(
'a',
@@ -384,7 +385,10 @@ def test_PythonFunctionArgument_exceptions(
PythonFunctionArgument(name, kind, default, annotation)
assert str(exc_info.value) == expected_exception_msg
@pytest.mark.parametrize('name', [
@pytest.mark.parametrize(
'name',
[
'a',
'_a',
'a1',
@@ -401,28 +405,41 @@ def test_PythonFunctionArgument_exceptions(
'none',
'true',
'false',
])
@pytest.mark.parametrize('kind', [
],
)
@pytest.mark.parametrize(
'kind',
[
PythonArgumentKind.NORMAL,
PythonArgumentKind.POSITIONAL_ONLY,
PythonArgumentKind.KEYWORD_ONLY,
])
@pytest.mark.parametrize('default', [
],
)
@pytest.mark.parametrize(
'default',
[
None,
'x',
'1',
None,
])
@pytest.mark.parametrize('annotation', [
],
)
@pytest.mark.parametrize(
'annotation',
[
None,
'a',
'list[int]',
'_',
])
],
)
def test_PythonFunctionArgument(name, kind, default, annotation):
PythonFunctionArgument(name, kind, default, annotation)
@pytest.mark.parametrize('name', [
@pytest.mark.parametrize(
'name',
[
'a',
'_a',
'a1',
@@ -439,21 +456,31 @@ def test_PythonFunctionArgument(name, kind, default, annotation):
'none',
'true',
'false',
])
@pytest.mark.parametrize('kind', [
],
)
@pytest.mark.parametrize(
'kind',
[
PythonArgumentKind.VARARG,
PythonArgumentKind.KWARGS,
])
@pytest.mark.parametrize('annotation', [
],
)
@pytest.mark.parametrize(
'annotation',
[
None,
'a',
'list[int]',
'_',
])
],
)
def test_PythonFunctionArgument_asterik(name, kind, annotation):
PythonFunctionArgument(name, kind, None, annotation)
@pytest.mark.parametrize('argument,expected', [
@pytest.mark.parametrize(
'argument,expected',
[
(
PythonFunctionArgument(
'a',
@@ -474,10 +501,7 @@ def test_PythonFunctionArgument_asterik(name, kind, annotation):
),
(
PythonFunctionArgument(
'a',
PythonArgumentKind.NORMAL,
None,
'int'
'a', PythonArgumentKind.NORMAL, None, 'int'
),
'a: int',
),
@@ -598,6 +622,7 @@ def test_PythonFunctionArgument_asterik(name, kind, annotation):
),
'**a: int',
),
])
],
)
def test_PythonFunctionArgument_signature_repr(argument, expected):
assert argument.signature_repr() == expected
@@ -25,7 +25,10 @@ from src.jcloud_docsgen.exceptions import InvalidPythonIdentifierError
from tests.utils.ast_node import ast_node
import pytest
@pytest.mark.parametrize('definition,expected', [
@pytest.mark.parametrize(
'definition,expected',
[
(
PythonClassDefinition(
'A',
@@ -34,7 +37,7 @@ import pytest
None,
[],
),
'PythonClassDefinition(\'A\', [], None, None, [])',
"PythonClassDefinition('A', [], None, None, [])",
),
(
PythonClassDefinition(
@@ -45,11 +48,12 @@ import pytest
[],
),
(
'PythonClassDefinition(\'A\', [], None, '
'PythonDocstring(\'Hello, World!\'), [])'
"PythonClassDefinition('A', [], None, "
"PythonDocstring('Hello, World!'), [])"
),
),
])
],
)
def test_PythonDefinition_string_representation(definition, expected):
assert repr(definition) == expected
@@ -364,7 +368,9 @@ def test_PythonFunctionDefinition_exceptions(
assert str(exc_info.value) == expected_exception_message
@pytest.mark.parametrize('node,expected', [
@pytest.mark.parametrize(
'node,expected',
[
(
ast_node(
(
@@ -428,7 +434,7 @@ def test_PythonFunctionDefinition_exceptions(
),
),
(
ast_node('class Class: \'\'\'docstring\'\'\''),
ast_node("class Class: '''docstring'''"),
PythonClassDefinition(
'Class',
[],
@@ -463,7 +469,7 @@ def test_PythonFunctionDefinition_exceptions(
),
),
(
ast_node('class Class:\n\t\'\'\'doc\'\'\'\n\tdef __init__(self): ...'),
ast_node("class Class:\n\t'''doc'''\n\tdef __init__(self): ..."),
PythonClassDefinition(
'Class',
[],
@@ -487,6 +493,7 @@ def test_PythonFunctionDefinition_exceptions(
],
),
),
])
],
)
def test_PythonDefinition_from_node(node, expected):
assert PythonClassDefinition.from_node(node) == expected
@@ -17,14 +17,17 @@ from src.jcloud_docsgen.core.python.docstrings import PythonDocstring
from src.jcloud_docsgen.exceptions import InvalidPythonIdentifierError
import pytest
@pytest.mark.parametrize('definition,expected', [
@pytest.mark.parametrize(
'definition,expected',
[
(
PythonDefinition(
'a',
None,
[],
),
'PythonDefinition(\'a\', None, [])',
"PythonDefinition('a', None, [])",
),
(
PythonDefinition(
@@ -32,9 +35,10 @@ import pytest
PythonDocstring('Hello, World!'),
[],
),
'PythonDefinition(\'a\', PythonDocstring(\'Hello, World!\'), [])',
"PythonDefinition('a', PythonDocstring('Hello, World!'), [])",
),
])
],
)
def test_PythonDefinition_string_representation(definition, expected):
assert repr(definition) == expected
@@ -280,7 +284,9 @@ def test_PythonDefinition_exceptions(
assert str(exc_info.value) == expected_exception_message
@pytest.mark.parametrize('definition1,definition2,expected', [
@pytest.mark.parametrize(
'definition1,definition2,expected',
[
(
PythonDefinition(
'a',
@@ -437,7 +443,8 @@ def test_PythonDefinition_exceptions(
),
False,
),
])
],
)
def test_PythonDefinition___eq__(definition1, definition2, expected):
assert (definition1 == definition2) == expected
assert (definition2 == definition1) == expected
@@ -22,7 +22,10 @@ from src.jcloud_docsgen.exceptions import InvalidPythonIdentifierError
from tests.utils.ast_node import ast_node
import pytest
@pytest.mark.parametrize('definition,expected', [
@pytest.mark.parametrize(
'definition,expected',
[
(
PythonFunctionDefinition(
'a',
@@ -31,7 +34,7 @@ import pytest
None,
[],
),
'PythonFunctionDefinition(\'a\', [], None, None, [])',
"PythonFunctionDefinition('a', [], None, None, [])",
),
(
PythonFunctionDefinition(
@@ -42,11 +45,12 @@ import pytest
[],
),
(
'PythonFunctionDefinition(\'a\', [], None,'
' PythonDocstring(\'Hello, World!\'), [])'
"PythonFunctionDefinition('a', [], None,"
" PythonDocstring('Hello, World!'), [])"
),
),
])
],
)
def test_PythonDefinition_string_representation(definition, expected):
assert repr(definition) == expected
@@ -345,7 +349,8 @@ def test_PythonDefinition_string_representation(definition, expected):
InvalidPythonIdentifierError,
'invalid identifier: True',
),
])
],
)
def test_PythonFunctionDefinition_exceptions(
name,
args,
@@ -360,11 +365,14 @@ def test_PythonFunctionDefinition_exceptions(
assert str(exc_info.value) == expected_exception_message
@pytest.mark.parametrize('node,expected', [
@pytest.mark.parametrize(
'node,expected',
[
(
ast_node('def add(a: int, b: int) -> int: return a + b'),
PythonFunctionDefinition(
'add', [
'add',
[
PythonFunctionArgument(
'a',
PythonArgumentKind.NORMAL,
@@ -384,7 +392,9 @@ def test_PythonFunctionDefinition_exceptions(
),
),
(
ast_node('@decorator\ndef add(a: int, b: int) -> int: return a + b'),
ast_node(
'@decorator\ndef add(a: int, b: int) -> int: return a + b'
),
PythonFunctionDefinition(
'add',
[
@@ -454,6 +464,7 @@ def test_PythonFunctionDefinition_exceptions(
['decorator'],
),
),
])
],
)
def test_PythonDefinition_from_node(node, expected):
assert PythonFunctionDefinition.from_node(node) == expected
@@ -15,17 +15,25 @@
from src.jcloud_docsgen.core.python.namespaces import PythonModuleNamespace
import pytest
class StrSubclass(str): ...
@pytest.mark.parametrize('namespace,expected_name', [
@pytest.mark.parametrize(
'namespace,expected_name',
[
(PythonModuleNamespace('a'), 'a'),
(PythonModuleNamespace('1'), '1'),
(PythonModuleNamespace(StrSubclass('a')), StrSubclass('a')),
])
],
)
def test_PythonModuleNamespace_name_attribute(namespace, expected_name):
assert namespace.name == expected_name
@pytest.mark.parametrize('namespace1,namespace2,expected', [
@pytest.mark.parametrize(
'namespace1,namespace2,expected',
[
(PythonModuleNamespace('a'), PythonModuleNamespace('a'), True),
(PythonModuleNamespace('b'), PythonModuleNamespace('b'), True),
(PythonModuleNamespace('1'), PythonModuleNamespace('1'), True),
@@ -33,15 +41,23 @@ def test_PythonModuleNamespace_name_attribute(namespace, expected_name):
(PythonModuleNamespace('a'), PythonModuleNamespace('b'), False),
(PythonModuleNamespace('0'), PythonModuleNamespace('-0'), False),
(PythonModuleNamespace('1'), PythonModuleNamespace('a'), False),
])
],
)
def test_PythonModuleNamespace___eq__(namespace1, namespace2, expected):
assert (namespace1 == namespace2) == expected
assert (namespace2 == namespace1) == expected
@pytest.mark.parametrize('namespace,expected', [
(PythonModuleNamespace('a'), 'PythonModuleNamespace(\'a\')'),
(PythonModuleNamespace('1'), 'PythonModuleNamespace(\'1\')'),
(PythonModuleNamespace(StrSubclass('a')), 'PythonModuleNamespace(\'a\')'),
])
@pytest.mark.parametrize(
'namespace,expected',
[
(PythonModuleNamespace('a'), "PythonModuleNamespace('a')"),
(PythonModuleNamespace('1'), "PythonModuleNamespace('1')"),
(
PythonModuleNamespace(StrSubclass('a')),
"PythonModuleNamespace('a')",
),
],
)
def test_PythonModuleNamespace_string_representation(namespace, expected):
assert repr(namespace) == expected
@@ -15,20 +15,29 @@
from src.jcloud_docsgen.core.python.namespaces import PythonNamespace
import pytest
class StrSubclass(str): ...
@pytest.mark.parametrize('namespace,expected_name', [
@pytest.mark.parametrize(
'namespace,expected_name',
[
(PythonNamespace('a'), 'a'),
(PythonNamespace('1'), '1'),
(PythonNamespace(StrSubclass('a')), StrSubclass('a')),
])
],
)
def test_PythonNamespace_name_attribute(namespace, expected_name):
assert namespace.name == expected_name
@pytest.mark.parametrize('namespace,expected', [
(PythonNamespace('a'), 'PythonNamespace(\'a\')'),
(PythonNamespace('1'), 'PythonNamespace(\'1\')'),
(PythonNamespace(StrSubclass('a')), 'PythonNamespace(\'a\')'),
])
@pytest.mark.parametrize(
'namespace,expected',
[
(PythonNamespace('a'), "PythonNamespace('a')"),
(PythonNamespace('1'), "PythonNamespace('1')"),
(PythonNamespace(StrSubclass('a')), "PythonNamespace('a')"),
],
)
def test_PythonNamespace_string_representation(namespace, expected):
assert repr(namespace) == expected
@@ -22,9 +22,13 @@ from src.jcloud_docsgen.exceptions import (
InvalidNamespaceError,
)
class StrSubclass(str): ...
@pytest.mark.parametrize('namespace,expected_name', [
@pytest.mark.parametrize(
'namespace,expected_name',
[
(
PythonPackageNamespace(
'a',
@@ -46,14 +50,19 @@ class StrSubclass(str): ...
),
StrSubclass('a'),
),
])
],
)
def test_PythonPackageNamespace_name_attribute(namespace, expected_name):
assert namespace.name == expected_name
namespace_b = PythonModuleNamespace('b')
namespace_a = PythonPackageNamespace('a', [
namespace_a = PythonPackageNamespace(
'a',
[
namespace_b,
])
],
)
namespace_c = PythonModuleNamespace('c')
namespace_k = PythonModuleNamespace('k')
namespace_j = PythonPackageNamespace('j', [namespace_k])
@@ -64,7 +73,10 @@ namespace_f = PythonPackageNamespace('f', [namespace_g])
namespace_e = PythonPackageNamespace('e', [namespace_f])
namespace_d = PythonPackageNamespace('d', [namespace_e])
@pytest.mark.parametrize('namespace,sub_namespaces,expected', [
@pytest.mark.parametrize(
'namespace,sub_namespaces,expected',
[
(
PythonPackageNamespace(
'name',
@@ -104,7 +116,8 @@ namespace_d = PythonPackageNamespace('d', [namespace_e])
['e', 'f', 'g', 'h', 'i', 'j', 'k'],
namespace_k,
),
])
],
)
def test_PythonPackageNamespace_namespace(
namespace: PythonPackageNamespace,
sub_namespaces,
@@ -112,6 +125,7 @@ def test_PythonPackageNamespace_namespace(
):
assert namespace.namespace(sub_namespaces) == expected
@pytest.mark.parametrize(
'namespace,sub_namespaces,expected_exception,expected_exception_msg',
[
@@ -157,7 +171,10 @@ def test_PythonPackageNamespace_namespace_exceptions(
namespace.namespace(sub_namespaces)
assert str(exc_info.value) == expected_exception_msg
@pytest.mark.parametrize('namespace,expected', [
@pytest.mark.parametrize(
'namespace,expected',
[
(
PythonPackageNamespace(
'a',
@@ -185,11 +202,15 @@ def test_PythonPackageNamespace_namespace_exceptions(
PythonPackageNamespace('a', []),
{},
),
])
],
)
def test_PythonPackageNamespace_sub_namespace_names(namespace, expected):
assert namespace.sub_namespace_names() == expected
@pytest.mark.parametrize('namespace1,namespace2,expected', [
@pytest.mark.parametrize(
'namespace1,namespace2,expected',
[
(
PythonPackageNamespace(
'a',
@@ -234,43 +255,51 @@ def test_PythonPackageNamespace_sub_namespace_names(namespace, expected):
PythonPackageNamespace('b', []),
False,
),
])
],
)
def test_PythonPackageNamespace___eq__(namespace1, namespace2, expected):
assert (namespace1 == namespace2) == expected
assert (namespace2 == namespace1) == expected
@pytest.mark.parametrize('namespace,expected', [
@pytest.mark.parametrize(
'namespace,expected',
[
(
PythonPackageNamespace('a', []),
'PythonPackageNamespace(\'a\', [])',
"PythonPackageNamespace('a', [])",
),
(
PythonPackageNamespace('1', []),
'PythonPackageNamespace(\'1\', [])',
"PythonPackageNamespace('1', [])",
),
(
PythonPackageNamespace(StrSubclass('a'), []),
'PythonPackageNamespace(\'a\', [])',
"PythonPackageNamespace('a', [])",
),
(
namespace_a,
'PythonPackageNamespace(\'a\', [PythonModuleNamespace(\'b\')])',
"PythonPackageNamespace('a', [PythonModuleNamespace('b')])",
),
(
namespace_d,
(
'PythonPackageNamespace(\'d\', [PythonPackageNamespac'
'e(\'e\', [PythonPackageNamespace(\'f\', [PythonPacka'
'geNamespace(\'g\', [PythonPackageNamespace(\'h\', [P'
'ythonPackageNamespace(\'i\', [PythonPackageNamespace'
'(\'j\', [PythonModuleNamespace(\'k\')])])])])])])])'
"PythonPackageNamespace('d', [PythonPackageNamespac"
"e('e', [PythonPackageNamespace('f', [PythonPacka"
"geNamespace('g', [PythonPackageNamespace('h', [P"
"ythonPackageNamespace('i', [PythonPackageNamespace"
"('j', [PythonModuleNamespace('k')])])])])])])])"
),
),
])
],
)
def test_PythonPackageNamespace_string_representation(namespace, expected):
assert repr(namespace) == expected
@pytest.mark.parametrize('namespace,new_namespace,expected', [
@pytest.mark.parametrize(
'namespace,new_namespace,expected',
[
(
PythonPackageNamespace('a', []),
PythonModuleNamespace('b'),
@@ -279,7 +308,8 @@ def test_PythonPackageNamespace_string_representation(namespace, expected):
[PythonModuleNamespace('b')],
),
),
])
],
)
def test_PythonPackageNamespace___getitem__(
namespace,
new_namespace,
@@ -16,47 +16,58 @@ from src.jcloud_docsgen.utils import assert_that_is_instance
import pytest
import types
class TestType: ...
TestType.__name__ = 'NOT TestType'
@pytest.mark.parametrize('obj,class_or_tuple', [
@pytest.mark.parametrize(
'obj,class_or_tuple',
[
(1, int),
(1, (int,)),
(1, str | int),
(1, int | str | list),
(TestType(), TestType),
(None, types.NoneType),
])
],
)
def test_assert_that_is_instance(obj, class_or_tuple):
assert_that_is_instance(obj, class_or_tuple)
@pytest.mark.parametrize('obj,class_or_tuple,expected_exception_msg', [
@pytest.mark.parametrize(
'obj,class_or_tuple,expected_exception_msg',
[
(
1,
str,
'expected \'str\', got \'int\'',
"expected 'str', got 'int'",
),
(
1,
(str,),
'expected \'str\', got \'int\'',
"expected 'str', got 'int'",
),
(
1,
str | float,
'expected either \'str\' or \'float\', got \'int\'',
"expected either 'str' or 'float', got 'int'",
),
(
1,
str | float | list,
'expected either \'str\', \'float\' or \'list\', got \'int\'',
"expected either 'str', 'float' or 'list', got 'int'",
),
(
1,
TestType,
'expected \'NOT TestType\', got \'int\'',
"expected 'NOT TestType', got 'int'",
),
])
],
)
def test_assert_that_is_instance_exceptions(
obj,
class_or_tuple,
@@ -16,7 +16,10 @@ from src.jcloud_docsgen.utils import get_relative_path_segments
import pytest
import pathlib
@pytest.mark.parametrize('path,root,expected', [
@pytest.mark.parametrize(
'path,root,expected',
[
(pathlib.Path('/'), pathlib.Path('/'), []),
(pathlib.Path('/a'), pathlib.Path('/'), ['a']),
(pathlib.Path('/a/'), pathlib.Path('/'), ['a']),
@@ -34,7 +37,6 @@ import pathlib
(pathlib.Path('/a/b/'), pathlib.Path('/a/b'), []),
(pathlib.Path('/a/b'), pathlib.Path('/a/b/'), []),
(pathlib.Path('/a/b/'), pathlib.Path('/a/b/'), []),
(pathlib.Path('./'), pathlib.Path('./'), []),
(pathlib.Path('./a'), pathlib.Path('./'), ['a']),
(pathlib.Path('./a/'), pathlib.Path('./'), ['a']),
@@ -52,7 +54,6 @@ import pathlib
(pathlib.Path('./a/b/'), pathlib.Path('./a/b'), []),
(pathlib.Path('./a/b'), pathlib.Path('./a/b/'), []),
(pathlib.Path('./a/b/'), pathlib.Path('./a/b/'), []),
(pathlib.Path(''), pathlib.Path(''), []),
(pathlib.Path('a'), pathlib.Path(''), ['a']),
(pathlib.Path('a/'), pathlib.Path(''), ['a']),
@@ -70,7 +71,7 @@ import pathlib
(pathlib.Path('a/b/'), pathlib.Path('a/b'), []),
(pathlib.Path('a/b'), pathlib.Path('a/b/'), []),
(pathlib.Path('a/b/'), pathlib.Path('a/b/'), []),
])
],
)
def test_get_relative_path_segments(path, root, expected):
assert get_relative_path_segments(path, root) == expected
+20 -16
View File
@@ -15,10 +15,13 @@
from src.jcloud_docsgen.utils import human_readable_list
import pytest
@pytest.mark.parametrize('ls,final_separator,quotation_mark,expected', [
@pytest.mark.parametrize(
'ls,final_separator,quotation_mark,expected',
[
([], '', '', ''),
([], 'and', '', ''),
([], 'and', '\'', ''),
([], 'and', "'", ''),
([1], '', '', '1'),
([-1], '', '', '-1'),
([0], '', '', '0'),
@@ -29,31 +32,32 @@ import pytest
(['a'], '', '', 'a'),
([1], 'and', '', '1'),
(['a'], 'and', '', 'a'),
([1], 'and', '\'', '\'1\''),
(['a'], 'and', '\'', '\'a\''),
([1], 'and', "'", "'1'"),
(['a'], 'and', "'", "'a'"),
([1, 2], 'and', '', '1 and 2'),
([1, 2], 'and', '\'', '\'1\' and \'2\''),
([1, 2], 'and', "'", "'1' and '2'"),
([1, 'a'], 'and', '', '1 and a'),
([1, 'a'], 'and', '\'', '\'1\' and \'a\''),
([1, 'a'], 'and', "'", "'1' and 'a'"),
([1, 'a'], 'and', '', '1 and a'),
([1, 'a'], 'and', '\'', '\'1\' and \'a\''),
([1, 'a'], 'and', '\'', '\'1\' and \'a\''),
([1, 'a'], 'and', "'", "'1' and 'a'"),
([1, 'a'], 'and', "'", "'1' and 'a'"),
([1, 'a'], '', '', '1 a'),
([1, 'a'], '', '\'', '\'1\' \'a\''),
([1, 'a'], '', "'", "'1' 'a'"),
([1, 'a'], '', '', '1 a'),
([1, 'a'], '', '\'', '\'1\' \'a\''),
([1, 'a'], '', '\'', '\'1\' \'a\''),
([1, 'a'], '', "'", "'1' 'a'"),
([1, 'a'], '', "'", "'1' 'a'"),
([1, 2, 3], 'and', '', '1, 2 and 3'),
([1, 2, 3], '', '', '1, 2 3'),
([1, 2, 3], 'and', '\'', '\'1\', \'2\' and \'3\''),
([1, 2, 3], '', '\'', '\'1\', \'2\' \'3\''),
([1, 2, 3], 'and', "'", "'1', '2' and '3'"),
([1, 2, 3], '', "'", "'1', '2' '3'"),
([1, 2, 3, 42, 'a', 23], 'and', '', '1, 2, 3, 42, a and 23'),
(
[1, 2, 3, 42, 'a', 23],
'and',
'\'',
'\'1\', \'2\', \'3\', \'42\', \'a\' and \'23\'',
"'",
"'1', '2', '3', '42', 'a' and '23'",
),
])
],
)
def test_human_readable_list(ls, final_separator, quotation_mark, expected):
assert human_readable_list(ls, final_separator, quotation_mark) == expected
+6 -2
View File
@@ -15,7 +15,10 @@
from src.jcloud_docsgen.utils import non_empty_str
import pytest
@pytest.mark.parametrize('value,expected', [
@pytest.mark.parametrize(
'value,expected',
[
(None, None),
('', None),
(' ', None),
@@ -25,6 +28,7 @@ import pytest
(' a', ' a'),
('a ', 'a '),
('a', 'a'),
])
],
)
def test_non_empty_str(value, expected):
assert non_empty_str(value) == expected
+4 -5
View File
@@ -15,12 +15,11 @@
from typing import Union
import ast
__all__ = [
'ast_node'
]
__all__ = ['ast_node']
def ast_node(statement: str) -> Union[ast.stmt, None]:
'''
"""
Returns an AST node from the statement.
:param statement: The statement.
@@ -28,7 +27,7 @@ def ast_node(statement: str) -> Union[ast.stmt, None]:
:return: The node.
:rtype: Union[ast.stmt, None]
'''
"""
body = ast.parse(statement).body
if not body: