Chore(lint): set up Ruff, lint and format #22

Merged
jakob.scheid merged 4 commits from chore/ruff into main 2026-09-21 22:06:22 +02:00
46 changed files with 6275 additions and 5808 deletions

No files matched your search

Generated
+30 -2
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. # This file is automatically @generated by Poetry 2.4.3 and should not be changed by hand.
[[package]] [[package]]
name = "colorama" name = "colorama"
@@ -143,6 +143,34 @@ files = [
[package.dependencies] [package.dependencies]
pytest = ">=5.0" pytest = ">=5.0"
[[package]]
name = "ruff"
version = "0.16.8"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "ruff-0.16.8-py3-none-linux_armv6l.whl", hash = "sha256:6ffbd6d87383c1edf5f6fa890f10200950240d7c1a16052a19a09d3a2307dd38"},
{file = "ruff-0.16.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:42ed6b878ed61e3acca92f2730a17acff39286944ea82398544696366a6f925e"},
{file = "ruff-0.16.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ea781c7f2afba8c6a505ea0fb3f994020249e0c450635f5381286fea6b46170"},
{file = "ruff-0.16.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8efeae3bbe414a5efefda11a792dfb51ef90ac48d50c4830de2f644caf3e8659"},
{file = "ruff-0.16.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a79b795469fef7fc6e908b218eed2eb17332afd85031db6480dc864560e69b2"},
{file = "ruff-0.16.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fdc5563cdc50555e6fba39322850860e9267c1b3d12c26a74729d8604c3c812"},
{file = "ruff-0.16.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34508983c70665578dab88f5223d8e6228307e1135398ca8bfc8b7e9501e282b"},
{file = "ruff-0.16.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:644bb578569e0ffc575741232bd385dacdd6fbe123f1a729e7a225f54aa3957f"},
{file = "ruff-0.16.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15e7d226246961db9235098333caa13063906d3851136b84c2900b82f5daa1df"},
{file = "ruff-0.16.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a2bf6bc3e9ebdd4449abc6f06cf64b98051a2c61cf94d2fe9596518c881f1a1e"},
{file = "ruff-0.16.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ca111ba0849539165e9e59d2b442542f3c1e8060ebbdea82494f1ffbccb1e1f"},
{file = "ruff-0.16.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:359a1e5b495448ee1e91018064382ebc86f90e8aac2fed222c7d0e4e8df85fd2"},
{file = "ruff-0.16.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:59e8f5681349474110b24d62e93cfda6593f5fa3473446ca3705200cac1a08b9"},
{file = "ruff-0.16.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:efa3e7a16d1baaa79957888dfdf8be9ef2e44db81cb032af06d76632ab59e773"},
{file = "ruff-0.16.8-py3-none-win32.whl", hash = "sha256:55793ba85c69921e89be061426d91a78652d6e50317c962240922747a4eb713f"},
{file = "ruff-0.16.8-py3-none-win_amd64.whl", hash = "sha256:a6b85621fd3c81e31fc5f5add09c9c078b430db3595ca632efafdec9e64ebfaa"},
{file = "ruff-0.16.8-py3-none-win_arm64.whl", hash = "sha256:d075e820af612102ce217f07cc93e69f9490b10ec13ea85fa87bd03d996cef8a"},
{file = "ruff-0.16.8.tar.gz", hash = "sha256:9247bf92b5f04d825c8639a4fe423ec2e4222acd9222e58412b0dab7e442798b"},
]
[[package]] [[package]]
name = "tomli" name = "tomli"
version = "2.4.1" version = "2.4.1"
@@ -217,4 +245,4 @@ files = [
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = ">=3.10" python-versions = ">=3.10"
content-hash = "c5fdfbbe6dbd3da1ba43276d267c57089584d9940904d4cd53d25ce9e9fcd1c9" content-hash = "24c9a2d464f6c737eeae0a66f792fa23824c3960b7cc72a22fff1408f21ec812"
+18 -1
View File
@@ -26,5 +26,22 @@ priority = "supplemental"
[dependency-groups] [dependency-groups]
dev = [ dev = [
"pytest (>=9.1.1,<10.0.0)", "pytest (>=9.1.1,<10.0.0)",
"pytest-expectdir (>=1.2.0,<2.0.0)" "pytest-expectdir (>=1.2.0,<2.0.0)",
"ruff (>=0.16.8,<0.17.0)"
] ]
[tool.ruff]
line-length = 79
target-version = "py311"
[tool.ruff.lint]
select = [
"E",
"F",
"W",
"I",
"UP"
]
[tool.ruff.format]
quote-style = "single"
+1
View File
@@ -13,6 +13,7 @@
# limitations under the License. # limitations under the License.
import sys import sys
from .cli.cli import main from .cli.cli import main
if __name__ == '__main__': if __name__ == '__main__':
+1 -2
View File
@@ -12,8 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from . import cli from . import cli, languages
from . import languages
from .cli import main from .cli import main
__all__ = [ __all__ = [
+42 -38
View File
@@ -12,13 +12,14 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
''' """
The command line interface The command line interface
''' """
import argparse import argparse
import pathlib import pathlib
import sys import sys
from .languages.python import VALID_SECTIONS as VALID_PYTHON_SECTIONS from .languages.python import VALID_SECTIONS as VALID_PYTHON_SECTIONS
__all__ = [ __all__ = [
@@ -31,80 +32,81 @@ __all__ = [
_LANG_PYTHON = 'python' _LANG_PYTHON = 'python'
_PROGRAM_NAME = 'jcloud-docsgen' _PROGRAM_NAME = 'jcloud-docsgen'
def existing_directory(path: str) -> pathlib.Path: def existing_directory(path: str) -> pathlib.Path:
_path = path _path = path
path = pathlib.Path(_path) path = pathlib.Path(_path)
if not path.exists(): if not path.exists():
raise argparse.ArgumentTypeError( raise argparse.ArgumentTypeError(f'{_path}: no such file or directory')
f'{_path}: no such file or directory'
)
if not path.is_dir(): if not path.is_dir():
raise argparse.ArgumentTypeError(f'{_path}: not a directory') raise argparse.ArgumentTypeError(f'{_path}: not a directory')
return path return path
def error(message: str, exit_code: int = 1): def error(message: str, exit_code: int = 1):
''' """
Shows an error message and returns the exit code. Shows an error message and returns the exit code.
:param message: The error message to show. :param message: The error message to show.
:type message: str :type message: str
:param exit_code: The exit code. :param exit_code: The exit code.
:type exit_code: int :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 return exit_code
def create_argument_parser() -> argparse.ArgumentParser: def create_argument_parser() -> argparse.ArgumentParser:
''' """
Prepares a parser to parse the command-line arguments using Prepares a parser to parse the command-line arguments using
argparse. argparse.
:return: The parser. :return: The parser.
:rtype: argparse.ArgumentParser :rtype: argparse.ArgumentParser
''' """
common_parser = argparse.ArgumentParser(add_help = False) common_parser = argparse.ArgumentParser(add_help=False)
common_parser.add_argument( common_parser.add_argument(
'project_directory', 'project_directory',
type = existing_directory, type=existing_directory,
nargs = '?', nargs='?',
default = pathlib.Path('.'), default=pathlib.Path('.'),
help = 'The directory of the project', help='The directory of the project',
) )
common_parser.add_argument( common_parser.add_argument(
'-o', '-o',
'--output-dir', '--output-dir',
type = pathlib.Path, type=pathlib.Path,
help = 'the output directory the documentation will be written to', help='the output directory the documentation will be written to',
) )
argument_parser = argparse.ArgumentParser() argument_parser = argparse.ArgumentParser()
subparsers = argument_parser.add_subparsers( subparsers = argument_parser.add_subparsers(
dest = 'language', dest='language',
required = True, required=True,
) )
python_parser = subparsers.add_parser( python_parser = subparsers.add_parser(
_LANG_PYTHON, _LANG_PYTHON,
parents = [common_parser], parents=[common_parser],
help = 'generate documentation for Python project', help='generate documentation for Python project',
) )
python_parser.add_argument( python_parser.add_argument(
'-s', '-s',
'--sections', '--sections',
nargs = '+', nargs='+',
choices = VALID_PYTHON_SECTIONS, choices=VALID_PYTHON_SECTIONS,
help = 'the sections to be included in the documentation', help='the sections to be included in the documentation',
) )
python_parser.add_argument( python_parser.add_argument(
'-S', '-S',
'--sections-exclude', '--sections-exclude',
nargs = '+', nargs='+',
default = [], default=[],
choices = VALID_PYTHON_SECTIONS, choices=VALID_PYTHON_SECTIONS,
help = ( help=(
'the sections to be excluded in the documentation. ' 'the sections to be excluded in the documentation. '
'If a section shall be included AND excluded, it is excluded.' 'If a section shall be included AND excluded, it is excluded.'
), ),
@@ -112,8 +114,8 @@ def create_argument_parser() -> argparse.ArgumentParser:
python_parser.add_argument( python_parser.add_argument(
'-H', '-H',
'--allow-html', '--allow-html',
action = 'store_true', action='store_true',
help = ( help=(
'indicates whether HTML is allowed in the documentation. ' 'indicates whether HTML is allowed in the documentation. '
'By default, HTML is disabled.' 'By default, HTML is disabled.'
), ),
@@ -121,8 +123,8 @@ def create_argument_parser() -> argparse.ArgumentParser:
python_parser.add_argument( python_parser.add_argument(
'-T', '-T',
'--allow-tables', '--allow-tables',
action = 'store_true', action='store_true',
help = ( help=(
'indicates whether Markdown tables are allowed in the ' 'indicates whether Markdown tables are allowed in the '
'documentation. By default, Markdown tables are disabled. ' 'documentation. By default, Markdown tables are disabled. '
'Warning: Markdown tables are not CommonMark.' 'Warning: Markdown tables are not CommonMark.'
@@ -131,7 +133,8 @@ def create_argument_parser() -> argparse.ArgumentParser:
return argument_parser return argument_parser
def main(argv = None) -> int:
def main(argv=None) -> int:
argument_parser = create_argument_parser() argument_parser = create_argument_parser()
args = argument_parser.parse_args(argv) args = argument_parser.parse_args(argv)
@@ -141,13 +144,14 @@ def main(argv = None) -> int:
try: try:
if args.language == _LANG_PYTHON: if args.language == _LANG_PYTHON:
from .languages.python import generate from .languages.python import generate
generate( generate(
args.project_directory, args.project_directory,
args.output_dir, args.output_dir,
include_sections = args.sections, include_sections=args.sections,
exclude_sections = args.sections_exclude, exclude_sections=args.sections_exclude,
allow_html = args.allow_html, allow_html=args.allow_html,
allow_tables = args.allow_tables, allow_tables=args.allow_tables,
) )
except FileNotFoundError as exc: except FileNotFoundError as exc:
return error(f'{exc.filename}: no such file or directory') return error(f'{exc.filename}: no such file or directory')
@@ -166,7 +170,7 @@ def main(argv = None) -> int:
except SyntaxError: except SyntaxError:
# TODO: add a custom exception class to clearify that a project # TODO: add a custom exception class to clearify that a project
# parsing syntax error is meant. # parsing syntax error is meant.
return error(f'syntax error in project source code') return error('syntax error in project source code')
except KeyboardInterrupt: except KeyboardInterrupt:
return 130 return 130
except BrokenPipeError: except BrokenPipeError:
+29 -26
View File
@@ -13,11 +13,11 @@
# limitations under the License. # limitations under the License.
import pathlib import pathlib
from ...core.python import ( from ...core.python import (
PythonDefinitionDocumentationIncludeSections, PythonDefinitionDocumentationIncludeSections,
PythonDocumentationGenerator, PythonDocumentationGenerator,
) )
from typing import Optional
__all__ = [ __all__ = [
'VALID_SECTIONS', 'VALID_SECTIONS',
@@ -29,24 +29,25 @@ VALID_SECTIONS = list(
PythonDefinitionDocumentationIncludeSections()._args.keys() PythonDefinitionDocumentationIncludeSections()._args.keys()
) )
def parse_sections_list( def parse_sections_list(
include_sections: Optional[list[str]] = None, include_sections: list[str] | None = None,
exclude_sections: list[str] = [], exclude_sections: list[str] = [],
) -> PythonDefinitionDocumentationIncludeSections: ) -> PythonDefinitionDocumentationIncludeSections:
''' """
Returns a Python definition documentation sections list from the Returns a Python definition documentation sections list from the
sections list to be included and the sections list to be excluded. sections list to be included and the sections list to be excluded.
These lists both consist of the sections as strings which contain These lists both consist of the sections as strings which contain
the section name. the section name.
:param include_sections: The list of sections to be included. :param include_sections: The list of sections to be included.
:type include_sections: Optional[list[str]] :type include_sections: list[str] | None
:param exclude_sections: The list of sections to be excluded. :param exclude_sections: The list of sections to be excluded.
:type exclude_sections: list[str] :type exclude_sections: list[str]
:return: The Python definition documentation sections object. :return: The Python definition documentation sections object.
:rtype: PythonDefinitionDocumentationIncludeSections :rtype: PythonDefinitionDocumentationIncludeSections
''' """
if include_sections is None: if include_sections is None:
include_sections = VALID_SECTIONS[:] include_sections = VALID_SECTIONS[:]
@@ -56,17 +57,18 @@ def parse_sections_list(
del include_sections[include_sections.index(section)] del include_sections[include_sections.index(section)]
return PythonDefinitionDocumentationIncludeSections( return PythonDefinitionDocumentationIncludeSections(
signature = 'signature' in include_sections, signature='signature' in include_sections,
returns = 'returns' in include_sections, returns='returns' in include_sections,
params = 'params' in include_sections, params='params' in include_sections,
exceptions = 'exceptions' in include_sections, exceptions='exceptions' in include_sections,
examples = 'examples' in include_sections, examples='examples' in include_sections,
docstring = 'docstring' in include_sections, docstring='docstring' in include_sections,
bases = 'bases' in include_sections, bases='bases' in include_sections,
decorators = 'decorators' in include_sections, decorators='decorators' in include_sections,
class_body = 'class_body' in include_sections, class_body='class_body' in include_sections,
) )
def _generate( def _generate(
project_directory: pathlib.Path, project_directory: pathlib.Path,
output_directory: pathlib.Path, output_directory: pathlib.Path,
@@ -77,7 +79,7 @@ def _generate(
allow_html: bool = False, allow_html: bool = False,
allow_tables: bool = False, allow_tables: bool = False,
) -> None: ) -> None:
''' """
Generates the documentation for a Python project. Generates the documentation for a Python project.
:param project_directory: The project directory. :param project_directory: The project directory.
@@ -92,27 +94,28 @@ def _generate(
:type allow_html: bool :type allow_html: bool
:param allow_tables: Indicates whether Markdown tables are allowed. :param allow_tables: Indicates whether Markdown tables are allowed.
Warning: Markdown tables are not CommonMark. Warning: Markdown tables are not CommonMark.
''' """
docs_generator = PythonDocumentationGenerator( docs_generator = PythonDocumentationGenerator(
project_directory, project_directory,
output_directory, output_directory,
include_sections = include_sections, include_sections=include_sections,
allow_html = allow_html, allow_html=allow_html,
allow_tables = allow_tables, allow_tables=allow_tables,
) )
docs_generator.generate_documentation() docs_generator.generate_documentation()
def generate( def generate(
project_directory: pathlib.Path, project_directory: pathlib.Path,
output_directory: pathlib.Path, output_directory: pathlib.Path,
*, *,
include_sections: Optional[list[str]] = None, include_sections: list[str] | None = None,
exclude_sections: list[str] = [], exclude_sections: list[str] = [],
allow_html: bool = False, allow_html: bool = False,
allow_tables: bool = False, allow_tables: bool = False,
) -> None: ) -> None:
''' """
Generates the documentation for a Python project. Generates the documentation for a Python project.
:param project_directory: The project directory. :param project_directory: The project directory.
@@ -121,7 +124,7 @@ def generate(
:type output_directory: pathlib.Path :type output_directory: pathlib.Path
:param include_sections: The sections to be included in the :param include_sections: The sections to be included in the
documentation. documentation.
:type include_sections: Optional[list[str]] :type include_sections: list[str] | None
:param exclude_sections: The sections to be excluded in the :param exclude_sections: The sections to be excluded in the
documentation. documentation.
:type exclude_sections: list[str] :type exclude_sections: list[str]
@@ -130,14 +133,14 @@ def generate(
:type allow_html: bool :type allow_html: bool
:param allow_tables: Indicates whether Markdown tables are allowed. :param allow_tables: Indicates whether Markdown tables are allowed.
Warning: Markdown tables are not CommonMark. Warning: Markdown tables are not CommonMark.
''' """
sections = parse_sections_list(include_sections, exclude_sections) sections = parse_sections_list(include_sections, exclude_sections)
_generate( _generate(
project_directory, project_directory,
output_directory, output_directory,
include_sections = sections, include_sections=sections,
allow_html = allow_html, allow_html=allow_html,
allow_tables = allow_tables, allow_tables=allow_tables,
) )
+1 -1
View File
@@ -12,9 +12,9 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from . import namespaces
from ._core import * from ._core import *
from ._core import __all__ as _core__all__ from ._core import __all__ as _core__all__
from . import namespaces
__all__ = [ __all__ = [
*_core__all__, *_core__all__,
+276 -246
View File
@@ -13,25 +13,26 @@
# limitations under the License. # limitations under the License.
from __future__ import annotations from __future__ import annotations
from ...utils import non_empty_str, get_relative_path_segments
from ...exceptions import PythonArgumentStructureError, InvalidNamespaceError import ast
from .namespaces import PythonModuleNamespace, PythonPackageNamespace import pathlib
from collections.abc import Iterator
from dataclasses import dataclass
from typing import TypeAlias
import docstring_parser
from ...exceptions import InvalidNamespaceError, PythonArgumentStructureError
from ...utils import get_relative_path_segments, non_empty_str
from .arguments import PythonArgumentKind, PythonFunctionArgument
from .definitions import ( from .definitions import (
PythonAsyncFunctionDefinition, PythonAsyncFunctionDefinition,
PythonClassDefinition, PythonClassDefinition,
PythonDefinition, PythonDefinition,
PythonFunctionDefinition, PythonFunctionDefinition,
) )
from .arguments import PythonArgumentKind, PythonFunctionArgument
from .docstrings import PythonDocstring from .docstrings import PythonDocstring
from collections.abc import Iterator from .namespaces import PythonModuleNamespace, PythonPackageNamespace
from typing import TypeAlias, Union
from dataclasses import dataclass
from enum import Enum
import docstring_parser
import pathlib
import ast
import types
__all__ = [ __all__ = [
'PythonDefinitionDocumentationIncludeSections', 'PythonDefinitionDocumentationIncludeSections',
@@ -40,15 +41,16 @@ __all__ = [
'PythonDocumentationGenerator', 'PythonDocumentationGenerator',
] ]
def _collect_definitions(tree_or_node) -> Iterator[PythonDefinition]: def _collect_definitions(tree_or_node) -> Iterator[PythonDefinition]:
''' """
Collects all definitions of the tree or node. Collects all definitions of the tree or node.
:param tree_or_node: The tree or node. :param tree_or_node: The tree or node.
:return: The definitions. :return: The definitions.
:rtype: Iterator[PythonDefinition] :rtype: Iterator[PythonDefinition]
''' """
for node in tree_or_node.body: for node in tree_or_node.body:
if isinstance(node, ast.FunctionDef): if isinstance(node, ast.FunctionDef):
@@ -58,18 +60,20 @@ def _collect_definitions(tree_or_node) -> Iterator[PythonDefinition]:
if isinstance(node, ast.ClassDef): if isinstance(node, ast.ClassDef):
yield PythonClassDefinition.from_node(node) yield PythonClassDefinition.from_node(node)
_Arguments: TypeAlias = tuple[ _Arguments: TypeAlias = tuple[
list[PythonFunctionArgument], list[PythonFunctionArgument],
list[PythonFunctionArgument], list[PythonFunctionArgument],
Union[PythonFunctionArgument, None], PythonFunctionArgument | None,
list[PythonFunctionArgument], list[PythonFunctionArgument],
Union[PythonFunctionArgument, None], PythonFunctionArgument | None,
] ]
def _group_argument_list( def _group_argument_list(
argument_list: list[PythonFunctionArgument], argument_list: list[PythonFunctionArgument],
) -> _Arguments: ) -> _Arguments:
''' """
Groups the argument list into a list of positional-only arguments, a 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 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. keyword-only arguments and the var-keyword-argument if there is one.
@@ -78,13 +82,12 @@ def _group_argument_list(
:type argument_list: list[PythonFunctionArgument] :type argument_list: list[PythonFunctionArgument]
:rtype: _Arguments :rtype: _Arguments
''' """
if argument_list: if argument_list:
if argument_list[-1].kind != PythonArgumentKind.KWARGS and any([ if argument_list[-1].kind != PythonArgumentKind.KWARGS and any(
arg.kind == PythonArgumentKind.KWARGS [arg.kind == PythonArgumentKind.KWARGS for arg in argument_list]
for arg in argument_list ):
]):
raise PythonArgumentStructureError( raise PythonArgumentStructureError(
'arguments cannot follow var-keyword argument', 'arguments cannot follow var-keyword argument',
) )
@@ -106,7 +109,7 @@ def _group_argument_list(
if vararg is not None: if vararg is not None:
raise PythonArgumentStructureError( raise PythonArgumentStructureError(
'* argument may only appear once', '* argument may only appear once',
definition = arg, definition=arg,
) )
vararg = arg vararg = arg
@@ -124,10 +127,11 @@ def _group_argument_list(
kwargs, kwargs,
) )
def _signature_argument_list( def _signature_argument_list(
argument_list: list[PythonFunctionArgument], argument_list: list[PythonFunctionArgument],
) -> list[str]: ) -> list[str]:
''' """
Returns a list of the arguments as strings for a function signature. Returns a list of the arguments as strings for a function signature.
:param argument_list: The argument list. :param argument_list: The argument list.
@@ -135,7 +139,7 @@ def _signature_argument_list(
:return: The list of function signature argument strings. :return: The list of function signature argument strings.
:rtype: list[str] :rtype: list[str]
''' """
( (
positional_only_args, positional_only_args,
@@ -170,8 +174,9 @@ def _signature_argument_list(
return args return args
def _fit_row_length(expected: int, row: tuple) -> tuple: def _fit_row_length(expected: int, row: tuple) -> tuple:
''' """
Fits the row length to the expected length. Fits the row length to the expected length.
If the row is too long, the tail, that makes it too long, is omitted. If the row is too long, the tail, that makes it too long, is omitted.
@@ -181,7 +186,7 @@ def _fit_row_length(expected: int, row: tuple) -> tuple:
:type expected: int :type expected: int
:param row: The row. :param row: The row.
:type row: tuple :type row: tuple
''' """
if expected < 0: if expected < 0:
raise ValueError('expected must be larger than or equal 0.') raise ValueError('expected must be larger than or equal 0.')
@@ -191,6 +196,7 @@ def _fit_row_length(expected: int, row: tuple) -> tuple:
else: else:
return row + (None,) * (expected - len(row)) return row + (None,) * (expected - len(row))
def _markdown_table( def _markdown_table(
header: tuple, header: tuple,
body: list[tuple], body: list[tuple],
@@ -200,7 +206,7 @@ def _markdown_table(
one_line_values: bool = True, one_line_values: bool = True,
missing_value: str = '--', missing_value: str = '--',
) -> str: ) -> str:
''' """
Generates a table. Generates a table.
If ``allow_markdown_table`` is ``False``, an alternative If ``allow_markdown_table`` is ``False``, an alternative
@@ -222,7 +228,7 @@ def _markdown_table(
:param missing_value: The string that is used if a value is missing :param missing_value: The string that is used if a value is missing
(only if ``force_row_lengths`` is ``False``). (only if ``force_row_lengths`` is ``False``).
:type missing_value: str :type missing_value: str
''' """
if not header or not body: if not header or not body:
return '' return ''
@@ -231,37 +237,34 @@ def _markdown_table(
if allow_markdown_table: if allow_markdown_table:
table = ( table = (
'| ' \ '| ' + ' | '.join(header) + ' |\n|' + ' --- |' * len(header) + '\n'
+ ' | '.join(header) \
+ ' |\n|' \
+ ' --- |' * len(header) \
+ '\n'
) )
for row in body: for row in body:
if len(row) != header_len: if len(row) != header_len:
if force_row_lengths: if force_row_lengths:
raise ValueError( raise ValueError(
f'expected {header_len} value{ f'expected {header_len} value{
"s" "s" if header_len > 1 else ""
if header_len > 1
else ""
}, got {len(row)}', }, got {len(row)}',
) )
else: else:
row = _fit_row_length(header_len, row) row = _fit_row_length(header_len, row)
table += '| ' + ' | '.join([ table += (
str( '| '
col + ' | '.join(
if col is not None [
else missing_value str(col if col is not None else missing_value).replace(
).replace('\n', ' ') '\n', ' '
if one_line_values )
else str(col if col is not None else missing_value) if one_line_values
for col in row else str(col if col is not None else missing_value)
]) + ' |\n' for col in row
]
)
+ ' |\n'
)
else: else:
table = '' table = ''
@@ -270,9 +273,7 @@ def _markdown_table(
if force_row_lengths: if force_row_lengths:
raise ValueError( raise ValueError(
f'expected {header_len} value{ f'expected {header_len} value{
"s" "s" if header_len > 1 else ""
if header_len > 1
else ""
}, got {len(row)}', }, got {len(row)}',
) )
else: else:
@@ -281,37 +282,33 @@ def _markdown_table(
sep = '- ' sep = '- '
for i, col in enumerate(row): for i, col in enumerate(row):
table += ( table += (
sep \ sep
+ header[i] \ + header[i]
+ ': ' \ + ': '
+ ( + (
str( str(col if col is not None else missing_value).replace(
col '\n', ' '
if col is not None
else missing_value
).replace('\n', ' ') \
if one_line_values \
else str(
col
if col is not None
else missing_value
) )
) + '\n\n' if one_line_values
else str(col if col is not None else missing_value)
)
+ '\n\n'
) )
sep = ' ' sep = ' '
return table.strip() return table.strip()
def _is_docstring(docstring: Union[str, PythonDocstring, None]) -> bool:
''' def _is_docstring(docstring: str | PythonDocstring | None) -> bool:
"""
Checks whether the docstring is a docstring or an empty docstring. Checks whether the docstring is a docstring or an empty docstring.
:param docstring: The docstring. :param docstring: The docstring.
:type docstring: Union[str, PythonDocstring, None] :type docstring: str | PythonDocstring | None
:return: Whether there is a docstring. :return: Whether there is a docstring.
:rtype: bool :rtype: bool
''' """
if docstring is None: if docstring is None:
return False return False
@@ -321,10 +318,12 @@ def _is_docstring(docstring: Union[str, PythonDocstring, None]) -> bool:
return docstring.strip() != '' return docstring.strip() != ''
@dataclass @dataclass
class _FunctionParamInfo: class _FunctionParamInfo:
annotation: Union[str, None] annotation: str | None
description: Union[str, None] description: str | None
@dataclass @dataclass
class _NoDocstringTemplate: class _NoDocstringTemplate:
@@ -340,10 +339,12 @@ class _NoDocstringTemplate:
long_description = None long_description = None
blank_after_short_description = None blank_after_short_description = None
_NoDocstring = _NoDocstringTemplate() _NoDocstring = _NoDocstringTemplate()
def _argument_kind_human_readable(argument_kind: PythonArgumentKind) -> str: def _argument_kind_human_readable(argument_kind: PythonArgumentKind) -> str:
''' """
Returns the human-readable representation of a Returns the human-readable representation of a
``PythonArgumentKind``. ``PythonArgumentKind``.
@@ -352,15 +353,16 @@ def _argument_kind_human_readable(argument_kind: PythonArgumentKind) -> str:
:return: The human-readable representation. :return: The human-readable representation.
:rtype: str :rtype: str
''' """
return argument_kind.name.lower().replace('_', '-') return argument_kind.name.lower().replace('_', '-')
def _function_params_info( def _function_params_info(
arg_list: list[PythonFunctionArgument], arg_list: list[PythonFunctionArgument],
docstring_arg_list: list[docstring_parser.DocstringParam], docstring_arg_list: list[docstring_parser.DocstringParam],
) -> dict[str, _FunctionParamInfo]: ) -> dict[str, _FunctionParamInfo]:
''' """
Returns a dictionary with the parameter info. Returns a dictionary with the parameter info.
The keys of the dictionary are the parameter names and the values are The keys of the dictionary are the parameter names and the values are
@@ -377,11 +379,10 @@ def _function_params_info(
:return: The dictionary with the parameter types. :return: The dictionary with the parameter types.
:rtype: dict[str, _FunctionParamInfo] :rtype: dict[str, _FunctionParamInfo]
''' """
arg_annotations = { arg_annotations = {
arg.name: _FunctionParamInfo(arg.annotation, None) arg.name: _FunctionParamInfo(arg.annotation, None) for arg in arg_list
for arg in arg_list
} }
for arg in docstring_arg_list: for arg in docstring_arg_list:
@@ -393,8 +394,9 @@ def _function_params_info(
return arg_annotations return arg_annotations
class PythonDefinitionDocumentationIncludeSections: class PythonDefinitionDocumentationIncludeSections:
''' """
A class representing the included sections in a Python definition A class representing the included sections in a Python definition
documentation. documentation.
@@ -414,50 +416,55 @@ class PythonDefinitionDocumentationIncludeSections:
:type bases: bool :type bases: bool
:param decorators: Whether include the decorators section. :param decorators: Whether include the decorators section.
:type decorators: bool :type decorators: bool
''' """
def __init__( def __init__(
self, self,
*, *,
signature: bool = True, signature: bool = True,
returns: bool = True, returns: bool = True,
params: bool = True, params: bool = True,
exceptions: bool = True, exceptions: bool = True,
examples: bool = True, examples: bool = True,
docstring: bool = True, docstring: bool = True,
bases: bool = True, bases: bool = True,
decorators: bool = True, decorators: bool = True,
class_body: bool = True, class_body: bool = True,
): ):
self.signature = signature self.signature = signature
self.returns = returns self.returns = returns
self.params = params self.params = params
self.exceptions = exceptions self.exceptions = exceptions
self.examples = examples self.examples = examples
self.docstring = docstring self.docstring = docstring
self.bases = bases self.bases = bases
self.decorators = decorators self.decorators = decorators
self.class_body = class_body self.class_body = class_body
self._args = { self._args = {
'signature': self.signature, 'signature': self.signature,
'returns': self.returns, 'returns': self.returns,
'params': self.params, 'params': self.params,
'exceptions': self.exceptions, 'exceptions': self.exceptions,
'examples': self.examples, 'examples': self.examples,
'docstring': self.docstring, 'docstring': self.docstring,
'bases': self.bases, 'bases': self.bases,
'decorators': self.decorators, 'decorators': self.decorators,
'class_body': self.class_body, 'class_body': self.class_body,
} }
def __repr__(self) -> str: def __repr__(self) -> str:
return type(self).__name__+ '(' + ', '.join( return (
[ type(self).__name__
key + ' = ' + repr(value) + '('
for key, value in self._args.items() + ', '.join(
], [
) + ')' key + ' = ' + repr(value)
for key, value in self._args.items()
],
)
+ ')'
)
def __eq__(self, value) -> str: def __eq__(self, value) -> str:
if not isinstance( if not isinstance(
@@ -467,8 +474,9 @@ class PythonDefinitionDocumentationIncludeSections:
return False return False
return self._args == value._args return self._args == value._args
class PythonDefinitionDocumentationGenerator: class PythonDefinitionDocumentationGenerator:
''' """
A documentation generator for a Python definition. A documentation generator for a Python definition.
:param definition: The Python definition. :param definition: The Python definition.
@@ -492,21 +500,21 @@ class PythonDefinitionDocumentationGenerator:
:type skip_empty_sections: bool :type skip_empty_sections: bool
:param is_method: Controls whether the definition is a class method. :param is_method: Controls whether the definition is a class method.
:type is_method: bool :type is_method: bool
''' """
def __init__( def __init__(
self, self,
definition: PythonDefinition, definition: PythonDefinition,
include_sections: PythonDefinitionDocumentationIncludeSections = ( include_sections: PythonDefinitionDocumentationIncludeSections = (
PythonDefinitionDocumentationIncludeSections() PythonDefinitionDocumentationIncludeSections()
), ),
level: int = 1, level: int = 1,
*, *,
allow_html: bool = True, allow_html: bool = True,
allow_tables: bool = False, allow_tables: bool = False,
skip_empty_sections: bool = True, skip_empty_sections: bool = True,
is_method: bool = False, is_method: bool = False,
) -> None: ) -> None:
self.definition = definition self.definition = definition
self.include_sections = include_sections self.include_sections = include_sections
@@ -521,13 +529,15 @@ class PythonDefinitionDocumentationGenerator:
@property @property
def _docstring(self): def _docstring(self):
if self._docstring_cache is None: if self._docstring_cache is None:
self._docstring_cache = self.definition.doc.parse() \ self._docstring_cache = (
if _is_docstring(self.definition.doc) \ self.definition.doc.parse()
if _is_docstring(self.definition.doc)
else _NoDocstring else _NoDocstring
)
return self._docstring_cache return self._docstring_cache
def generate_signature(self) -> str: def generate_signature(self) -> str:
''' """
Generates the signature of the definition. Works only if the Generates the signature of the definition. Works only if the
definition is a function or asynchronous function definition. definition is a function or asynchronous function definition.
@@ -535,18 +545,23 @@ class PythonDefinitionDocumentationGenerator:
:return: The signature. :return: The signature.
:rtype: str :rtype: str
''' """
if isinstance(self.definition, PythonClassDefinition): if isinstance(self.definition, PythonClassDefinition):
raise TypeError( raise TypeError(
'Cannot generate a signature of a class definition.', 'Cannot generate a signature of a class definition.',
) )
signature = self.definition.name + '(' + ', '.join( signature = (
_signature_argument_list( self.definition.name
self.definition.args, + '('
), + ', '.join(
) + ')' _signature_argument_list(
self.definition.args,
),
)
+ ')'
)
if self.definition.returns is not None: if self.definition.returns is not None:
signature += ' -> ' + self.definition.returns signature += ' -> ' + self.definition.returns
@@ -554,38 +569,38 @@ class PythonDefinitionDocumentationGenerator:
return signature.strip() return signature.strip()
def _generate_signature_section(self) -> str: def _generate_signature_section(self) -> str:
''' """
Generates the signature section. Generates the signature section.
:return: The signature section. :return: The signature section.
:rtype: str :rtype: str
''' """
definition_type = 'Function' if not self.is_method else 'Method' definition_type = 'Function' if not self.is_method else 'Method'
heading = self.level * '#' + '# ' + (definition_type) +' signature' heading = self.level * '#' + '# ' + (definition_type) + ' signature'
return f'''{heading} return f"""{heading}
```python ```python
{self.generate_signature()} {self.generate_signature()}
```''' ```"""
def _generate_docstring_section(self) -> str: def _generate_docstring_section(self) -> str:
''' """
Generates the docstring section. Generates the docstring section.
:return: The docstring section. :return: The docstring section.
:rtype: str :rtype: str
''' """
md = '' md = ''
if _is_docstring(self.definition.doc) or not self.skip_empty_sections: if _is_docstring(self.definition.doc) or not self.skip_empty_sections:
if self.allow_html: if self.allow_html:
md += f'<details><summary><h{str( md += f'<details><summary><h{
self.level + 1 str(self.level + 1)
)} style="display:inline">Docstring</h{str( } style="display:inline">Docstring</h{
self.level + 1 str(self.level + 1)
)}></summary>' }></summary>'
else: else:
md += self.level * '#' + '# Docstring\n\n' md += self.level * '#' + '# Docstring\n\n'
@@ -602,12 +617,12 @@ class PythonDefinitionDocumentationGenerator:
return md.strip() return md.strip()
def _generate_params_section(self) -> str: def _generate_params_section(self) -> str:
''' """
Generates the parameters section. Generates the parameters section.
:return: The parameters section. :return: The parameters section.
:rtype: str :rtype: str
''' """
md = '' md = ''
@@ -636,7 +651,7 @@ class PythonDefinitionDocumentationGenerator:
) )
for arg in self.definition.args for arg in self.definition.args
], ],
allow_markdown_table = self.allow_tables, allow_markdown_table=self.allow_tables,
) )
md += '\n\n' md += '\n\n'
@@ -647,12 +662,12 @@ class PythonDefinitionDocumentationGenerator:
return md.strip() return md.strip()
def _generate_returns_section(self) -> str: def _generate_returns_section(self) -> str:
''' """
Generates the returns section. Generates the returns section.
:return: The returns section. :return: The returns section.
:rtype: str :rtype: str
''' """
md = '' md = ''
@@ -686,12 +701,12 @@ class PythonDefinitionDocumentationGenerator:
return md.strip() return md.strip()
def _generate_exceptions_section(self) -> str: def _generate_exceptions_section(self) -> str:
''' """
Generates the exceptions section. Generates the exceptions section.
:return: The exceptions section. :return: The exceptions section.
:rtype: str :rtype: str
''' """
md = '' md = ''
@@ -699,26 +714,31 @@ class PythonDefinitionDocumentationGenerator:
md += self.level * '#' + '# Exceptions\n\n' md += self.level * '#' + '# Exceptions\n\n'
if self._docstring.raises: 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) ('`' + exception.type_name + '`')
) if exception.type_name is not None
for exception in self._docstring.raises else None,
], allow_markdown_table = self.allow_tables) non_empty_str(exception.description),
)
for exception in self._docstring.raises
],
allow_markdown_table=self.allow_tables,
)
elif not self.skip_empty_sections: elif not self.skip_empty_sections:
md += 'This function raises no exceptions.' md += 'This function raises no exceptions.'
return md.strip() return md.strip()
def _generate_examples_section(self) -> str: def _generate_examples_section(self) -> str:
''' """
Generates the examples section. Generates the examples section.
:return: The examples section. :return: The examples section.
:rtype: str :rtype: str
''' """
md = '' md = ''
@@ -727,9 +747,11 @@ class PythonDefinitionDocumentationGenerator:
examples = [] examples = []
for example in self._docstring.examples: for example in self._docstring.examples:
examples.append('```python\n' +\ examples.append(
(example.snippet or example.description) + \ '```python\n'
'\n```') + (example.snippet or example.description)
+ '\n```'
)
md += '\n\n---\n\n'.join(examples) md += '\n\n---\n\n'.join(examples)
if not self._docstring.examples and not self.skip_empty_sections: if not self._docstring.examples and not self.skip_empty_sections:
@@ -738,12 +760,12 @@ class PythonDefinitionDocumentationGenerator:
return md.strip() return md.strip()
def _generate_decorators_section(self) -> str: def _generate_decorators_section(self) -> str:
''' """
Generates the decorators section. Generates the decorators section.
:return: The decorators section. :return: The decorators section.
:rtype: str :rtype: str
''' """
md = '' md = ''
@@ -759,12 +781,12 @@ class PythonDefinitionDocumentationGenerator:
return md.strip() return md.strip()
def _generate_bases_section(self) -> str: def _generate_bases_section(self) -> str:
''' """
Generates the bases section. Generates the bases section.
:return: The bases section. :return: The bases section.
:rtype: str :rtype: str
''' """
md = '' md = ''
@@ -780,12 +802,12 @@ class PythonDefinitionDocumentationGenerator:
return md.strip() return md.strip()
def _generate_body_section(self) -> str: def _generate_body_section(self) -> str:
''' """
Generates the body section. Generates the body section.
:return: The body section. :return: The body section.
:rtype: str :rtype: str
''' """
md = '' md = ''
@@ -793,15 +815,18 @@ class PythonDefinitionDocumentationGenerator:
md += self.level * '#' + '# Body\n\n' md += self.level * '#' + '# Body\n\n'
for method_or_class in self.definition.body: for method_or_class in self.definition.body:
md += PythonDefinitionDocumentationGenerator( md += (
method_or_class, PythonDefinitionDocumentationGenerator(
self.include_sections, method_or_class,
level = self.level + 2, self.include_sections,
allow_html = self.allow_html, level=self.level + 2,
allow_tables = self.allow_tables, allow_html=self.allow_html,
skip_empty_sections = self.skip_empty_sections, allow_tables=self.allow_tables,
is_method = True, skip_empty_sections=self.skip_empty_sections,
).generate_documentation() + '\n\n' is_method=True,
).generate_documentation()
+ '\n\n'
)
if not self.definition.body and not self.skip_empty_sections: if not self.definition.body and not self.skip_empty_sections:
md += 'This class does not have a body.' md += 'This class does not have a body.'
@@ -809,12 +834,12 @@ class PythonDefinitionDocumentationGenerator:
return md.strip() return md.strip()
def generate_documentation(self) -> str: def generate_documentation(self) -> str:
''' """
Generates the documentation in the markdown format. Generates the documentation in the markdown format.
:return: The documentation. :return: The documentation.
:rtype: str :rtype: str
''' """
md = self.level * '#' + ' ' md = self.level * '#' + ' '
@@ -848,13 +873,13 @@ class PythonDefinitionDocumentationGenerator:
md += self._generate_signature_section() + '\n\n' md += self._generate_signature_section() + '\n\n'
if self.include_sections.returns: if self.include_sections.returns:
md += self._generate_returns_section() + '\n\n' md += self._generate_returns_section() + '\n\n'
if self.include_sections.params: if self.include_sections.params:
md += self._generate_params_section() + '\n\n' md += self._generate_params_section() + '\n\n'
if self.include_sections.exceptions: if self.include_sections.exceptions:
md += self._generate_exceptions_section() + '\n\n' md += self._generate_exceptions_section() + '\n\n'
if isinstance(self.definition, PythonClassDefinition): if isinstance(self.definition, PythonClassDefinition):
if self.include_sections.bases: if self.include_sections.bases:
@@ -874,8 +899,9 @@ class PythonDefinitionDocumentationGenerator:
return md.strip() return md.strip()
class PythonModuleDocumentationGenerator: class PythonModuleDocumentationGenerator:
''' """
A documentation generator for a Python module. A documentation generator for a Python module.
:param module_path: The path of the module file. :param module_path: The path of the module file.
@@ -883,25 +909,25 @@ class PythonModuleDocumentationGenerator:
:param src_directory: The directory of the source code. It is :param src_directory: The directory of the source code. It is
optional and only used for making the module optional and only used for making the module
identifiers in the documentation relative. identifiers in the documentation relative.
:type src_directory: Union[pathlib.Path, None] :type src_directory: pathlib.Path | None
:param include_sections: The sections that will be included. :param include_sections: The sections that will be included.
:type include_sections: PythonDefinitionDocumentationIncludeSections :type include_sections: PythonDefinitionDocumentationIncludeSections
:param kwargs: The options for ``PythonDefinitionDocumentationGenerator``. :param kwargs: The options for ``PythonDefinitionDocumentationGenerator``.
''' """
def __init__( def __init__(
self, self,
module_path: pathlib.Path, module_path: pathlib.Path,
src_directory: Union[pathlib.Path, None] = None, src_directory: pathlib.Path | None = None,
include_sections: PythonDefinitionDocumentationIncludeSections = ( include_sections: PythonDefinitionDocumentationIncludeSections = (
PythonDefinitionDocumentationIncludeSections() PythonDefinitionDocumentationIncludeSections()
), ),
**kwargs, **kwargs,
) -> None: ) -> None:
if module_path.name.count('.') > 1: if module_path.name.count('.') > 1:
raise InvalidNamespaceError( raise InvalidNamespaceError(
'namespace identifier cannot contain more than one dot', '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)) self.module_path = pathlib.Path(str(module_path))
@@ -930,25 +956,24 @@ class PythonModuleDocumentationGenerator:
return self._ast_cache return self._ast_cache
def collect_definitions(self) -> Iterator[PythonDefinition]: def collect_definitions(self) -> Iterator[PythonDefinition]:
''' """
Collects all function and class definitions of the module. Collects all function and class definitions of the module.
:return: All definitions of the module :return: All definitions of the module
:rtype: Iterator[PythonDefinition] :rtype: Iterator[PythonDefinition]
''' """
tree = self._ast tree = self._ast
return _collect_definitions(tree) return _collect_definitions(tree)
def generate_documentation(self) -> str: def generate_documentation(self) -> str:
''' """
Generates the documentation in the markdown format. Generates the documentation in the markdown format.
:return: The documentation. :return: The documentation.
:rtype: str :rtype: str
''' """
md = '# Module `' md = '# Module `'
@@ -973,26 +998,30 @@ class PythonModuleDocumentationGenerator:
md += docstring + '\n\n' md += docstring + '\n\n'
for definition in self.collect_definitions(): for definition in self.collect_definitions():
md += PythonDefinitionDocumentationGenerator( md += (
definition, PythonDefinitionDocumentationGenerator(
self.include_sections, definition,
2, self.include_sections,
**self.kwargs, 2,
).generate_documentation() + '\n\n' **self.kwargs,
).generate_documentation()
+ '\n\n'
)
return md.strip() return md.strip()
def _generate_package_namespace_documentation( def _generate_package_namespace_documentation(
namespace: PythonPackageNamespace, namespace: PythonPackageNamespace,
documentation_directory: pathlib.Path, documentation_directory: pathlib.Path,
root_directory: pathlib.Path, root_directory: pathlib.Path,
dir: pathlib.Path, dir: pathlib.Path,
include_sections: PythonDefinitionDocumentationIncludeSections = ( include_sections: PythonDefinitionDocumentationIncludeSections = (
PythonDefinitionDocumentationIncludeSections() PythonDefinitionDocumentationIncludeSections()
), ),
kwargs: dict = {}, kwargs: dict = {},
) -> None: ) -> None:
''' """
Generates the documentation for a package namespace and writes it to Generates the documentation for a package namespace and writes it to
the documentation directory. the documentation directory.
@@ -1007,19 +1036,20 @@ def _generate_package_namespace_documentation(
:param include_sections: The sections to be included in the documentation. :param include_sections: The sections to be included in the documentation.
:type include_sections: PythonDefinitionDocumentationIncludeSections :type include_sections: PythonDefinitionDocumentationIncludeSections
:param kwargs: The options for the documentation generator. :param kwargs: The options for the documentation generator.
''' """
documentation_directory = pathlib.Path(str(documentation_directory)) documentation_directory = pathlib.Path(str(documentation_directory))
for sub_namespace in namespace.sub_namespaces: for sub_namespace in namespace.sub_namespaces:
if isinstance(sub_namespace, PythonPackageNamespace): if isinstance(sub_namespace, PythonPackageNamespace):
( (documentation_directory / sub_namespace.name).mkdir(
documentation_directory / sub_namespace.name mode=0o700, exist_ok=True
).mkdir(mode = 0o700, exist_ok = True) )
_generate_package_namespace_documentation( _generate_package_namespace_documentation(
sub_namespace, sub_namespace,
documentation_directory / sub_namespace.name, documentation_directory / sub_namespace.name,
root_directory, dir / sub_namespace.name, root_directory,
dir / sub_namespace.name,
include_sections, include_sections,
kwargs, kwargs,
) )
@@ -1035,8 +1065,9 @@ def _generate_package_namespace_documentation(
).generate_documentation() ).generate_documentation()
) )
class PythonDocumentationGenerator: class PythonDocumentationGenerator:
''' """
The class for the documentation generator. The class for the documentation generator.
:param project_directory: The directory of the project. :param project_directory: The directory of the project.
@@ -1046,17 +1077,17 @@ class PythonDocumentationGenerator:
:param include_sections: The sections to be included in the documentation. :param include_sections: The sections to be included in the documentation.
:type include_sections: PythonDefinitionDocumentationIncludeSections :type include_sections: PythonDefinitionDocumentationIncludeSections
:param kwargs: The options for ``PythonDefinitionDocumentationGenerator``. :param kwargs: The options for ``PythonDefinitionDocumentationGenerator``.
''' """
def __init__( def __init__(
self, self,
project_directory: pathlib.Path, project_directory: pathlib.Path,
docs_directory: pathlib.Path, docs_directory: pathlib.Path,
include_sections: PythonDefinitionDocumentationIncludeSections = ( include_sections: PythonDefinitionDocumentationIncludeSections = (
PythonDefinitionDocumentationIncludeSections() PythonDefinitionDocumentationIncludeSections()
), ),
**kwargs, **kwargs,
) -> None: ) -> None:
self.project_directory = pathlib.Path(str(project_directory)) self.project_directory = pathlib.Path(str(project_directory))
self.docs_directory = pathlib.Path(str(docs_directory)) self.docs_directory = pathlib.Path(str(docs_directory))
self.include_sections = include_sections self.include_sections = include_sections
@@ -1074,25 +1105,24 @@ class PythonDocumentationGenerator:
return namespace return namespace
def namespace(self) -> list[PythonPackageNamespace]: def namespace(self) -> list[PythonPackageNamespace]:
''' """
Returns the project as a namespace. Returns the project as a namespace.
:return: The project as a namespace. :return: The project as a namespace.
:rtype: list[PythonPackageNamespace] :rtype: list[PythonPackageNamespace]
''' """
src_dir = self.project_directory / 'src' src_dir = self.project_directory / 'src'
return [ return [
self._namespace(dir) self._namespace(dir) for dir in src_dir.iterdir() if dir.is_dir()
for dir in src_dir.iterdir() if dir.is_dir()
] ]
def generate_documentation(self) -> None: def generate_documentation(self) -> None:
''' """
Generates the full project documentation and writes it to the Generates the full project documentation and writes it to the
documentation directory. documentation directory.
''' """
_generate_package_namespace_documentation( _generate_package_namespace_documentation(
PythonPackageNamespace('src', self.namespace()), PythonPackageNamespace('src', self.namespace()),
+52 -47
View File
@@ -13,17 +13,18 @@
# limitations under the License. # limitations under the License.
from __future__ import annotations from __future__ import annotations
from enum import Enum
from typing import Union
import ast import ast
from ...utils import assert_that_is_instance import keyword
import types
from enum import Enum
from ...exceptions import ( from ...exceptions import (
InvalidPythonAnnotationError, InvalidPythonAnnotationError,
InvalidPythonIdentifierError, InvalidPythonIdentifierError,
PythonArgumentStructureError, PythonArgumentStructureError,
) )
import keyword from ...utils import assert_that_is_instance
import types
__all__ = [ __all__ = [
'PythonArgumentKind', 'PythonArgumentKind',
@@ -31,6 +32,7 @@ __all__ = [
'PythonASTArgumentsListParser', 'PythonASTArgumentsListParser',
] ]
class PythonArgumentKind(Enum): class PythonArgumentKind(Enum):
NORMAL = 0 NORMAL = 0
POSITIONAL_ONLY = 1 POSITIONAL_ONLY = 1
@@ -38,8 +40,9 @@ class PythonArgumentKind(Enum):
VARARG = 3 VARARG = 3
KWARGS = 4 KWARGS = 4
class PythonFunctionArgument: class PythonFunctionArgument:
''' """
Represents an argument of a Python function. Represents an argument of a Python function.
:param name: The name of the argument. :param name: The name of the argument.
@@ -47,17 +50,17 @@ class PythonFunctionArgument:
:param kind: The kind of the argument. :param kind: The kind of the argument.
:type kind: ArgumentKind :type kind: ArgumentKind
:param default: The default value of the argument. :param default: The default value of the argument.
:type default: Union[str, None] :type default: str | None
:param annotation: The type annotation of the argument. :param annotation: The type annotation of the argument.
:type annotation: Union[str, None] :type annotation: str | None
''' """
def __init__( def __init__(
self, self,
name: str, name: str,
kind: PythonArgumentKind, kind: PythonArgumentKind,
default: Union[str, None], default: str | None,
annotation: Union[str, None], annotation: str | None,
) -> None: ) -> None:
assert_that_is_instance(name, str) assert_that_is_instance(name, str)
assert_that_is_instance(kind, PythonArgumentKind) assert_that_is_instance(kind, PythonArgumentKind)
@@ -68,7 +71,7 @@ class PythonFunctionArgument:
if not name or not name.isidentifier() or keyword.iskeyword(name): if not name or not name.isidentifier() or keyword.iskeyword(name):
raise InvalidPythonIdentifierError( raise InvalidPythonIdentifierError(
'invalid identifier', 'invalid identifier',
identifier = name, identifier=name,
) )
# check whether the annotation is a valid annotation # check whether the annotation is a valid annotation
@@ -77,7 +80,7 @@ class PythonFunctionArgument:
if annotation is not None and not annotation: if annotation is not None and not annotation:
raise InvalidPythonAnnotationError( raise InvalidPythonAnnotationError(
'invalid annotation', 'invalid annotation',
annotation = annotation, annotation=annotation,
) )
self.name = name self.name = name
@@ -85,13 +88,17 @@ class PythonFunctionArgument:
self.default = default self.default = default
self.annotation = annotation self.annotation = annotation
if self.kind in ( if (
PythonArgumentKind.VARARG, self.kind
PythonArgumentKind.KWARGS, in (
) and self.default is not None: PythonArgumentKind.VARARG,
PythonArgumentKind.KWARGS,
)
and self.default is not None
):
raise PythonArgumentStructureError( raise PythonArgumentStructureError(
'* or ** arguments cannot have default values', '* or ** arguments cannot have default values',
definition = self.signature_repr(), definition=self.signature_repr(),
) )
def __eq__(self, value: PythonFunctionArgument) -> bool: def __eq__(self, value: PythonFunctionArgument) -> bool:
@@ -105,20 +112,22 @@ class PythonFunctionArgument:
) )
def __repr__(self) -> str: def __repr__(self) -> str:
return type(self).__name__ + repr(( return type(self).__name__ + repr(
self.name, (
self.kind, self.name,
self.default, self.kind,
self.annotation, self.default,
)) self.annotation,
)
)
def signature_repr(self) -> str: def signature_repr(self) -> str:
''' """
Returns the function signature representation of the argument. Returns the function signature representation of the argument.
:return: The string representation of the argument. :return: The string representation of the argument.
:rtype: str :rtype: str
''' """
if self.kind == PythonArgumentKind.KWARGS: if self.kind == PythonArgumentKind.KWARGS:
str_repr = '**' str_repr = '**'
@@ -137,48 +146,44 @@ class PythonFunctionArgument:
return str_repr return str_repr
class PythonASTArgumentsListParser: class PythonASTArgumentsListParser:
''' """
A parser for making ``PythonFunctionArgument`` objects from an A parser for making ``PythonFunctionArgument`` objects from an
``ast.arguments`` arguments list. ``ast.arguments`` arguments list.
:param ast_arguments_list: The ``ast.arguments`` arguments list. :param ast_arguments_list: The ``ast.arguments`` arguments list.
:type ast_arguments_list: ast.arguments :type ast_arguments_list: ast.arguments
''' """
def __init__(self, ast_arguments_list: ast.arguments) -> None: def __init__(self, ast_arguments_list: ast.arguments) -> None:
self.ast_arguments_list = ast_arguments_list self.ast_arguments_list = ast_arguments_list
def to_argument_list(self) -> list[PythonFunctionArgument]: def to_argument_list(self) -> list[PythonFunctionArgument]:
''' """
Converts the AST arguments list to a list of Python function Converts the AST arguments list to a list of Python function
argument objects. argument objects.
:return: The list of Python function argument objects. :return: The list of Python function argument objects.
:rtype: list[PythonFunctionArgument] :rtype: list[PythonFunctionArgument]
''' """
defaults = ( defaults = (
[ [
None None
for _ in range(len( for _ in range(
self.ast_arguments_list.posonlyargs + len(
self.ast_arguments_list.args + self.ast_arguments_list.posonlyargs
self.ast_arguments_list.kwonlyargs + self.ast_arguments_list.args
) - len( + self.ast_arguments_list.kwonlyargs
self.ast_arguments_list.defaults )
) - len( - len(self.ast_arguments_list.defaults)
self.ast_arguments_list.kw_defaults - len(self.ast_arguments_list.kw_defaults)
)) )
] ]
+ [ast.unparse(d) for d in self.ast_arguments_list.defaults]
+ [ + [
ast.unparse(d) ast.unparse(d) if d is not None else None
for d in self.ast_arguments_list.defaults
]
+ [
ast.unparse(d)
if d is not None
else None
for d in self.ast_arguments_list.kw_defaults for d in self.ast_arguments_list.kw_defaults
] ]
) )
+41 -39
View File
@@ -13,14 +13,15 @@
# limitations under the License. # limitations under the License.
from __future__ import annotations from __future__ import annotations
from typing import Union
import ast import ast
from .arguments import PythonFunctionArgument, PythonASTArgumentsListParser
from .docstrings import PythonDocstring
import keyword import keyword
import types
from ...exceptions import InvalidPythonIdentifierError from ...exceptions import InvalidPythonIdentifierError
from ...utils import assert_that_is_instance from ...utils import assert_that_is_instance
import types from .arguments import PythonASTArgumentsListParser, PythonFunctionArgument
from .docstrings import PythonDocstring
__all__ = [ __all__ = [
'PythonDefinition', 'PythonDefinition',
@@ -29,46 +30,48 @@ __all__ = [
'PythonClassDefinition', 'PythonClassDefinition',
] ]
def _python_docstring(doc: Union[str, None]) -> Union[PythonDocstring, None]:
''' def _python_docstring(doc: str | None) -> PythonDocstring | None:
"""
Returns the PythonDocstring object if the docstring is a string and Returns the PythonDocstring object if the docstring is a string and
not ``None``. not ``None``.
:param doc: The docstring. :param doc: The docstring.
:type doc: Union[str, None] :type doc: str | None
:return: The PythonDocstring object or ``None``. :return: The PythonDocstring object or ``None``.
:rtype: Union[PythonDocstring, None] :rtype: PythonDocstring | None
''' """
return PythonDocstring(doc) if doc is not None else None return PythonDocstring(doc) if doc is not None else None
class PythonDefinition: class PythonDefinition:
''' """
A base class for Python definition (a class, function or asynchronous A base class for Python definition (a class, function or asynchronous
function definition) classes. function definition) classes.
:param name: The name of the definition. :param name: The name of the definition.
:type name: str :type name: str
:param doc: The docstring. :param doc: The docstring.
:type doc: Union[PythonDocstring, None] :type doc: PythonDocstring | None
:param decorators: The definition decorators. :param decorators: The definition decorators.
:type decorators: list[str] :type decorators: list[str]
:raises InvalidPythonIdentifierError: If the name is not a valid :raises InvalidPythonIdentifierError: If the name is not a valid
Python identifier. Python identifier.
''' """
def __init__( def __init__(
self, self,
name: str, name: str,
doc: Union[PythonDocstring, None], doc: PythonDocstring | None,
decorators: list[str], decorators: list[str],
) -> None: ) -> None:
if keyword.iskeyword(name) or not name.isidentifier() or not name: if keyword.iskeyword(name) or not name.isidentifier() or not name:
raise InvalidPythonIdentifierError( raise InvalidPythonIdentifierError(
'invalid identifier', 'invalid identifier',
identifier = name, identifier=name,
) )
assert_that_is_instance(doc, (PythonDocstring, types.NoneType)) assert_that_is_instance(doc, (PythonDocstring, types.NoneType))
@@ -89,12 +92,12 @@ class PythonDefinition:
@classmethod @classmethod
def from_node(cls, node: ast.stmt) -> PythonDefinition: def from_node(cls, node: ast.stmt) -> PythonDefinition:
''' """
From an AST node. From an AST node.
:param node: The AST node. :param node: The AST node.
:type node: ast.stmt :type node: ast.stmt
''' """
return cls( return cls(
node.name, node.name,
@@ -107,8 +110,9 @@ class PythonDefinition:
return False return False
return self._arg_list() == value._arg_list() return self._arg_list() == value._arg_list()
class PythonFunctionDefinition(PythonDefinition): class PythonFunctionDefinition(PythonDefinition):
''' """
A PythonDefinition subclass representing a Python function A PythonDefinition subclass representing a Python function
definition. definition.
@@ -117,22 +121,22 @@ class PythonFunctionDefinition(PythonDefinition):
:param args: The arguments. :param args: The arguments.
:type args: list[PythonFunctionArgument] :type args: list[PythonFunctionArgument]
:param returns: The return type of the function or class. :param returns: The return type of the function or class.
:type returns: Union[str, None] :type returns: str | None
:param doc: The docstring. :param doc: The docstring.
:type doc: Union[PythonDocstring, None] :type doc: PythonDocstring | None
:param decorators: The definition decorators. :param decorators: The definition decorators.
:type decorators: list[str] :type decorators: list[str]
:raises InvalidPythonIdentifierError: If the name is not a valid :raises InvalidPythonIdentifierError: If the name is not a valid
Python identifier. Python identifier.
''' """
def __init__( def __init__(
self, self,
name: str, name: str,
args: list[PythonFunctionArgument], args: list[PythonFunctionArgument],
returns: Union[str, None], returns: str | None,
doc: Union[PythonDocstring, None], doc: PythonDocstring | None,
decorators: list[str], decorators: list[str],
) -> None: ) -> None:
super().__init__(name, doc, decorators) super().__init__(name, doc, decorators)
@@ -150,12 +154,12 @@ class PythonFunctionDefinition(PythonDefinition):
@classmethod @classmethod
def from_node(cls, node: ast.stmt) -> PythonFunctionDefinition: def from_node(cls, node: ast.stmt) -> PythonFunctionDefinition:
''' """
From an AST node. From an AST node.
:param node: The AST node. :param node: The AST node.
:type node: ast.stmt :type node: ast.stmt
''' """
return cls( return cls(
node.name, node.name,
@@ -165,8 +169,9 @@ class PythonFunctionDefinition(PythonDefinition):
[ast.unparse(d) for d in node.decorator_list], [ast.unparse(d) for d in node.decorator_list],
) )
class PythonAsyncFunctionDefinition(PythonFunctionDefinition): class PythonAsyncFunctionDefinition(PythonFunctionDefinition):
''' """
A PythonDefinition subclass representing an asynchronous Python function A PythonDefinition subclass representing an asynchronous Python function
definition. definition.
@@ -175,18 +180,19 @@ class PythonAsyncFunctionDefinition(PythonFunctionDefinition):
:param args: The arguments. :param args: The arguments.
:type args: list[PythonFunctionArgument] :type args: list[PythonFunctionArgument]
:param returns: The return type of the function or class. :param returns: The return type of the function or class.
:type returns: Union[str, None] :type returns: str | None
:param doc: The docstring. :param doc: The docstring.
:type doc: Union[PythonDocstring, None] :type doc: PythonDocstring | None
:param decorators: The definition decorators. :param decorators: The definition decorators.
:type decorators: list[str] :type decorators: list[str]
:raises InvalidPythonIdentifierError: If the name is not a valid :raises InvalidPythonIdentifierError: If the name is not a valid
Python identifier. Python identifier.
''' """
class PythonClassDefinition(PythonDefinition): class PythonClassDefinition(PythonDefinition):
''' """
A PythonDefinition subclass representing a Python class definition. A PythonDefinition subclass representing a Python class definition.
:param name: The name of the definition. :param name: The name of the definition.
@@ -194,7 +200,7 @@ class PythonClassDefinition(PythonDefinition):
:param bases: The base classes. :param bases: The base classes.
:type bases: list[str] :type bases: list[str]
:param doc: The docstring. :param doc: The docstring.
:type doc: Union[PythonDocstring, None] :type doc: PythonDocstring | None
:param decorators: The definition decorators. :param decorators: The definition decorators.
:type decorators: list[str] :type decorators: list[str]
:param body: :param body:
@@ -202,13 +208,13 @@ class PythonClassDefinition(PythonDefinition):
:raises InvalidPythonIdentifierError: If the name is not a valid :raises InvalidPythonIdentifierError: If the name is not a valid
Python identifier. Python identifier.
''' """
def __init__( def __init__(
self, self,
name: str, name: str,
bases: list[str], bases: list[str],
doc: Union[PythonDocstring, None], doc: PythonDocstring | None,
decorators: list[str], decorators: list[str],
body: list[PythonDefinition], body: list[PythonDefinition],
) -> None: ) -> None:
@@ -227,12 +233,12 @@ class PythonClassDefinition(PythonDefinition):
@classmethod @classmethod
def from_node(cls, node: ast.stmt) -> PythonClassDefinition: def from_node(cls, node: ast.stmt) -> PythonClassDefinition:
''' """
From an AST node. From an AST node.
:param node: The AST node. :param node: The AST node.
:type node: ast.stmt :type node: ast.stmt
''' """
return cls( return cls(
node.name, node.name,
@@ -242,16 +248,12 @@ class PythonClassDefinition(PythonDefinition):
[ [
PythonFunctionDefinition.from_node(expr) PythonFunctionDefinition.from_node(expr)
if isinstance(expr, ast.FunctionDef) if isinstance(expr, ast.FunctionDef)
else PythonAsyncFunctionDefinition.from_node(expr) else PythonAsyncFunctionDefinition.from_node(expr)
if isinstance(expr, ast.AsyncFunctionDef) if isinstance(expr, ast.AsyncFunctionDef)
else PythonClassDefinition.from_node(expr) else PythonClassDefinition.from_node(expr)
for expr in node.body for expr in node.body
if isinstance( if isinstance(
expr, expr, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)
(ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)
) )
], ],
) )
+6 -8
View File
@@ -12,20 +12,18 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from enum import Enum
import docstring_parser import docstring_parser
__all__ = [ __all__ = ['PythonDocstring']
'PythonDocstring'
]
class PythonDocstring: class PythonDocstring:
''' """
Represents a Python docstring. Represents a Python docstring.
:param docstring: The docstring as a string. :param docstring: The docstring as a string.
:type docstring: str :type docstring: str
''' """
def __init__(self, docstring: str) -> None: def __init__(self, docstring: str) -> None:
self.docstring = docstring self.docstring = docstring
@@ -39,11 +37,11 @@ class PythonDocstring:
return self.docstring == value.docstring return self.docstring == value.docstring
def parse(self) -> docstring_parser.Docstring: def parse(self) -> docstring_parser.Docstring:
''' """
Parses the docstring. Parses the docstring.
:return: The parsed docstring. :return: The parsed docstring.
:rtype: docstring_parser.Docstring :rtype: docstring_parser.Docstring
''' """
return docstring_parser.parse(self.docstring) return docstring_parser.parse(self.docstring)
+32 -25
View File
@@ -13,13 +13,15 @@
# limitations under the License. # limitations under the License.
from __future__ import annotations from __future__ import annotations
from typing import TypeAlias, Union
from ...exceptions import ( from ...exceptions import (
InvalidNamespaceError, InvalidNamespaceError,
NamespaceNotFoundError,
NamespaceExistsError, NamespaceExistsError,
NamespaceNotFoundError,
) )
from ...utils import assert_that_is_instance from ...utils import assert_that_is_instance
from typing import Union, TypeAlias, Dict, List
__all__ = [ __all__ = [
'PythonNamespace', 'PythonNamespace',
@@ -27,20 +29,22 @@ __all__ = [
'PythonModuleNamespace', 'PythonModuleNamespace',
] ]
class PythonNamespace: class PythonNamespace:
''' """
The base class for all Python namespaces such as modules or packages. The base class for all Python namespaces such as modules or packages.
:param name: The name of the namespace. :param name: The name of the namespace.
:type name: str :type name: str
''' """
def __init__(self, name: str) -> None: def __init__(self, name: str) -> None:
assert_that_is_instance(name, str) assert_that_is_instance(name, str)
if not name.strip(): if not name.strip():
raise InvalidNamespaceError( raise InvalidNamespaceError(
'invalid namespace', 'invalid namespace',
namespace_identifier = name, namespace_identifier=name,
) )
self.name = name self.name = name
@@ -48,7 +52,8 @@ class PythonNamespace:
def __repr__(self) -> str: def __repr__(self) -> str:
return f'{type(self).__name__}({self.name!r})' return f'{type(self).__name__}({self.name!r})'
PythonPackageNamespaceDictType: TypeAlias = Dict[
PythonPackageNamespaceDictType: TypeAlias = dict[
str, str,
Union[ Union[
str, str,
@@ -56,20 +61,21 @@ PythonPackageNamespaceDictType: TypeAlias = Dict[
], ],
] ]
class PythonPackageNamespace(PythonNamespace): class PythonPackageNamespace(PythonNamespace):
''' """
A subclass of ``PythonNamespace`` for Python package namespaces. A subclass of ``PythonNamespace`` for Python package namespaces.
:param name: The name of the namespace. :param name: The name of the namespace.
:type name: str :type name: str
:param sub_namespaces: The sub-namespaces. :param sub_namespaces: The sub-namespaces.
:type sub_namespaces: list[PythonNamespace] :type sub_namespaces: list[PythonNamespace]
''' """
def __init__( def __init__(
self, self,
name: str, name: str,
sub_namespaces: List[PythonNamespace], sub_namespaces: list[PythonNamespace],
) -> None: ) -> None:
assert_that_is_instance(sub_namespaces, list) assert_that_is_instance(sub_namespaces, list)
@@ -77,8 +83,8 @@ class PythonPackageNamespace(PythonNamespace):
self._sub_namespaces = sub_namespaces self._sub_namespaces = sub_namespaces
def namespace(self, sub_namespaces: List[str]) -> PythonNamespace: def namespace(self, sub_namespaces: list[str]) -> PythonNamespace:
''' """
Returns the namespace object with a specific identifier, such as Returns the namespace object with a specific identifier, such as
``['package', 'subpackage', 'module']``. ``['package', 'subpackage', 'module']``.
@@ -91,7 +97,7 @@ class PythonPackageNamespace(PythonNamespace):
:return: The namespace. :return: The namespace.
:rtype: PythonNamespace :rtype: PythonNamespace
''' """
if not sub_namespaces: if not sub_namespaces:
raise InvalidNamespaceError('invalid namespace') raise InvalidNamespaceError('invalid namespace')
@@ -115,7 +121,7 @@ class PythonPackageNamespace(PythonNamespace):
self, self,
package: PythonPackageNamespace, package: PythonPackageNamespace,
) -> PythonPackageNamespaceDictType: ) -> PythonPackageNamespaceDictType:
''' """
Returns the names of all sub-namespaces of the specified package Returns the names of all sub-namespaces of the specified package
namespace and keeps the package structure. namespace and keeps the package structure.
@@ -124,7 +130,7 @@ class PythonPackageNamespace(PythonNamespace):
:return: The names if all sub-namespaces. :return: The names if all sub-namespaces.
:rtype: PythonPackageNamespaceDictType :rtype: PythonPackageNamespaceDictType
''' """
return { return {
sn.name: ( sn.name: (
@@ -136,13 +142,13 @@ class PythonPackageNamespace(PythonNamespace):
} }
def sub_namespace_names(self) -> PythonPackageNamespaceDictType: def sub_namespace_names(self) -> PythonPackageNamespaceDictType:
''' """
Returns the names of all sub-namespaces and keeps the package Returns the names of all sub-namespaces and keeps the package
structure. structure.
:return: The names if all sub-namespaces. :return: The names if all sub-namespaces.
:rtype: PythonPackageNamespaceDictType :rtype: PythonPackageNamespaceDictType
''' """
return self._sub_namespaces_names(self) return self._sub_namespaces_names(self)
@@ -159,7 +165,7 @@ class PythonPackageNamespace(PythonNamespace):
return type(self).__name__ + repr((self.name, self._sub_namespaces)) return type(self).__name__ + repr((self.name, self._sub_namespaces))
def __getitem__(self, name: str) -> PythonNamespace: def __getitem__(self, name: str) -> PythonNamespace:
''' """
Returns a namespace with the specified name. Returns a namespace with the specified name.
:param name: The name of the namespace :param name: The name of the namespace
@@ -169,7 +175,7 @@ class PythonPackageNamespace(PythonNamespace):
:return: The namespace :return: The namespace
:rtype: PythonNamespace :rtype: PythonNamespace
''' """
for sub in self._sub_namespaces: for sub in self._sub_namespaces:
if sub.name == name: if sub.name == name:
@@ -177,11 +183,11 @@ class PythonPackageNamespace(PythonNamespace):
raise NamespaceNotFoundError( raise NamespaceNotFoundError(
'no such namespace', 'no such namespace',
namespace_identifier = name, namespace_identifier=name,
) )
def add(self, namespace: PythonNamespace) -> None: def add(self, namespace: PythonNamespace) -> None:
''' """
Adds a namespace. Adds a namespace.
:param namespace: The namespace to add. :param namespace: The namespace to add.
@@ -190,27 +196,28 @@ class PythonPackageNamespace(PythonNamespace):
:raises NamespaceExistsError: If a namespace with the name of the :raises NamespaceExistsError: If a namespace with the name of the
namespace that is attempted to add namespace that is attempted to add
already exists. already exists.
''' """
try: try:
self.__getitem__(namespace.name) self.__getitem__(namespace.name)
raise NamespaceExistsError( raise NamespaceExistsError(
'namespace already exists', 'namespace already exists',
namespace_identifier = namespace.name, namespace_identifier=namespace.name,
) )
except NamespaceNotFoundError: except NamespaceNotFoundError:
self._sub_namespaces.append(namespace) self._sub_namespaces.append(namespace)
@property @property
def sub_namespaces(self) -> List[PythonNamespace]: def sub_namespaces(self) -> list[PythonNamespace]:
return self._sub_namespaces return self._sub_namespaces
class PythonModuleNamespace(PythonNamespace): class PythonModuleNamespace(PythonNamespace):
''' """
A subclass of ``PythonNamespace`` for Python module namespaces. A subclass of ``PythonNamespace`` for Python module namespaces.
:param name: The name of the namespace. :param name: The name of the namespace.
:type name: str :type name: str
''' """
def __init__(self, name: str) -> None: def __init__(self, name: str) -> None:
super().__init__(name) super().__init__(name)
+32 -40
View File
@@ -12,10 +12,12 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
class NamespaceError(ValueError): class NamespaceError(ValueError):
''' """
Base class for namespace errors. Base class for namespace errors.
''' """
def __init__(self, *args: object, namespace_identifier: str = '') -> None: def __init__(self, *args: object, namespace_identifier: str = '') -> None:
super().__init__(*args) super().__init__(*args)
self.namespace_identifier = namespace_identifier self.namespace_identifier = namespace_identifier
@@ -25,23 +27,24 @@ class NamespaceError(ValueError):
return '' return ''
else: else:
return f'{self.args[0]}{ return f'{self.args[0]}{
": " ": " if self.namespace_identifier and self.args[0] else ""
if self.namespace_identifier and self.args[0] }{self.namespace_identifier if self.args[0] else ""}'
else ""
}{
self.namespace_identifier
if self.args[0]
else ""
}'
class InvalidNamespaceError(NamespaceError): ... class InvalidNamespaceError(NamespaceError): ...
class NamespaceNotFoundError(NamespaceError): ... class NamespaceNotFoundError(NamespaceError): ...
class NamespaceExistsError(NamespaceError): ... class NamespaceExistsError(NamespaceError): ...
class PythonIdentifierError(ValueError): class PythonIdentifierError(ValueError):
''' """
Base class for Python identifier errors. Base class for Python identifier errors.
''' """
def __init__(self, *args: object, identifier: str = '') -> None: def __init__(self, *args: object, identifier: str = '') -> None:
super().__init__(*args) super().__init__(*args)
self.identifier = identifier self.identifier = identifier
@@ -51,21 +54,18 @@ class PythonIdentifierError(ValueError):
return '' return ''
else: else:
return f'{self.args[0]}{ return f'{self.args[0]}{
": " ": " if self.identifier and self.args[0] else ""
if self.identifier and self.args[0] }{self.identifier if self.args[0] else ""}'
else ""
}{
self.identifier
if self.args[0]
else ""
}'
class InvalidPythonIdentifierError(PythonIdentifierError): ... class InvalidPythonIdentifierError(PythonIdentifierError): ...
class PythonAnnotationError(ValueError): class PythonAnnotationError(ValueError):
''' """
Base class for Python annotation errors. Base class for Python annotation errors.
''' """
def __init__(self, *args: object, annotation: str = '') -> None: def __init__(self, *args: object, annotation: str = '') -> None:
super().__init__(*args) super().__init__(*args)
self.annotation = annotation self.annotation = annotation
@@ -75,21 +75,18 @@ class PythonAnnotationError(ValueError):
return '' return ''
else: else:
return f'{self.args[0]}{ return f'{self.args[0]}{
": " ": " if self.annotation and self.args[0] else ""
if self.annotation and self.args[0] }{self.annotation if self.args[0] else ""}'
else ""
}{
self.annotation
if self.args[0]
else ""
}'
class InvalidPythonAnnotationError(PythonAnnotationError): ... class InvalidPythonAnnotationError(PythonAnnotationError): ...
class PythonDefinitionError(ValueError): class PythonDefinitionError(ValueError):
''' """
Base class for Python definition errors. Base class for Python definition errors.
''' """
def __init__(self, *args: object, definition: str = '') -> None: def __init__(self, *args: object, definition: str = '') -> None:
super().__init__(*args) super().__init__(*args)
self.definition = definition self.definition = definition
@@ -99,13 +96,8 @@ class PythonDefinitionError(ValueError):
return '' return ''
else: else:
return f'{self.args[0]}{ return f'{self.args[0]}{
": " ": " if self.definition and self.args[0] else ""
if self.definition and self.args[0] }{self.definition if self.args[0] else ""}'
else ""
}{
self.definition
if self.args[0]
else ""
}'
class PythonArgumentStructureError(PythonDefinitionError): ... class PythonArgumentStructureError(PythonDefinitionError): ...
+58 -43
View File
@@ -12,30 +12,32 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
''' """
The utilities for jcloud_docsgen The utilities for jcloud_docsgen
''' """
import pathlib
import os
import errno import errno
from typing import Union import os
import pathlib
import types import types
from typing import Union
try: try:
from warnings import deprecated from warnings import deprecated
except ImportError: except ImportError:
from typing_extensions import deprecated from typing_extensions import deprecated
@deprecated('Use pathlib.Path instead.') @deprecated('Use pathlib.Path instead.')
class ExistingDirectory(pathlib.Path): class ExistingDirectory(pathlib.Path):
''' """
pathlib.Path subclass that is a directory. pathlib.Path subclass that is a directory.
ExistingDirectory represents a filesystem path but it also checks ExistingDirectory represents a filesystem path but it also checks
whether the path exists and is not a file but a directory. It whether the path exists and is not a file but a directory. It
inherits from pathlib.Path. inherits from pathlib.Path.
''' """
def __init__(self, path, *args, **kwargs) -> None: def __init__(self, path, *args, **kwargs) -> None:
if not os.path.exists(path): if not os.path.exists(path):
raise FileNotFoundError( raise FileNotFoundError(
@@ -47,15 +49,17 @@ class ExistingDirectory(pathlib.Path):
raise NotADirectoryError(errno.ENOTDIR, 'not a directory', path) raise NotADirectoryError(errno.ENOTDIR, 'not a directory', path)
super().__init__(*(path, *args), **kwargs) super().__init__(*(path, *args), **kwargs)
@deprecated('Use pathlib.Path instead.') @deprecated('Use pathlib.Path instead.')
class ExistingFile(pathlib.Path): class ExistingFile(pathlib.Path):
''' """
pathlib.Path subclass that is a file. pathlib.Path subclass that is a file.
ExistingFile represents a filesystem path but it also checks whether ExistingFile represents a filesystem path but it also checks whether
the path exists and is not a directory but a file. It inherits from the path exists and is not a directory but a file. It inherits from
pathlib.Path. pathlib.Path.
''' """
def __init__(self, path, *args, **kwargs) -> None: def __init__(self, path, *args, **kwargs) -> None:
if not os.path.exists(path): if not os.path.exists(path):
raise FileNotFoundError( raise FileNotFoundError(
@@ -67,8 +71,9 @@ class ExistingFile(pathlib.Path):
raise IsADirectoryError(errno.EISDIR, 'is a directory', path) raise IsADirectoryError(errno.EISDIR, 'is a directory', path)
super().__init__(*(path, *args), **kwargs) super().__init__(*(path, *args), **kwargs)
def _quote(string: str, quotation_mark: str) -> str: def _quote(string: str, quotation_mark: str) -> str:
''' """
Quotes a string. Quotes a string.
:param string: The string to quote :param string: The string to quote
@@ -78,16 +83,17 @@ def _quote(string: str, quotation_mark: str) -> str:
:return: The quoted string :return: The quoted string
:rtype: str :rtype: str
''' """
return quotation_mark + string + quotation_mark return quotation_mark + string + quotation_mark
def human_readable_list( def human_readable_list(
ls: list, ls: list,
final_separator: str = 'and', final_separator: str = 'and',
quotation_mark: str = '', quotation_mark: str = '',
) -> str: ) -> str:
''' """
Generates a human-readable list from a list. Generates a human-readable list from a list.
:param ls: The list. :param ls: The list.
@@ -101,7 +107,7 @@ def human_readable_list(
:return: The human-readable list. :return: The human-readable list.
:rtype: str :rtype: str
''' """
# empty list # empty list
if not ls: if not ls:
@@ -111,18 +117,26 @@ def human_readable_list(
if len(ls) == 1: if len(ls) == 1:
return _quote(str(ls[0]), quotation_mark) return _quote(str(ls[0]), quotation_mark)
return ', '.join( return (
[ ', '.join(
_quote( [
str(obj), _quote(
quotation_mark str(obj), quotation_mark
) # quote the string representation of the object ) # 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
) + ' ' + final_separator + ' ' + _quote(str(ls[-1]), quotation_mark) ] # do not include the last element of the list
],
)
+ ' '
+ final_separator
+ ' '
+ _quote(str(ls[-1]), quotation_mark)
)
def _list_type_names(types: list[type]) -> list[str]: def _list_type_names(types: list[type]) -> list[str]:
''' """
Converts a list of types into a list of their names (``__name__`` Converts a list of types into a list of their names (``__name__``
attribute) attribute)
@@ -131,22 +145,19 @@ def _list_type_names(types: list[type]) -> list[str]:
:return: The list of the names of the types :return: The list of the names of the types
:rtype: list[str] :rtype: list[str]
''' """
return [tp.__name__ for tp in types] return [tp.__name__ for tp in types]
def assert_that_is_instance( def assert_that_is_instance(
obj: object, obj: object,
class_or_tuple: Union[ class_or_tuple: type | types.UnionType | tuple[type, ...],
type,
types.UnionType,
tuple[type, ...],
],
) -> None: ) -> None:
if not isinstance(class_or_tuple, Union[type, types.UnionType, tuple]): if not isinstance(class_or_tuple, Union[type, types.UnionType, tuple]):
raise TypeError( raise TypeError(
f'class_or_tuple: expected \'Union[type, types.UnionType, ' f"class_or_tuple: expected 'Union[type, types.UnionType, "
f'tuple[type, ...]]\', \'got {type(class_or_tuple).__name__}\'', f"tuple[type, ...]]', 'got {type(class_or_tuple).__name__}'",
) )
if not isinstance(obj, class_or_tuple): if not isinstance(obj, class_or_tuple):
if isinstance(class_or_tuple, (tuple, types.UnionType)): if isinstance(class_or_tuple, (tuple, types.UnionType)):
@@ -157,18 +168,21 @@ def assert_that_is_instance(
else: else:
exception_message_expected = '' exception_message_expected = ''
exception_message_expected += human_readable_list( exception_message_expected += human_readable_list(
_list_type_names(class_or_tuple), 'or', '\'', _list_type_names(class_or_tuple),
'or',
"'",
) )
else: else:
exception_message_expected = '\'' + class_or_tuple.__name__ + '\'' exception_message_expected = "'" + class_or_tuple.__name__ + "'"
raise TypeError( raise TypeError(
f'expected {exception_message_expected}, got \'{type( f"expected {exception_message_expected}, got '{
obj type(obj).__name__
).__name__}\'', }'",
) )
def non_empty_str(value: Union[str, None]) -> Union[str, None]:
''' def non_empty_str(value: str | None) -> str | None:
"""
Returns the value or ``None``. Returns the value or ``None``.
Returns the value if it is not empty and not `Ǹone``; otherwise, Returns the value if it is not empty and not `Ǹone``; otherwise,
@@ -177,11 +191,11 @@ def non_empty_str(value: Union[str, None]) -> Union[str, None]:
of spaces, it is also considered empty. of spaces, it is also considered empty.
:param value: The value. :param value: The value.
:type value: Union[str, None] :type value: str | None
:return: The value or ``None``. :return: The value or ``None``.
:rtype: Union[str, None] :rtype: str | None
''' """
if value is None: if value is None:
return None return None
@@ -191,11 +205,12 @@ def non_empty_str(value: Union[str, None]) -> Union[str, None]:
return value return value
def get_relative_path_segments( def get_relative_path_segments(
path: pathlib.Path, path: pathlib.Path,
root: pathlib.Path, root: pathlib.Path,
) -> list[str]: ) -> list[str]:
''' """
Returns the path segments relative to ``root``. Returns the path segments relative to ``root``.
:param path: The path. :param path: The path.
@@ -205,7 +220,7 @@ def get_relative_path_segments(
:return: The path segments relative to ``root``. :return: The path segments relative to ``root``.
:rtype: list[str] :rtype: list[str]
''' """
return list( return list(
path.resolve() path.resolve()
+9 -7
View File
@@ -15,6 +15,7 @@
import subprocess import subprocess
import sys import sys
def test_cli(): def test_cli():
result = subprocess.run( result = subprocess.run(
[ [
@@ -23,12 +24,13 @@ def test_cli():
'jcloud_docsgen', 'jcloud_docsgen',
'--help', '--help',
], ],
capture_output = True, capture_output=True,
text = True, text=True,
) )
stdout = result.stdout.lower() stdout = result.stdout.lower()
assert \ assert (
'usage:' in stdout and \ 'usage:' in stdout
'positional arguments' in stdout and \ and 'positional arguments' in stdout
'options' in stdout and \ and 'options' in stdout
'--help' in stdout and '--help' in stdout
)
@@ -1,5 +1,5 @@
def f(a: int = 42) -> str: def f(a: int = 42) -> str:
''' """
Does something. Does something.
:param a: A parameter. :param a: A parameter.
@@ -7,5 +7,5 @@ def f(a: int = 42) -> str:
:return: A string. :return: A string.
:rtype: str :rtype: str
''' """
... ...
+17 -8
View File
@@ -13,22 +13,31 @@
# limitations under the License. # limitations under the License.
import pytest import pytest
from jcloud_docsgen.cli.cli import create_argument_parser from jcloud_docsgen.cli.cli import create_argument_parser
@pytest.mark.parametrize('lang', [
'javascript', @pytest.mark.parametrize(
'Python', 'lang',
'PYTHON', [
]) 'javascript',
'Python',
'PYTHON',
],
)
def test_language_error(lang): def test_language_error(lang):
parser = create_argument_parser() parser = create_argument_parser()
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
parser.parse_args([lang]) parser.parse_args([lang])
@pytest.mark.parametrize('lang', [
'python', @pytest.mark.parametrize(
]) 'lang',
[
'python',
],
)
def test_language_success(lang): def test_language_success(lang):
parser = create_argument_parser() parser = create_argument_parser()
parser.parse_args([lang]) parser.parse_args([lang])
+144 -124
View File
@@ -13,156 +13,176 @@
# limitations under the License. # limitations under the License.
import pathlib import pathlib
import pytest import pytest
from jcloud_docsgen.cli.cli import create_argument_parser, main as cli_main
from jcloud_docsgen.cli.cli import create_argument_parser
from jcloud_docsgen.cli.cli import main as cli_main
from jcloud_docsgen.cli.languages.python import parse_sections_list from jcloud_docsgen.cli.languages.python import parse_sections_list
from jcloud_docsgen.core.python import ( 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, PythonDefinitionDocumentationIncludeSections(
exceptions = False, signature=False,
examples = False, returns=False,
docstring = False, params=False,
bases = False, exceptions=False,
decorators = False, examples=False,
class_body = False, docstring=False,
bases=False,
decorators=False,
class_body=False,
),
), ),
), (
( [],
[], ['signature'],
['signature'], PythonDefinitionDocumentationIncludeSections(
PythonDefinitionDocumentationIncludeSections( signature=False,
signature = False, returns=False,
returns = False, params=False,
params = False, exceptions=False,
exceptions = False, examples=False,
examples = False, docstring=False,
docstring = False, bases=False,
bases = False, decorators=False,
decorators = False, class_body=False,
class_body = False, ),
), ),
), (
( ['signature'],
['signature'], ['signature'],
['signature'], PythonDefinitionDocumentationIncludeSections(
PythonDefinitionDocumentationIncludeSections( signature=False,
signature = False, returns=False,
returns = False, params=False,
params = False, exceptions=False,
exceptions = False, examples=False,
examples = False, docstring=False,
docstring = False, bases=False,
bases = False, decorators=False,
decorators = False, class_body=False,
class_body = False, ),
), ),
), (
( ['signature'],
['signature'], [],
[], PythonDefinitionDocumentationIncludeSections(
PythonDefinitionDocumentationIncludeSections( signature=True,
signature = True, returns=False,
returns = False, params=False,
params = False, exceptions=False,
exceptions = False, examples=False,
examples = False, docstring=False,
docstring = False, bases=False,
bases = False, decorators=False,
decorators = False, class_body=False,
class_body = False, ),
), ),
), (
( None,
None, [],
[], PythonDefinitionDocumentationIncludeSections(),
PythonDefinitionDocumentationIncludeSections(),
),
(
['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', 'exceptions', 'bases', 'class_body'],
None, ['signature', 'bases'],
['params', 'docstring'], PythonDefinitionDocumentationIncludeSections(
PythonDefinitionDocumentationIncludeSections( signature=False,
signature = True, returns=False,
returns = True, params=False,
params = False, exceptions=True,
exceptions = True, examples=False,
examples = True, docstring=False,
docstring = False, bases=False,
bases = True, decorators=False,
decorators = True, class_body=True,
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,
),
),
],
)
def test_parse_sections_list(include, exclude, expected): def test_parse_sections_list(include, exclude, expected):
assert parse_sections_list(include, exclude) == expected assert parse_sections_list(include, exclude) == expected
def test_sections_error(): def test_sections_error():
parser = create_argument_parser() parser = create_argument_parser()
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
parser.parse_args([ parser.parse_args(
'python', [
'-s', 'python',
'param', # correct: `params` '-s',
]) 'param', # correct: `params`
]
)
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
parser.parse_args([ parser.parse_args(
'python', [
'-s', 'python',
'params', '-s',
'docstrng', # correct: `docstring` 'params',
]) 'docstrng', # correct: `docstring`
]
)
def test_sections_success(): def test_sections_success():
parser = create_argument_parser() parser = create_argument_parser()
parser.parse_args([ parser.parse_args(
'python', [
'-s', 'python',
'params', '-s',
]) 'params',
parser.parse_args([ ]
'python', )
'-s', parser.parse_args(
'params', [
'docstring', 'python',
]) '-s',
'params',
'docstring',
]
)
def test_docs_generation(expectdir): def test_docs_generation(expectdir):
with expectdir('python_project') as output_dir: with expectdir('python_project') as output_dir:
output_dir: pathlib.Path output_dir: pathlib.Path
input_dir = pathlib.Path(__file__).parent / 'python_project/project' input_dir = pathlib.Path(__file__).parent / 'python_project/project'
cli_main([ cli_main(
'python', [
'-s', 'python',
'params', '-s',
'docstring', 'params',
'-HTo', 'docstring',
str(output_dir), '-HTo',
str(input_dir), str(output_dir),
]) str(input_dir),
]
)
File diff suppressed because it is too large. Load diff
@@ -12,86 +12,92 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import pathlib
import pytest
from src.jcloud_docsgen.core.python import PythonDocumentationGenerator from src.jcloud_docsgen.core.python import PythonDocumentationGenerator
from src.jcloud_docsgen.core.python.namespaces import ( from src.jcloud_docsgen.core.python.namespaces import (
PythonModuleNamespace, PythonModuleNamespace,
PythonPackageNamespace, PythonPackageNamespace,
) )
import pytest
import pathlib
@pytest.mark.parametrize('python_documentation_generator,expected', [
( @pytest.mark.parametrize(
PythonDocumentationGenerator( 'python_documentation_generator,expected',
pathlib.Path( [
'tests/unit/core/python/_core/test_project_dirs/pdir_1' (
), PythonDocumentationGenerator(
pathlib.Path( pathlib.Path(
'tests/unit/core/python/_core/test_project_dirs/pdir_1/docs' 'tests/unit/core/python/_core/test_project_dirs/pdir_1'
),
pathlib.Path(
'tests/unit/core/python/_core/test_project_dirs/pdir_1/docs'
),
), ),
[],
), ),
[], (
), PythonDocumentationGenerator(
( pathlib.Path(
PythonDocumentationGenerator( 'tests/unit/core/python/_core/test_project_dirs/pdir_2'
pathlib.Path( ),
'tests/unit/core/python/_core/test_project_dirs/pdir_2' pathlib.Path(
), 'tests/unit/core/python/_core/test_project_dirs/pdir_2/docs'
pathlib.Path( ),
'tests/unit/core/python/_core/test_project_dirs/pdir_2/docs'
), ),
[
PythonPackageNamespace(
'pkg',
[PythonModuleNamespace('module')],
),
],
), ),
[ (
PythonPackageNamespace( PythonDocumentationGenerator(
'pkg', pathlib.Path(
[ 'tests/unit/core/python/_core/test_project_dirs/pdir_3'
PythonModuleNamespace('module') ),
], pathlib.Path(
), 'tests/unit/core/python/_core/test_project_dirs/pdir_3/docs'
], ),
),
(
PythonDocumentationGenerator(
pathlib.Path(
'tests/unit/core/python/_core/test_project_dirs/pdir_3'
),
pathlib.Path(
'tests/unit/core/python/_core/test_project_dirs/pdir_3/docs'
), ),
[
PythonPackageNamespace(
'pkg',
[
PythonModuleNamespace('module'),
PythonPackageNamespace(
'pkg',
[
PythonModuleNamespace('module2'),
],
),
],
),
],
), ),
[ ],
PythonPackageNamespace( )
'pkg',
[
PythonModuleNamespace('module'),
PythonPackageNamespace(
'pkg',
[
PythonModuleNamespace(
'module2'
),
],
),
],
),
],
),
])
def test_PythonDocumentationGenerator_collect_modules( def test_PythonDocumentationGenerator_collect_modules(
python_documentation_generator: PythonDocumentationGenerator, python_documentation_generator: PythonDocumentationGenerator,
expected, expected,
): ):
assert python_documentation_generator.namespace() == expected assert python_documentation_generator.namespace() == expected
@pytest.mark.parametrize('dir', [
d.name @pytest.mark.parametrize(
for d in pathlib.Path( 'dir',
( [
'tests/unit/core/python/_core/test_PythonDocume' d.name
'ntationGenerator/test_generate_documentation/' for d in pathlib.Path(
), (
).iterdir() 'tests/unit/core/python/_core/test_PythonDocume'
]) 'ntationGenerator/test_generate_documentation/'
),
).iterdir()
],
)
def test_generate_documentation(dir, expectdir): def test_generate_documentation(dir, expectdir):
with expectdir( with expectdir(
'test_PythonDocumentationGenerator/test_generate_documentation/' + dir 'test_PythonDocumentationGenerator/test_generate_documentation/' + dir
@@ -104,12 +110,12 @@ def test_generate_documentation(dir, expectdir):
'tests/unit/core/python/_core/test_PythonDocume' 'tests/unit/core/python/_core/test_PythonDocume'
'ntationGenerator/test_generate_documentation/' 'ntationGenerator/test_generate_documentation/'
), ),
) / )
dir / / dir
'project', / 'project',
), ),
), ),
pathlib.Path(str(d)), pathlib.Path(str(d)),
allow_tables = True, allow_tables=True,
allow_html = False, allow_html=False,
).generate_documentation() # simplify the tests ).generate_documentation() # simplify the tests
@@ -1,8 +1,10 @@
class UserNotFoundError(Exception): class UserNotFoundError(Exception):
"""User does not exist.""" """User does not exist."""
pass pass
class InvalidEmailError(Exception): class InvalidEmailError(Exception):
"""Email address is invalid.""" """Email address is invalid."""
pass
pass
@@ -7,7 +7,8 @@ class User:
""" """
Represents a user. Represents a user.
""" """
id: str id: str
name: str name: str
email: str email: str
age: Optional[int] = None age: Optional[int] = None
@@ -13,23 +13,16 @@ class UserService:
self.storage = storage self.storage = storage
def create_user( def create_user(
self, self, user_id: str, name: str, email: str, age: int | None = None
user_id: str,
name: str,
email: str,
age: int | None = None
) -> User: ) -> User:
""" """
Creates and stores a new user. Creates and stores a new user.
""" """
if not is_valid_email(email): if not is_valid_email(email):
raise InvalidEmailError(f"Invalid email: {email}") raise InvalidEmailError(f'Invalid email: {email}')
user = User( user = User(
id=user_id, id=user_id, name=normalize_name(name), email=email, age=age
name=normalize_name(name),
email=email,
age=age
) )
self.storage.add(user) self.storage.add(user)
@@ -51,4 +44,4 @@ class UserService:
try: try:
self.storage.delete(user_id) self.storage.delete(user_id)
except KeyError: except KeyError:
raise UserNotFoundError(user_id) raise UserNotFoundError(user_id)
@@ -25,4 +25,4 @@ class InMemoryUserStorage:
def list_all(self) -> List[User]: def list_all(self) -> List[User]:
"""Returns all users.""" """Returns all users."""
return list(self._users.values()) return list(self._users.values())
@@ -5,7 +5,7 @@ def is_valid_email(email: str) -> bool:
""" """
Checks whether an email address is valid. 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 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. Removes extra spaces and formats the name.
""" """
return " ".join(name.strip().split()).title() return ' '.join(name.strip().split()).title()
@@ -12,10 +12,13 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import pathlib
import pytest
from src.jcloud_docsgen.core.python import PythonModuleDocumentationGenerator from src.jcloud_docsgen.core.python import PythonModuleDocumentationGenerator
from src.jcloud_docsgen.exceptions import InvalidNamespaceError from src.jcloud_docsgen.exceptions import InvalidNamespaceError
import pathlib
import pytest
@pytest.mark.parametrize( @pytest.mark.parametrize(
'module_path,src_directory,expected_exception,expected_exception_msg', 'module_path,src_directory,expected_exception,expected_exception_msg',
@@ -216,22 +219,25 @@ def test_PythonModuleDocumentationGenerator_exceptions(
PythonModuleDocumentationGenerator(module_path, src_directory) PythonModuleDocumentationGenerator(module_path, src_directory)
assert str(exc_info.value) == expected_exception_msg assert str(exc_info.value) == expected_exception_msg
@pytest.mark.parametrize('module_documentation_generator,expected', [
( @pytest.mark.parametrize(
PythonModuleDocumentationGenerator( 'module_documentation_generator,expected',
pathlib.Path( [
( (
'tests/unit/core/python/_core/test_pr' PythonModuleDocumentationGenerator(
'oject_dirs/pdir_2/src/pkg/module.py' pathlib.Path(
(
'tests/unit/core/python/_core/test_pr'
'oject_dirs/pdir_2/src/pkg/module.py'
),
), ),
pathlib.Path(
'tests/unit/core/python/_core/test_project_dirs/pdir_2/src/'
),
allow_tables=True,
allow_html=False,
), ),
pathlib.Path( """# Module `pkg.module`
'tests/unit/core/python/_core/test_project_dirs/pdir_2/src/'
),
allow_tables = True,
allow_html = False,
),
'''# Module `pkg.module`
A test module. A test module.
@@ -398,205 +404,21 @@ Barks.
### Docstring ### Docstring
A dog.''' A dog.""",
),
(
PythonModuleDocumentationGenerator(
pathlib.Path(
(
'tests/unit/core/python/_core/test_pr'
'oject_dirs/pdir_2/src/pkg/module.py'
),
),
None,
allow_tables = True,
allow_html = False,
),
'''# Module `module`
A test module.
## Function `add`
Adds two numbers.
### Function signature
```python
add(a: int | float, b: int | float) -> int | float
```
### Returns
The sum.
Return type: `int | float`
### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `a` | `int | float` | normal | -- | The first number. |
| `b` | `int | float` | normal | -- | The second number. |
### Docstring
Adds two numbers.
:param a: The first number.
:type a: int | float
:param b: The second number.
:type b: int | float
:return: The sum.
:rtype: int | float
## Class `Animal`
A class representing an animal.
### Body
#### Constructor (method `__init__`)
##### Method signature
```python
__init__(self, name: str) -> None
```
##### Returns
Return type: `None`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
| `name` | `str` | normal | -- | -- |
#### Method `greet`
Returns a greeting.
##### Method signature
```python
greet(self)
```
##### Returns
The greeting.
Return type: `str`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
##### Docstring
Returns a greeting.
:return: The greeting.
:rtype: str
### Docstring
A class representing an animal.
:param name: Its name.
:type name: str
## Class `Dog`
A dog.
### Base classes
- `Animal`
### Body
#### Method `bark`
Barks.
##### Method signature
```python
bark(self) -> None
```
##### Returns
Return type: `None`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
##### Docstring
Barks.
### Docstring
A dog.'''
),
(
PythonModuleDocumentationGenerator(
pathlib.Path(
(
'tests/unit/core/python/_core/test_pr'
'oject_dirs/pdir_2/src/pkg/module.py'
),
),
None,
allow_tables = True,
allow_html = True,
), ),
( (
'''# Module `module` PythonModuleDocumentationGenerator(
pathlib.Path(
(
'tests/unit/core/python/_core/test_pr'
'oject_dirs/pdir_2/src/pkg/module.py'
),
),
None,
allow_tables=True,
allow_html=False,
),
"""# Module `module`
A test module. A test module.
@@ -630,8 +452,190 @@ Return type: `int | float`
<details><summary><h3 style="display:inline">Docstring</h3></summary>Adds ''' ### Docstring
'''two numbers.
Adds two numbers.
:param a: The first number.
:type a: int | float
:param b: The second number.
:type b: int | float
:return: The sum.
:rtype: int | float
## Class `Animal`
A class representing an animal.
### Body
#### Constructor (method `__init__`)
##### Method signature
```python
__init__(self, name: str) -> None
```
##### Returns
Return type: `None`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
| `name` | `str` | normal | -- | -- |
#### Method `greet`
Returns a greeting.
##### Method signature
```python
greet(self)
```
##### Returns
The greeting.
Return type: `str`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
##### Docstring
Returns a greeting.
:return: The greeting.
:rtype: str
### Docstring
A class representing an animal.
:param name: Its name.
:type name: str
## Class `Dog`
A dog.
### Base classes
- `Animal`
### Body
#### Method `bark`
Barks.
##### Method signature
```python
bark(self) -> None
```
##### Returns
Return type: `None`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
##### Docstring
Barks.
### Docstring
A dog.""",
),
(
PythonModuleDocumentationGenerator(
pathlib.Path(
(
'tests/unit/core/python/_core/test_pr'
'oject_dirs/pdir_2/src/pkg/module.py'
),
),
None,
allow_tables=True,
allow_html=True,
),
(
"""# Module `module`
A test module.
## Function `add`
Adds two numbers.
### Function signature
```python
add(a: int | float, b: int | float) -> int | float
```
### Returns
The sum.
Return type: `int | float`
### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `a` | `int | float` | normal | -- | The first number. |
| `b` | `int | float` | normal | -- | The second number. |
<details><summary><h3 style="display:inline">Docstring</h3></summary>Adds """
"""two numbers.
:param a: The first number. :param a: The first number.
:type a: int | float :type a: int | float
@@ -698,8 +702,8 @@ Return type: `str`
<details><summary><h5 style="display:inline">Docstring</h5></summary>''' <details><summary><h5 style="display:inline">Docstring</h5></summary>"""
'''Returns a greeting. """Returns a greeting.
:return: The greeting. :return: The greeting.
:rtype: str</details> :rtype: str</details>
@@ -708,8 +712,8 @@ Return type: `str`
<details><summary><h3 style="display:inline">Docstring</h3></summary>A ''' <details><summary><h3 style="display:inline">Docstring</h3></summary>A """
'''class representing an animal. """class representing an animal.
:param name: Its name. :param name: Its name.
:type name: str</details> :type name: str</details>
@@ -750,32 +754,31 @@ Return type: `None`
<details><summary><h5 style="display:inline">Docstring</h5></summary>Barks.''' <details><summary><h5 style="display:inline">Docstring</h5></summary>Barks."""
'''</details> """</details>
<details><summary><h3 style="display:inline">Docstring</h3></summary>A dog.''' <details><summary><h3 style="display:inline">Docstring</h3></summary>A dog."""
'</details>' '</details>'
),
),
(
PythonModuleDocumentationGenerator(
pathlib.Path(
(
'tests/unit/core/python/_core/test_pr'
'oject_dirs/pdir_2/src/pkg/module.py'
),
), ),
None,
allow_tables = False,
allow_html = True,
), ),
( (
'''# Module `module` PythonModuleDocumentationGenerator(
pathlib.Path(
(
'tests/unit/core/python/_core/test_pr'
'oject_dirs/pdir_2/src/pkg/module.py'
),
),
None,
allow_tables=False,
allow_html=True,
),
(
"""# Module `module`
A test module. A test module.
@@ -824,8 +827,8 @@ Return type: `int | float`
<details><summary><h3 style="display:inline">Docstring</h3></summary>Adds ''' <details><summary><h3 style="display:inline">Docstring</h3></summary>Adds """
'''two numbers. """two numbers.
:param a: The first number. :param a: The first number.
:type a: int | float :type a: int | float
@@ -913,8 +916,8 @@ Return type: `str`
<details><summary><h5 style="display:inline">Docstring</h5></summary>''' <details><summary><h5 style="display:inline">Docstring</h5></summary>"""
'''Returns a greeting. """Returns a greeting.
:return: The greeting. :return: The greeting.
:rtype: str</details> :rtype: str</details>
@@ -923,8 +926,8 @@ Return type: `str`
<details><summary><h3 style="display:inline">Docstring</h3></summary>A ''' <details><summary><h3 style="display:inline">Docstring</h3></summary>A """
'''class representing an animal. """class representing an animal.
:param name: Its name. :param name: Its name.
:type name: str</details> :type name: str</details>
@@ -971,31 +974,30 @@ Return type: `None`
<details><summary><h5 style="display:inline">Docstring</h5></summary>Barks.''' <details><summary><h5 style="display:inline">Docstring</h5></summary>Barks."""
'''</details> """</details>
<details><summary><h3 style="display:inline">Docstring</h3></summary>A ''' <details><summary><h3 style="display:inline">Docstring</h3></summary>A """
'dog.</details>' 'dog.</details>'
),
),
(
PythonModuleDocumentationGenerator(
pathlib.Path(
(
'tests/unit/core/python/_core/test_pr'
'oject_dirs/pdir_2/src/pkg/module.py'
),
), ),
None,
allow_tables = False,
allow_html = False,
), ),
'''# Module `module` (
PythonModuleDocumentationGenerator(
pathlib.Path(
(
'tests/unit/core/python/_core/test_pr'
'oject_dirs/pdir_2/src/pkg/module.py'
),
),
None,
allow_tables=False,
allow_html=False,
),
"""# Module `module`
A test module. A test module.
@@ -1204,9 +1206,10 @@ Barks.
### Docstring ### Docstring
A dog.''' A dog.""",
), ),
]) ],
)
def test_PythonModuleDocumentationGenerator_exceptions( def test_PythonModuleDocumentationGenerator_exceptions(
module_documentation_generator, module_documentation_generator,
expected, expected,
@@ -12,16 +12,21 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from src.jcloud_docsgen.core.python._core import _argument_kind_human_readable
from src.jcloud_docsgen.core.python.arguments import PythonArgumentKind
import pytest import pytest
@pytest.mark.parametrize('argument_kind,expected', [ from src.jcloud_docsgen.core.python._core import _argument_kind_human_readable
(PythonArgumentKind.NORMAL, 'normal'), from src.jcloud_docsgen.core.python.arguments import PythonArgumentKind
(PythonArgumentKind.POSITIONAL_ONLY, 'positional-only'),
(PythonArgumentKind.KEYWORD_ONLY, 'keyword-only'),
(PythonArgumentKind.VARARG, 'vararg'), @pytest.mark.parametrize(
(PythonArgumentKind.KWARGS, 'kwargs'), '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): def test__argument_kind_human_readable(argument_kind, expected):
assert _argument_kind_human_readable(argument_kind) == expected assert _argument_kind_human_readable(argument_kind) == expected
@@ -12,21 +12,24 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import ast
import pytest
from src.jcloud_docsgen.core.python._core import _collect_definitions from src.jcloud_docsgen.core.python._core import _collect_definitions
from src.jcloud_docsgen.core.python.arguments import (
PythonArgumentKind,
PythonFunctionArgument,
)
from src.jcloud_docsgen.core.python.definitions import ( from src.jcloud_docsgen.core.python.definitions import (
PythonClassDefinition, PythonClassDefinition,
PythonFunctionDefinition, PythonFunctionDefinition,
) )
from src.jcloud_docsgen.core.python.arguments import (
PythonFunctionArgument,
PythonArgumentKind,
)
from src.jcloud_docsgen.core.python.docstrings import PythonDocstring from src.jcloud_docsgen.core.python.docstrings import PythonDocstring
import pytest
import ast
def module(source_code: str) -> ast.Module: def module(source_code: str) -> ast.Module:
''' """
Returns an AST module of the source code. Returns an AST module of the source code.
:param source_code: The module source code. :param source_code: The module source code.
@@ -34,38 +37,41 @@ def module(source_code: str) -> ast.Module:
:return: The AST module. :return: The AST module.
:rtype: ast.Module :rtype: ast.Module
''' """
return ast.parse(source_code) return ast.parse(source_code)
@pytest.mark.parametrize('module,expected', [
( @pytest.mark.parametrize(
module('def add(a, b):\n\treturn a + b'), 'module,expected',
[ [
PythonFunctionDefinition( (
'add', module('def add(a, b):\n\treturn a + b'),
[ [
PythonFunctionArgument( PythonFunctionDefinition(
'a', 'add',
PythonArgumentKind.NORMAL, [
None, PythonFunctionArgument(
None, 'a',
), PythonArgumentKind.NORMAL,
PythonFunctionArgument( None,
'b', None,
PythonArgumentKind.NORMAL, ),
None, PythonFunctionArgument(
None, 'b',
), PythonArgumentKind.NORMAL,
], None,
None, None,
None, ),
[], ],
), None,
], None,
), [],
( ),
module('''def add(a: int | float, b: int | float) -> int | float: ],
),
(
module('''def add(a: int | float, b: int | float) -> int | float:
"""Adds two numbers.""" """Adds two numbers."""
return a + b return a + b
@@ -89,87 +95,87 @@ class Something:
""" """
return self._value return self._value
'''), '''),
[ [
PythonFunctionDefinition( PythonFunctionDefinition(
'add', 'add',
[ [
PythonFunctionArgument( PythonFunctionArgument(
'a', 'a',
PythonArgumentKind.NORMAL, PythonArgumentKind.NORMAL,
None, None,
'int | float', 'int | float',
), ),
PythonFunctionArgument( PythonFunctionArgument(
'b', 'b',
PythonArgumentKind.NORMAL, PythonArgumentKind.NORMAL,
None, None,
'int | float', 'int | float',
), ),
], ],
'int | float', 'int | float',
PythonDocstring('Adds two numbers.'), PythonDocstring('Adds two numbers.'),
[], [],
), ),
PythonClassDefinition( PythonClassDefinition(
'Something', 'Something',
[], [],
PythonDocstring('A class.'), PythonDocstring('A class.'),
[], [],
[ [
PythonFunctionDefinition( PythonFunctionDefinition(
'__init__', '__init__',
[ [
PythonFunctionArgument( PythonFunctionArgument(
'self', 'self',
PythonArgumentKind.NORMAL, PythonArgumentKind.NORMAL,
None, None,
None, None,
), ),
PythonFunctionArgument( PythonFunctionArgument(
'value', 'value',
PythonArgumentKind.NORMAL, PythonArgumentKind.NORMAL,
'42', '42',
'typing.Any', 'typing.Any',
), ),
], ],
'None', 'None',
None, None,
[], [],
), ),
PythonFunctionDefinition( PythonFunctionDefinition(
'value', 'value',
[ [
PythonFunctionArgument( PythonFunctionArgument(
'self', 'self',
PythonArgumentKind.NORMAL, PythonArgumentKind.NORMAL,
None, None,
None, None,
), ),
], ],
'typing.Any', 'typing.Any',
PythonDocstring('The value.'), PythonDocstring('The value.'),
['property'], ['property'],
), ),
PythonFunctionDefinition( PythonFunctionDefinition(
'get_value', 'get_value',
[ [
PythonFunctionArgument( PythonFunctionArgument(
'self', 'self',
PythonArgumentKind.NORMAL, PythonArgumentKind.NORMAL,
None, None,
None, None,
), ),
], ],
'typing.Any', 'typing.Any',
PythonDocstring('Returns the value.'), PythonDocstring('Returns the value.'),
[], [],
), ),
], ],
), ),
], ],
), ),
( (
module('''from src.jcloud_docsgen.core.python import ( module("""from src.jcloud_docsgen.core.python import (
PythonDocumentationGenerator PythonDocumentationGenerator
) )
from src.jcloud_docsgen.core.python.namespaces import ( from src.jcloud_docsgen.core.python.namespaces import (
@@ -235,139 +241,142 @@ def test_PythonDocumentationGenerator_collect_modules(
python_documentation_generator: PythonDocumentationGenerator, python_documentation_generator: PythonDocumentationGenerator,
expected expected
): ):
assert python_documentation_generator.namespace() == expected'''), assert python_documentation_generator.namespace() == expected"""),
[ [
PythonFunctionDefinition( PythonFunctionDefinition(
'test_PythonDocumentationGenerator_type_exceptions', 'test_PythonDocumentationGenerator_type_exceptions',
[ [
PythonFunctionArgument( PythonFunctionArgument(
'project_directory', 'project_directory',
PythonArgumentKind.NORMAL, PythonArgumentKind.NORMAL,
None, None,
None, None,
), ),
PythonFunctionArgument( PythonFunctionArgument(
'docs_directory', 'docs_directory',
PythonArgumentKind.NORMAL, PythonArgumentKind.NORMAL,
None, None,
None, None,
) ),
], ],
None, None,
None, None,
[ [
( (
'''pytest.mark.parametrize('project_directo''' """pytest.mark.parametrize('project_directo"""
'''ry,docs_directory', [(1, 1), (1, None), ''' """ry,docs_directory', [(1, 1), (1, None), """
'''(42, None), (pathlib.Path('tests'), 1), ''' """(42, None), (pathlib.Path('tests'), 1), """
'''(pathlib.Path('tests/'), 1), (1, pathlib''' """(pathlib.Path('tests/'), 1), (1, pathlib"""
'''.Path('tests/')), (1, pathlib.Path('test''' """.Path('tests/')), (1, pathlib.Path('test"""
'''s')), (None, pathlib.Path('tests'))])''' """s')), (None, pathlib.Path('tests'))])"""
), ),
], ],
), ),
PythonFunctionDefinition( PythonFunctionDefinition(
'test_PythonDocumentationGenerator_collect_modules', 'test_PythonDocumentationGenerator_collect_modules',
[ [
PythonFunctionArgument( PythonFunctionArgument(
'python_documentation_generator', 'python_documentation_generator',
PythonArgumentKind.NORMAL, PythonArgumentKind.NORMAL,
None, None,
'PythonDocumentationGenerator', 'PythonDocumentationGenerator',
), ),
PythonFunctionArgument( PythonFunctionArgument(
'expected', 'expected',
PythonArgumentKind.NORMAL, PythonArgumentKind.NORMAL,
None, None,
None, None,
) ),
], ],
None, None,
None, None,
[( [
'''pytest.mark.parametrize('python_documentation_gener''' (
'''ator,expected', [(PythonDocumentationGenerator(path''' """pytest.mark.parametrize('python_documentation_gener"""
'''lib.Path('tests/unit/core/python/_core/test_project''' """ator,expected', [(PythonDocumentationGenerator(path"""
'''_dirs/pdir_1'), pathlib.Path('tests/unit/core/pytho''' """lib.Path('tests/unit/core/python/_core/test_project"""
'''n/_core/test_project_dirs/pdir_1/docs')), []), (Pyt''' """_dirs/pdir_1'), pathlib.Path('tests/unit/core/pytho"""
'''honDocumentationGenerator(pathlib.Path('tests/unit/''' """n/_core/test_project_dirs/pdir_1/docs')), []), (Pyt"""
'''core/python/_core/test_project_dirs/pdir_2'), pathl''' """honDocumentationGenerator(pathlib.Path('tests/unit/"""
'''ib.Path('tests/unit/core/python/_core/test_project_''' """core/python/_core/test_project_dirs/pdir_2'), pathl"""
'''dirs/pdir_2/docs')), [PythonPackageNamespace('pkg',''' """ib.Path('tests/unit/core/python/_core/test_project_"""
''' [PythonModuleNamespace('module')])]), (PythonDocum''' """dirs/pdir_2/docs')), [PythonPackageNamespace('pkg',"""
'''entationGenerator(pathlib.Path('tests/unit/core/pyt''' """ [PythonModuleNamespace('module')])]), (PythonDocum"""
'''hon/_core/test_project_dirs/pdir_3'), pathlib.Path(''' """entationGenerator(pathlib.Path('tests/unit/core/pyt"""
''''tests/unit/core/python/_core/test_project_dirs/pdi''' """hon/_core/test_project_dirs/pdir_3'), pathlib.Path("""
'''r_3/docs')), [PythonPackageNamespace('pkg', [Python''' """'tests/unit/core/python/_core/test_project_dirs/pdi"""
'''ModuleNamespace(\'module'), PythonPackageNamespace(''' """r_3/docs')), [PythonPackageNamespace('pkg', [Python"""
'''\'pkg\', [PythonModuleNamespace(\'module2')])])])])''' """ModuleNamespace(\'module'), PythonPackageNamespace("""
)], """\'pkg\', [PythonModuleNamespace(\'module2')])])])])"""
), )
], ],
), ),
( ],
module( ),
'''class C1: (
module(
"""class C1:
class C2: class C2:
class C3: class C3:
def function(self, value: bool, /, arg) -> bool: def function(self, value: bool, /, arg) -> bool:
def should_be_not_in_the_result(arg: int) -> object: def should_be_not_in_the_result(arg: int) -> object:
pass pass
return not value''' return not value"""
),
[
PythonClassDefinition(
'C1',
[],
None,
[],
[
PythonClassDefinition(
'C2',
[],
None,
[],
[
PythonClassDefinition(
'C3',
[],
None,
[],
[
PythonFunctionDefinition(
'function',
[
PythonFunctionArgument(
'self',
PythonArgumentKind.POSITIONAL_ONLY,
None,
None,
),
PythonFunctionArgument(
'value',
PythonArgumentKind.POSITIONAL_ONLY,
None,
'bool',
),
PythonFunctionArgument(
'arg',
PythonArgumentKind.NORMAL,
None,
None,
)
],
'bool',
None,
[],
),
],
),
],
),
],
), ),
], [
), PythonClassDefinition(
]) 'C1',
[],
None,
[],
[
PythonClassDefinition(
'C2',
[],
None,
[],
[
PythonClassDefinition(
'C3',
[],
None,
[],
[
PythonFunctionDefinition(
'function',
[
PythonFunctionArgument(
'self',
PythonArgumentKind.POSITIONAL_ONLY,
None,
None,
),
PythonFunctionArgument(
'value',
PythonArgumentKind.POSITIONAL_ONLY,
None,
'bool',
),
PythonFunctionArgument(
'arg',
PythonArgumentKind.NORMAL,
None,
None,
),
],
'bool',
None,
[],
),
],
),
],
),
],
),
],
),
],
)
def test___collect_definitions(module, expected): def test___collect_definitions(module, expected):
assert list(_collect_definitions(module)) == expected assert list(_collect_definitions(module)) == expected
@@ -12,44 +12,54 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from src.jcloud_docsgen.core.python._core import _fit_row_length
import pytest import pytest
@pytest.mark.parametrize('expected_length,row,expected', [ from src.jcloud_docsgen.core.python._core import _fit_row_length
(0, (), ()),
(1, (), (None,)),
(2, (), (None, None)),
(2, (1,), (1, None)),
(0, (1,), ()),
(1, (1, 2), (1,)),
(1, (1, 2), (1,)),
(0, (1, 2), ()),
(0, (1, 2, 3), ()),
(1, (1, 2, 3), (1,)),
(2, (1, 2, 3), (1, 2)),
(1, (1,), (1,)), @pytest.mark.parametrize(
(2, (1, 1), (1, 1)), 'expected_length,row,expected',
(2, (1, 2), (1, 2)), [
(3, (1, 2, 3), (1, 2, 3)), (0, (), ()),
]) (1, (), (None,)),
(2, (), (None, None)),
(2, (1,), (1, None)),
(0, (1,), ()),
(1, (1, 2), (1,)),
(1, (1, 2), (1,)),
(0, (1, 2), ()),
(0, (1, 2, 3), ()),
(1, (1, 2, 3), (1,)),
(2, (1, 2, 3), (1, 2)),
(1, (1,), (1,)),
(2, (1, 1), (1, 1)),
(2, (1, 2), (1, 2)),
(3, (1, 2, 3), (1, 2, 3)),
],
)
def test__fit_row_length(expected_length, row, expected): def test__fit_row_length(expected_length, row, expected):
assert _fit_row_length(expected_length, row) == expected assert _fit_row_length(expected_length, row) == expected
@pytest.mark.parametrize('row', [
(), @pytest.mark.parametrize(
(1,), 'row',
(1, 2), [
(1, 2, 3), (),
(1, 2, 3, 456), (1,),
]) (1, 2),
@pytest.mark.parametrize('expected_length,expected_exception', [ (1, 2, 3),
(-1, ValueError), (1, 2, 3, 456),
(-2, ValueError), ],
(-3, ValueError), )
(-42, ValueError), @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): def test__fit_row_length_exceptions(expected_length, row, expected_exception):
with pytest.raises(expected_exception) as exc_info: with pytest.raises(expected_exception) as exc_info:
_fit_row_length(expected_length, row) _fit_row_length(expected_length, row)
@@ -12,21 +12,23 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import docstring_parser
import pytest
from src.jcloud_docsgen.core.python._core import ( from src.jcloud_docsgen.core.python._core import (
_function_params_info, _function_params_info,
_FunctionParamInfo, _FunctionParamInfo,
) )
from src.jcloud_docsgen.core.python.arguments import ( from src.jcloud_docsgen.core.python.arguments import (
PythonFunctionArgument,
PythonArgumentKind, PythonArgumentKind,
PythonFunctionArgument,
) )
import pytest
import docstring_parser
def docstring_args_list( def docstring_args_list(
docstring: str, docstring: str,
) -> list[docstring_parser.DocstringParam]: ) -> list[docstring_parser.DocstringParam]:
''' """
Returns the parameter list of a docstring. Returns the parameter list of a docstring.
:param docstring: The docstring :param docstring: The docstring
@@ -34,82 +36,164 @@ def docstring_args_list(
:return: The parameter list. :return: The parameter list.
:rtype: list[docstring_parser.DocstringParam] :rtype: list[docstring_parser.DocstringParam]
''' """
return docstring_parser.parse(docstring).params return docstring_parser.parse(docstring).params
@pytest.mark.parametrize('arg_list,docstring,expected', [
([ @pytest.mark.parametrize(
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', 'int'), 'arg_list,docstring,expected',
], ':param a: a\n:type a: str', { [
'a': _FunctionParamInfo('int', 'a'), (
}), [
([ PythonFunctionArgument(
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', None), 'a', PythonArgumentKind.NORMAL, '', 'int'
], ':param a: a\n:type a: str', { ),
'a': _FunctionParamInfo('str', 'a'), ],
}), ':param a: a\n:type a: str',
([ {
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', None), 'a': _FunctionParamInfo('int', 'a'),
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('a', PythonArgumentKind.NORMAL, '', None), ),
PythonFunctionArgument('b', PythonArgumentKind.NORMAL, '', 'int'), ],
], ':param a: a\n:type a: str', { ':param a: a\n:type a: str',
'a': _FunctionParamInfo('str', 'a'), {
'b': _FunctionParamInfo('int', None), 'a': _FunctionParamInfo('str', 'a'),
}), },
([ ),
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': _FunctionParamInfo('str', 'a'), 'a', PythonArgumentKind.NORMAL, '', None
'b': _FunctionParamInfo('int', 'b'), ),
}), PythonFunctionArgument(
([ 'b', PythonArgumentKind.NORMAL, '', None
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', None), ),
], ':param a: a\n:type a: str\n:param b: b\n:type b: int', { ],
'a': _FunctionParamInfo('str', 'a'), ':param a: a\n:type a: str',
}), {
([ 'a': _FunctionParamInfo('str', 'a'),
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', None), 'b': _FunctionParamInfo(None, None),
], '', { },
'a': _FunctionParamInfo(None, None), ),
}), (
([ [
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', 'str'), PythonFunctionArgument(
], '', { 'a', PythonArgumentKind.NORMAL, '', None
'a': _FunctionParamInfo('str', None), ),
}), PythonFunctionArgument(
([ 'b', PythonArgumentKind.NORMAL, '', 'int'
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', 'str'), ),
], ':param a:\n:type a: int', { ],
'a': _FunctionParamInfo('str', ''), ':param a: a\n:type a: str',
}), {
([ 'a': _FunctionParamInfo('str', 'a'),
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', None), 'b': _FunctionParamInfo('int', None),
], ':param a:\n:type a: int', { },
'a': _FunctionParamInfo('int', ''), ),
}), (
([ [
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', None), PythonFunctionArgument(
], ':param a: a', { 'a', PythonArgumentKind.NORMAL, '', None
'a': _FunctionParamInfo(None, 'a'), ),
}), PythonFunctionArgument(
([ 'b', PythonArgumentKind.NORMAL, '', 'int'
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', 'str'), ),
], ':param a: a', { ],
'a': _FunctionParamInfo('str', 'a'), ':param a: a\n:type a: str\n:param b: b\n:type b: str',
}), {
([], ':param a: a\n:type a: str', {}), 'a': _FunctionParamInfo('str', 'a'),
([], '', {}), 'b': _FunctionParamInfo('int', 'b'),
]) },
),
(
[
PythonFunctionArgument(
'a', PythonArgumentKind.NORMAL, '', None
),
],
':param a: a\n:type a: str\n:param b: b\n:type b: int',
{
'a': _FunctionParamInfo('str', 'a'),
},
),
(
[
PythonFunctionArgument(
'a', PythonArgumentKind.NORMAL, '', None
),
],
'',
{
'a': _FunctionParamInfo(None, None),
},
),
(
[
PythonFunctionArgument(
'a', PythonArgumentKind.NORMAL, '', 'str'
),
],
'',
{
'a': _FunctionParamInfo('str', None),
},
),
(
[
PythonFunctionArgument(
'a', PythonArgumentKind.NORMAL, '', 'str'
),
],
':param a:\n:type a: int',
{
'a': _FunctionParamInfo('str', ''),
},
),
(
[
PythonFunctionArgument(
'a', PythonArgumentKind.NORMAL, '', None
),
],
':param a:\n:type a: int',
{
'a': _FunctionParamInfo('int', ''),
},
),
(
[
PythonFunctionArgument(
'a', PythonArgumentKind.NORMAL, '', None
),
],
':param a: a',
{
'a': _FunctionParamInfo(None, 'a'),
},
),
(
[
PythonFunctionArgument(
'a', PythonArgumentKind.NORMAL, '', 'str'
),
],
':param a: a',
{
'a': _FunctionParamInfo('str', 'a'),
},
),
([], ':param a: a\n:type a: str', {}),
([], '', {}),
],
)
def test__function_params_types(arg_list, docstring, expected): def test__function_params_types(arg_list, docstring, expected):
assert _function_params_info( assert (
arg_list, _function_params_info(arg_list, docstring_args_list(docstring))
docstring_args_list(docstring) == expected
) == expected )
@@ -12,9 +12,11 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from src.jcloud_docsgen.core.python._core import _markdown_table
import pytest import pytest
from src.jcloud_docsgen.core.python._core import _markdown_table
@pytest.mark.parametrize( @pytest.mark.parametrize(
( (
'header,body,allow_markdown_table,force_row_le' 'header,body,allow_markdown_table,force_row_le'
@@ -55,9 +57,9 @@ import pytest
False, False,
True, True,
'--', '--',
'''| a | """| a |
| --- | | --- |
| 1 |''', | 1 |""",
), ),
( (
['a'], ['a'],
@@ -66,9 +68,9 @@ import pytest
False, False,
True, True,
'--', '--',
'''| a | """| a |
| --- | | --- |
| 1 |''', | 1 |""",
), ),
( (
['a', 'b'], ['a', 'b'],
@@ -77,9 +79,9 @@ import pytest
False, False,
True, True,
'--', '--',
'''| a | b | """| a | b |
| --- | --- | | --- | --- |
| 1 | 2 |''', | 1 | 2 |""",
), ),
( (
['a', 'b'], ['a', 'b'],
@@ -88,9 +90,9 @@ import pytest
False, False,
True, True,
'--', '--',
'''| a | b | """| a | b |
| --- | --- | | --- | --- |
| 1 | -- |''', | 1 | -- |""",
), ),
( (
['a', 'b'], ['a', 'b'],
@@ -99,9 +101,9 @@ import pytest
False, False,
True, True,
'--', '--',
'''| a | b | """| a | b |
| --- | --- | | --- | --- |
| -- | -- |''', | -- | -- |""",
), ),
( (
['a', 'b'], ['a', 'b'],
@@ -110,10 +112,10 @@ import pytest
False, False,
True, True,
'--', '--',
'''| a | b | """| a | b |
| --- | --- | | --- | --- |
| 1 | 2 | | 1 | 2 |
| 3 | 4 |''', | 3 | 4 |""",
), ),
( (
['a'], ['a'],
@@ -122,9 +124,9 @@ import pytest
False, False,
True, True,
'--', '--',
'''| a | """| a |
| --- | | --- |
| line1 line2 |''', | line1 line2 |""",
), ),
( (
['a'], ['a'],
@@ -133,11 +135,10 @@ import pytest
False, False,
False, False,
'--', '--',
'''| a | """| a |
| --- | | --- |
| line1\nline2 |''', | line1\nline2 |""",
), ),
( (
[], [],
[], [],
@@ -190,9 +191,9 @@ import pytest
False, False,
True, True,
'--', '--',
'''- a: 1 """- a: 1
b: 2''', b: 2""",
), ),
( (
['a', 'b'], ['a', 'b'],
@@ -201,9 +202,9 @@ import pytest
False, False,
True, True,
'--', '--',
'''- a: 1 """- a: 1
b: --''', b: --""",
), ),
( (
['a', 'b'], ['a', 'b'],
@@ -212,9 +213,9 @@ import pytest
False, False,
True, True,
'--', '--',
'''- a: -- """- a: --
b: --''', b: --""",
), ),
( (
['a', 'b'], ['a', 'b'],
@@ -223,13 +224,13 @@ import pytest
False, False,
True, True,
'--', '--',
'''- a: 1 """- a: 1
b: 2 b: 2
- a: 3 - a: 3
b: 4''', b: 4""",
), ),
( (
['a'], ['a'],
@@ -260,14 +261,18 @@ def test__markdown_table(
missing_value, missing_value,
expected, expected,
): ):
assert _markdown_table( assert (
header, _markdown_table(
body, header,
allow_markdown_table = allow_markdown_table, body,
force_row_lengths = force_row_lengths, allow_markdown_table=allow_markdown_table,
one_line_values = one_line_values, force_row_lengths=force_row_lengths,
missing_value = missing_value, one_line_values=one_line_values,
) == expected missing_value=missing_value,
)
== expected
)
@pytest.mark.parametrize( @pytest.mark.parametrize(
( (
@@ -335,7 +340,6 @@ def test__markdown_table(
ValueError, ValueError,
'expected 2 values, got 3', 'expected 2 values, got 3',
), ),
( (
['a'], ['a'],
[(1, 2)], [(1, 2)],
@@ -412,9 +416,9 @@ def test__markdown_table_exceptions(
_markdown_table( _markdown_table(
header, header,
body, body,
allow_markdown_table = allow_markdown_table, allow_markdown_table=allow_markdown_table,
force_row_lengths = force_row_lengths, force_row_lengths=force_row_lengths,
one_line_values = one_line_values, one_line_values=one_line_values,
missing_value = missing_value, missing_value=missing_value,
) )
assert str(exc_info.value) == expected_exception_msg assert str(exc_info.value) == expected_exception_msg
@@ -12,273 +12,336 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from src.jcloud_docsgen.core.python._core import _signature_argument_list
from src.jcloud_docsgen.core.python.arguments import (
PythonFunctionArgument,
PythonArgumentKind,
)
import pytest import pytest
@pytest.mark.parametrize('argument_list,expected', [ from src.jcloud_docsgen.core.python._core import _signature_argument_list
([ from src.jcloud_docsgen.core.python.arguments import (
PythonFunctionArgument( PythonArgumentKind,
'a', PythonFunctionArgument,
PythonArgumentKind.NORMAL, )
None,
None,
@pytest.mark.parametrize(
'argument_list,expected',
[
(
[
PythonFunctionArgument(
'a',
PythonArgumentKind.NORMAL,
None,
None,
),
],
['a'],
), ),
], ['a']), (
([ [
PythonFunctionArgument( PythonFunctionArgument(
'a', 'a',
PythonArgumentKind.POSITIONAL_ONLY, PythonArgumentKind.POSITIONAL_ONLY,
None, None,
None, None,
),
],
['a', '/'],
), ),
], ['a', '/']), (
([ [
PythonFunctionArgument( PythonFunctionArgument(
'a', 'a',
PythonArgumentKind.POSITIONAL_ONLY, PythonArgumentKind.POSITIONAL_ONLY,
None, None,
None, None,
),
PythonFunctionArgument(
'b',
PythonArgumentKind.NORMAL,
None,
None,
),
],
['a', '/', 'b'],
), ),
PythonFunctionArgument( (
'b', [
PythonArgumentKind.NORMAL, PythonFunctionArgument(
None, 'a',
None, PythonArgumentKind.POSITIONAL_ONLY,
None,
None,
),
PythonFunctionArgument(
'b',
PythonArgumentKind.NORMAL,
None,
None,
),
PythonFunctionArgument(
'c',
PythonArgumentKind.KEYWORD_ONLY,
None,
None,
),
],
['a', '/', 'b', '*', 'c'],
), ),
], ['a', '/', 'b']), (
([ [
PythonFunctionArgument( PythonFunctionArgument(
'a', 'b',
PythonArgumentKind.POSITIONAL_ONLY, PythonArgumentKind.NORMAL,
None, None,
None, None,
),
PythonFunctionArgument(
'c',
PythonArgumentKind.KEYWORD_ONLY,
None,
None,
),
],
['b', '*', 'c'],
), ),
PythonFunctionArgument( (
'b', [
PythonArgumentKind.NORMAL, PythonFunctionArgument(
None, 'c',
None, PythonArgumentKind.KEYWORD_ONLY,
None,
None,
),
],
['*', 'c'],
), ),
PythonFunctionArgument( (
'c', [
PythonArgumentKind.KEYWORD_ONLY, PythonFunctionArgument(
None, 'b',
None, PythonArgumentKind.POSITIONAL_ONLY,
None,
None,
),
PythonFunctionArgument(
'c',
PythonArgumentKind.KEYWORD_ONLY,
None,
None,
),
],
['b', '/', '*', 'c'],
), ),
], ['a', '/', 'b', '*', 'c']), (
([ [
PythonFunctionArgument( PythonFunctionArgument(
'b', 'args',
PythonArgumentKind.NORMAL, PythonArgumentKind.VARARG,
None, None,
None, None,
)
],
['*args'],
), ),
PythonFunctionArgument( (
'c', [
PythonArgumentKind.KEYWORD_ONLY, PythonFunctionArgument(
None, 'a',
None, PythonArgumentKind.NORMAL,
None,
None,
),
PythonFunctionArgument(
'args',
PythonArgumentKind.VARARG,
None,
None,
),
],
['a', '*args'],
), ),
], ['b', '*', 'c']), (
([ [
PythonFunctionArgument( PythonFunctionArgument(
'c', 'a',
PythonArgumentKind.KEYWORD_ONLY, PythonArgumentKind.POSITIONAL_ONLY,
None, None,
None, None,
),
PythonFunctionArgument(
'args',
PythonArgumentKind.VARARG,
None,
None,
),
],
['a', '/', '*args'],
), ),
], ['*', 'c']), (
([ [
PythonFunctionArgument( PythonFunctionArgument(
'b', 'args',
PythonArgumentKind.POSITIONAL_ONLY, PythonArgumentKind.VARARG,
None, None,
None, None,
),
PythonFunctionArgument(
'a',
PythonArgumentKind.KEYWORD_ONLY,
None,
None,
),
],
['*args', 'a'],
), ),
PythonFunctionArgument( (
'c', [
PythonArgumentKind.KEYWORD_ONLY, PythonFunctionArgument(
None, 'args',
None, PythonArgumentKind.VARARG,
None,
None,
),
PythonFunctionArgument(
'kwargs',
PythonArgumentKind.KWARGS,
None,
None,
),
],
['*args', '**kwargs'],
), ),
], ['b', '/', '*', 'c']), (
([ [
PythonFunctionArgument( PythonFunctionArgument(
'args', 'a',
PythonArgumentKind.VARARG, PythonArgumentKind.POSITIONAL_ONLY,
None, None,
None, None,
) ),
], ['*args']), PythonFunctionArgument(
([ 'args',
PythonFunctionArgument( PythonArgumentKind.VARARG,
'a', None,
PythonArgumentKind.NORMAL, None,
None, ),
None, PythonFunctionArgument(
'kwargs',
PythonArgumentKind.KWARGS,
None,
None,
),
],
['a', '/', '*args', '**kwargs'],
), ),
PythonFunctionArgument( (
'args', [
PythonArgumentKind.VARARG, PythonFunctionArgument(
None, 'a',
None, PythonArgumentKind.NORMAL,
None,
None,
),
PythonFunctionArgument(
'args',
PythonArgumentKind.VARARG,
None,
None,
),
PythonFunctionArgument(
'kwargs',
PythonArgumentKind.KWARGS,
None,
None,
),
],
['a', '*args', '**kwargs'],
), ),
], ['a', '*args']), (
([ [
PythonFunctionArgument( PythonFunctionArgument(
'a', PythonArgumentKind.POSITIONAL_ONLY, 'args',
None, PythonArgumentKind.VARARG,
None, None,
None,
),
PythonFunctionArgument(
'a',
PythonArgumentKind.KEYWORD_ONLY,
None,
None,
),
PythonFunctionArgument(
'kwargs',
PythonArgumentKind.KWARGS,
None,
None,
),
],
['*args', 'a', '**kwargs'],
), ),
PythonFunctionArgument( (
'args', [
PythonArgumentKind.VARARG, PythonFunctionArgument(
None, 'a',
None, PythonArgumentKind.KEYWORD_ONLY,
None,
None,
),
PythonFunctionArgument(
'kwargs',
PythonArgumentKind.KWARGS,
None,
None,
),
],
['*', 'a', '**kwargs'],
), ),
], ['a', '/', '*args']), (
([ [
PythonFunctionArgument( PythonFunctionArgument(
'args', 'a',
PythonArgumentKind.VARARG, PythonArgumentKind.NORMAL,
None, None,
None, None,
),
PythonFunctionArgument(
'kwargs',
PythonArgumentKind.KWARGS,
None,
None,
),
],
['a', '**kwargs'],
), ),
PythonFunctionArgument( (
'a', [
PythonArgumentKind.KEYWORD_ONLY, PythonFunctionArgument(
None, 'a',
None, PythonArgumentKind.POSITIONAL_ONLY,
None,
None,
),
PythonFunctionArgument(
'kwargs',
PythonArgumentKind.KWARGS,
None,
None,
),
],
['a', '/', '**kwargs'],
), ),
], ['*args', 'a']), (
([ [
PythonFunctionArgument( PythonFunctionArgument(
'args', 'a',
PythonArgumentKind.VARARG, PythonArgumentKind.NORMAL,
None, '1',
None, 'int',
),
],
['a: int = 1'],
), ),
PythonFunctionArgument( ],
'kwargs', )
PythonArgumentKind.KWARGS,
None,
None,
),
], ['*args', '**kwargs']),
([
PythonFunctionArgument(
'a',
PythonArgumentKind.POSITIONAL_ONLY,
None,
None,
),
PythonFunctionArgument(
'args',
PythonArgumentKind.VARARG,
None,
None,
),
PythonFunctionArgument(
'kwargs',
PythonArgumentKind.KWARGS,
None,
None,
),
], ['a', '/', '*args', '**kwargs']),
([
PythonFunctionArgument(
'a',
PythonArgumentKind.NORMAL,
None,
None,
),
PythonFunctionArgument(
'args',
PythonArgumentKind.VARARG,
None,
None,
),
PythonFunctionArgument(
'kwargs',
PythonArgumentKind.KWARGS,
None,
None,
),
], ['a', '*args', '**kwargs']),
([
PythonFunctionArgument(
'args',
PythonArgumentKind.VARARG,
None,
None,
),
PythonFunctionArgument(
'a',
PythonArgumentKind.KEYWORD_ONLY,
None,
None,
),
PythonFunctionArgument(
'kwargs',
PythonArgumentKind.KWARGS,
None,
None,
),
], ['*args', 'a', '**kwargs']),
([
PythonFunctionArgument(
'a',
PythonArgumentKind.KEYWORD_ONLY,
None,
None,
),
PythonFunctionArgument(
'kwargs',
PythonArgumentKind.KWARGS,
None,
None,
),
], ['*', 'a', '**kwargs']),
([
PythonFunctionArgument(
'a',
PythonArgumentKind.NORMAL,
None,
None,
),
PythonFunctionArgument(
'kwargs',
PythonArgumentKind.KWARGS,
None,
None,
),
], ['a', '**kwargs']),
([
PythonFunctionArgument(
'a',
PythonArgumentKind.POSITIONAL_ONLY,
None,
None,
),
PythonFunctionArgument(
'kwargs',
PythonArgumentKind.KWARGS,
None,
None,
),
], ['a', '/', '**kwargs']),
([
PythonFunctionArgument(
'a',
PythonArgumentKind.NORMAL,
'1',
'int',
),
], ['a: int = 1']),
])
def test__signature_argument_list(argument_list, expected): def test__signature_argument_list(argument_list, expected):
assert _signature_argument_list(argument_list) == expected assert _signature_argument_list(argument_list) == expected
@@ -1,9 +1,10 @@
''' """
A test module. A test module.
''' """
def add(a: int | float, b: int | float) -> int | float: def add(a: int | float, b: int | float) -> int | float:
''' """
Adds two numbers. Adds two numbers.
:param a: The first number. :param a: The first number.
@@ -13,38 +14,41 @@ def add(a: int | float, b: int | float) -> int | float:
:return: The sum. :return: The sum.
:rtype: int | float :rtype: int | float
''' """
return a + b return a + b
class Animal: class Animal:
''' """
A class representing an animal. A class representing an animal.
:param name: Its name. :param name: Its name.
:type name: str :type name: str
''' """
def __init__(self, name: str) -> None: def __init__(self, name: str) -> None:
self.name = name self.name = name
def greet(self): def greet(self):
''' """
Returns a greeting. Returns a greeting.
:return: The greeting. :return: The greeting.
:rtype: str :rtype: str
''' """
return f'Hi there! My name is {self.name}' return f'Hi there! My name is {self.name}'
class Dog(Animal): class Dog(Animal):
''' """
A dog. A dog.
''' """
def bark(self) -> None: def bark(self) -> None:
''' """
Barks. Barks.
''' """
print('Woof!') print('Woof!')
File diff suppressed because it is too large. Load diff
@@ -12,16 +12,18 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import pytest
from src.jcloud_docsgen.core.python.arguments import ( from src.jcloud_docsgen.core.python.arguments import (
PythonFunctionArgument,
PythonArgumentKind, PythonArgumentKind,
PythonFunctionArgument,
) )
from src.jcloud_docsgen.exceptions import ( from src.jcloud_docsgen.exceptions import (
InvalidPythonIdentifierError,
InvalidPythonAnnotationError, InvalidPythonAnnotationError,
InvalidPythonIdentifierError,
PythonArgumentStructureError, PythonArgumentStructureError,
) )
import pytest
@pytest.mark.parametrize( @pytest.mark.parametrize(
'name,kind,default,annotation,expected_exception,expected_exception_msg', 'name,kind,default,annotation,expected_exception,expected_exception_msg',
@@ -32,7 +34,7 @@ import pytest
None, None,
None, None,
TypeError, TypeError,
'expected \'str\', got \'NoneType\'', "expected 'str', got 'NoneType'",
), ),
( (
1, 1,
@@ -40,7 +42,7 @@ import pytest
None, None,
None, None,
TypeError, TypeError,
'expected \'str\', got \'int\'', "expected 'str', got 'int'",
), ),
( (
42, 42,
@@ -48,7 +50,7 @@ import pytest
None, None,
None, None,
TypeError, TypeError,
'expected \'str\', got \'int\'', "expected 'str', got 'int'",
), ),
( (
4.2, 4.2,
@@ -56,7 +58,7 @@ import pytest
None, None,
None, None,
TypeError, TypeError,
'expected \'str\', got \'float\'', "expected 'str', got 'float'",
), ),
( (
4.2, 4.2,
@@ -64,7 +66,7 @@ import pytest
None, None,
None, None,
TypeError, TypeError,
'expected \'str\', got \'float\'', "expected 'str', got 'float'",
), ),
( (
'a', 'a',
@@ -72,7 +74,7 @@ import pytest
1, 1,
None, None,
TypeError, TypeError,
'expected either \'str\' or \'NoneType\', got \'int\'', "expected either 'str' or 'NoneType', got 'int'",
), ),
( (
'a', 'a',
@@ -80,7 +82,7 @@ import pytest
None, None,
1, 1,
TypeError, TypeError,
'expected either \'str\' or \'NoneType\', got \'int\'', "expected either 'str' or 'NoneType', got 'int'",
), ),
( (
'a', 'a',
@@ -88,7 +90,7 @@ import pytest
None, None,
None, None,
TypeError, TypeError,
'expected \'PythonArgumentKind\', got \'int\'', "expected 'PythonArgumentKind', got 'int'",
), ),
( (
'a', 'a',
@@ -384,220 +386,244 @@ def test_PythonFunctionArgument_exceptions(
PythonFunctionArgument(name, kind, default, annotation) PythonFunctionArgument(name, kind, default, annotation)
assert str(exc_info.value) == expected_exception_msg assert str(exc_info.value) == expected_exception_msg
@pytest.mark.parametrize('name', [
'a', @pytest.mark.parametrize(
'_a', 'name',
'a1', [
'a0', 'a',
'a_', '_a',
'_', 'a1',
'_abc', 'a0',
'abc', 'a_',
'abc_', '_',
'match', '_abc',
'case', 'abc',
'type', 'abc_',
'ä', 'match',
'none', 'case',
'true', 'type',
'false', 'ä',
]) 'none',
@pytest.mark.parametrize('kind', [ 'true',
PythonArgumentKind.NORMAL, 'false',
PythonArgumentKind.POSITIONAL_ONLY, ],
PythonArgumentKind.KEYWORD_ONLY, )
]) @pytest.mark.parametrize(
@pytest.mark.parametrize('default', [ 'kind',
None, [
'x', PythonArgumentKind.NORMAL,
'1', PythonArgumentKind.POSITIONAL_ONLY,
None, PythonArgumentKind.KEYWORD_ONLY,
]) ],
@pytest.mark.parametrize('annotation', [ )
None, @pytest.mark.parametrize(
'a', 'default',
'list[int]', [
'_', None,
]) 'x',
'1',
None,
],
)
@pytest.mark.parametrize(
'annotation',
[
None,
'a',
'list[int]',
'_',
],
)
def test_PythonFunctionArgument(name, kind, default, annotation): def test_PythonFunctionArgument(name, kind, default, annotation):
PythonFunctionArgument(name, kind, default, annotation) PythonFunctionArgument(name, kind, default, annotation)
@pytest.mark.parametrize('name', [
'a', @pytest.mark.parametrize(
'_a', 'name',
'a1', [
'a0', 'a',
'a_', '_a',
'_', 'a1',
'_abc', 'a0',
'abc', 'a_',
'abc_', '_',
'match', '_abc',
'case', 'abc',
'type', 'abc_',
'ä', 'match',
'none', 'case',
'true', 'type',
'false', 'ä',
]) 'none',
@pytest.mark.parametrize('kind', [ 'true',
PythonArgumentKind.VARARG, 'false',
PythonArgumentKind.KWARGS, ],
]) )
@pytest.mark.parametrize('annotation', [ @pytest.mark.parametrize(
None, 'kind',
'a', [
'list[int]', PythonArgumentKind.VARARG,
'_', PythonArgumentKind.KWARGS,
]) ],
)
@pytest.mark.parametrize(
'annotation',
[
None,
'a',
'list[int]',
'_',
],
)
def test_PythonFunctionArgument_asterik(name, kind, annotation): def test_PythonFunctionArgument_asterik(name, kind, annotation):
PythonFunctionArgument(name, kind, None, annotation) PythonFunctionArgument(name, kind, None, annotation)
@pytest.mark.parametrize('argument,expected', [
( @pytest.mark.parametrize(
PythonFunctionArgument( 'argument,expected',
[
(
PythonFunctionArgument(
'a',
PythonArgumentKind.NORMAL,
None,
None,
),
'a', 'a',
PythonArgumentKind.NORMAL,
None,
None,
), ),
'a', (
), PythonFunctionArgument(
( 'a',
PythonFunctionArgument( PythonArgumentKind.NORMAL,
'1',
None,
),
'a = 1',
),
(
PythonFunctionArgument(
'a', PythonArgumentKind.NORMAL, None, 'int'
),
'a: int',
),
(
PythonFunctionArgument(
'a',
PythonArgumentKind.NORMAL,
'1',
'int',
),
'a: int = 1',
),
(
PythonFunctionArgument(
'a',
PythonArgumentKind.POSITIONAL_ONLY,
None,
None,
),
'a', 'a',
PythonArgumentKind.NORMAL,
'1',
None,
), ),
'a = 1', (
), PythonFunctionArgument(
( 'a',
PythonFunctionArgument( PythonArgumentKind.POSITIONAL_ONLY,
'1',
None,
),
'a = 1',
),
(
PythonFunctionArgument(
'a',
PythonArgumentKind.POSITIONAL_ONLY,
None,
'int',
),
'a: int',
),
(
PythonFunctionArgument(
'a',
PythonArgumentKind.POSITIONAL_ONLY,
'1',
'int',
),
'a: int = 1',
),
(
PythonFunctionArgument(
'a',
PythonArgumentKind.KEYWORD_ONLY,
None,
None,
),
'a', 'a',
PythonArgumentKind.NORMAL,
None,
'int'
), ),
'a: int', (
), PythonFunctionArgument(
( 'a',
PythonFunctionArgument( PythonArgumentKind.KEYWORD_ONLY,
'a', '1',
PythonArgumentKind.NORMAL, None,
'1', ),
'int', 'a = 1',
), ),
'a: int = 1', (
), PythonFunctionArgument(
( 'a',
PythonFunctionArgument( PythonArgumentKind.KEYWORD_ONLY,
'a', None,
PythonArgumentKind.POSITIONAL_ONLY, 'int',
None, ),
None, 'a: int',
), ),
'a', (
), PythonFunctionArgument(
( 'a',
PythonFunctionArgument( PythonArgumentKind.KEYWORD_ONLY,
'a', '1',
PythonArgumentKind.POSITIONAL_ONLY, 'int',
'1', ),
None, 'a: int = 1',
), ),
'a = 1', (
), PythonFunctionArgument(
( 'a',
PythonFunctionArgument( PythonArgumentKind.VARARG,
'a', None,
PythonArgumentKind.POSITIONAL_ONLY, None,
None, ),
'int', '*a',
), ),
'a: int', (
), PythonFunctionArgument(
( 'a',
PythonFunctionArgument( PythonArgumentKind.VARARG,
'a', None,
PythonArgumentKind.POSITIONAL_ONLY, 'int',
'1', ),
'int', '*a: int',
), ),
'a: int = 1', (
), PythonFunctionArgument(
( 'a',
PythonFunctionArgument( PythonArgumentKind.KWARGS,
'a', None,
PythonArgumentKind.KEYWORD_ONLY, None,
None, ),
None, '**a',
), ),
'a', (
), PythonFunctionArgument(
( 'a',
PythonFunctionArgument( PythonArgumentKind.KWARGS,
'a', None,
PythonArgumentKind.KEYWORD_ONLY, 'int',
'1', ),
None, '**a: int',
), ),
'a = 1', ],
), )
(
PythonFunctionArgument(
'a',
PythonArgumentKind.KEYWORD_ONLY,
None,
'int',
),
'a: int',
),
(
PythonFunctionArgument(
'a',
PythonArgumentKind.KEYWORD_ONLY,
'1',
'int',
),
'a: int = 1',
),
(
PythonFunctionArgument(
'a',
PythonArgumentKind.VARARG,
None,
None,
),
'*a',
),
(
PythonFunctionArgument(
'a',
PythonArgumentKind.VARARG,
None,
'int',
),
'*a: int',
),
(
PythonFunctionArgument(
'a',
PythonArgumentKind.KWARGS,
None,
None,
),
'**a',
),
(
PythonFunctionArgument(
'a',
PythonArgumentKind.KWARGS,
None,
'int',
),
'**a: int',
),
])
def test_PythonFunctionArgument_signature_repr(argument, expected): def test_PythonFunctionArgument_signature_repr(argument, expected):
assert argument.signature_repr() == expected assert argument.signature_repr() == expected
@@ -12,44 +12,49 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from src.jcloud_docsgen.core.python.definitions import ( import pytest
PythonFunctionDefinition,
PythonClassDefinition,
)
from src.jcloud_docsgen.core.python.arguments import ( from src.jcloud_docsgen.core.python.arguments import (
PythonFunctionArgument,
PythonArgumentKind, PythonArgumentKind,
PythonFunctionArgument,
)
from src.jcloud_docsgen.core.python.definitions import (
PythonClassDefinition,
PythonFunctionDefinition,
) )
from src.jcloud_docsgen.core.python.docstrings import PythonDocstring from src.jcloud_docsgen.core.python.docstrings import PythonDocstring
from src.jcloud_docsgen.exceptions import InvalidPythonIdentifierError from src.jcloud_docsgen.exceptions import InvalidPythonIdentifierError
from tests.utils.ast_node import ast_node from tests.utils.ast_node import ast_node
import pytest
@pytest.mark.parametrize('definition,expected', [
( @pytest.mark.parametrize(
PythonClassDefinition( 'definition,expected',
'A', [
[], (
None, PythonClassDefinition(
None, 'A',
[], [],
), None,
'PythonClassDefinition(\'A\', [], None, None, [])', None,
), [],
( ),
PythonClassDefinition( "PythonClassDefinition('A', [], None, None, [])",
'A',
[],
None,
PythonDocstring('Hello, World!'),
[],
), ),
( (
'PythonClassDefinition(\'A\', [], None, ' PythonClassDefinition(
'PythonDocstring(\'Hello, World!\'), [])' 'A',
[],
None,
PythonDocstring('Hello, World!'),
[],
),
(
"PythonClassDefinition('A', [], None, "
"PythonDocstring('Hello, World!'), [])"
),
), ),
), ],
]) )
def test_PythonDefinition_string_representation(definition, expected): def test_PythonDefinition_string_representation(definition, expected):
assert repr(definition) == expected assert repr(definition) == expected
@@ -364,129 +369,132 @@ def test_PythonFunctionDefinition_exceptions(
assert str(exc_info.value) == expected_exception_message assert str(exc_info.value) == expected_exception_message
@pytest.mark.parametrize('node,expected', [ @pytest.mark.parametrize(
( 'node,expected',
ast_node( [
( (
'@decorator\nclass Class(BaseClass1, BaseClass2)' ast_node(
':\n\tdef __init__(self, value: object) -> None:' (
'\n\t\tself._value = value\n\n\t@property\n\tdef' '@decorator\nclass Class(BaseClass1, BaseClass2)'
' value(self) -> object:\n\t\treturn self._value' ':\n\tdef __init__(self, value: object) -> None:'
'\n\t\tself._value = value\n\n\t@property\n\tdef'
' value(self) -> object:\n\t\treturn self._value'
),
),
PythonClassDefinition(
'Class',
['BaseClass1', 'BaseClass2'],
None,
['decorator'],
[
PythonFunctionDefinition(
'__init__',
[
PythonFunctionArgument(
'self',
PythonArgumentKind.NORMAL,
None,
None,
),
PythonFunctionArgument(
'value',
PythonArgumentKind.NORMAL,
None,
'object',
),
],
'None',
None,
[],
),
PythonFunctionDefinition(
'value',
[
PythonFunctionArgument(
'self',
PythonArgumentKind.NORMAL,
None,
None,
),
],
'object',
None,
['property'],
),
],
), ),
), ),
PythonClassDefinition( (
'Class', ast_node('class Class: ...'),
['BaseClass1', 'BaseClass2'], PythonClassDefinition(
None, 'Class',
['decorator'], [],
[ None,
PythonFunctionDefinition( [],
'__init__', [],
[ ),
PythonFunctionArgument(
'self',
PythonArgumentKind.NORMAL,
None,
None,
),
PythonFunctionArgument(
'value',
PythonArgumentKind.NORMAL,
None,
'object',
),
],
'None',
None,
[],
),
PythonFunctionDefinition(
'value',
[
PythonFunctionArgument(
'self',
PythonArgumentKind.NORMAL,
None,
None,
),
],
'object',
None,
['property'],
),
],
), ),
), (
( ast_node("class Class: '''docstring'''"),
ast_node('class Class: ...'), PythonClassDefinition(
PythonClassDefinition( 'Class',
'Class', [],
[], PythonDocstring('docstring'),
None, [],
[], [],
[], ),
), ),
), (
( ast_node('class Class:\n\tdef __init__(self): ...'),
ast_node('class Class: \'\'\'docstring\'\'\''), PythonClassDefinition(
PythonClassDefinition( 'Class',
'Class', [],
[], None,
PythonDocstring('docstring'), [],
[], [
[], PythonFunctionDefinition(
'__init__',
[
PythonFunctionArgument(
'self',
PythonArgumentKind.NORMAL,
None,
None,
)
],
None,
None,
[],
),
],
),
), ),
), (
( ast_node("class Class:\n\t'''doc'''\n\tdef __init__(self): ..."),
ast_node('class Class:\n\tdef __init__(self): ...'), PythonClassDefinition(
PythonClassDefinition( 'Class',
'Class', [],
[], PythonDocstring('doc'),
None, [],
[], [
[ PythonFunctionDefinition(
PythonFunctionDefinition( '__init__',
'__init__', [
[ PythonFunctionArgument(
PythonFunctionArgument( 'self',
'self', PythonArgumentKind.NORMAL,
PythonArgumentKind.NORMAL, None,
None, None,
None, ),
) ],
], None,
None, None,
None, [],
[], ),
), ],
], ),
), ),
), ],
( )
ast_node('class Class:\n\t\'\'\'doc\'\'\'\n\tdef __init__(self): ...'),
PythonClassDefinition(
'Class',
[],
PythonDocstring('doc'),
[],
[
PythonFunctionDefinition(
'__init__',
[
PythonFunctionArgument(
'self',
PythonArgumentKind.NORMAL,
None,
None,
),
],
None,
None,
[],
),
],
),
),
])
def test_PythonDefinition_from_node(node, expected): def test_PythonDefinition_from_node(node, expected):
assert PythonClassDefinition.from_node(node) == expected assert PythonClassDefinition.from_node(node) == expected
@@ -12,29 +12,34 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import pytest
from src.jcloud_docsgen.core.python.definitions import PythonDefinition from src.jcloud_docsgen.core.python.definitions import PythonDefinition
from src.jcloud_docsgen.core.python.docstrings import PythonDocstring from src.jcloud_docsgen.core.python.docstrings import PythonDocstring
from src.jcloud_docsgen.exceptions import InvalidPythonIdentifierError from src.jcloud_docsgen.exceptions import InvalidPythonIdentifierError
import pytest
@pytest.mark.parametrize('definition,expected', [
( @pytest.mark.parametrize(
PythonDefinition( 'definition,expected',
'a', [
None, (
[], PythonDefinition(
'a',
None,
[],
),
"PythonDefinition('a', None, [])",
), ),
'PythonDefinition(\'a\', None, [])', (
), PythonDefinition(
( 'a',
PythonDefinition( PythonDocstring('Hello, World!'),
'a', [],
PythonDocstring('Hello, World!'), ),
[], "PythonDefinition('a', PythonDocstring('Hello, World!'), [])",
), ),
'PythonDefinition(\'a\', PythonDocstring(\'Hello, World!\'), [])', ],
), )
])
def test_PythonDefinition_string_representation(definition, expected): def test_PythonDefinition_string_representation(definition, expected):
assert repr(definition) == expected assert repr(definition) == expected
@@ -280,164 +285,167 @@ def test_PythonDefinition_exceptions(
assert str(exc_info.value) == expected_exception_message assert str(exc_info.value) == expected_exception_message
@pytest.mark.parametrize('definition1,definition2,expected', [ @pytest.mark.parametrize(
( 'definition1,definition2,expected',
PythonDefinition( [
'a', (
None, PythonDefinition(
[], 'a',
None,
[],
),
PythonDefinition(
'a',
None,
[],
),
True,
), ),
PythonDefinition( (
'a', PythonDefinition(
None, 'a',
[], PythonDocstring('doc'),
[],
),
PythonDefinition(
'a',
PythonDocstring('doc'),
[],
),
True,
), ),
True, (
), PythonDefinition(
( 'a',
PythonDefinition( PythonDocstring('doc'),
'a', ['a'],
PythonDocstring('doc'), ),
[], PythonDefinition(
'a',
PythonDocstring('doc'),
['a'],
),
True,
), ),
PythonDefinition( (
'a', PythonDefinition(
PythonDocstring('doc'), 'a',
[], PythonDocstring('doc'),
['a', 'b'],
),
PythonDefinition(
'a',
PythonDocstring('doc'),
['a', 'b'],
),
True,
), ),
True, (
), PythonDefinition(
( 'a',
PythonDefinition( None,
'a', [],
PythonDocstring('doc'), ),
['a'], PythonDefinition(
'b',
None,
[],
),
False,
), ),
PythonDefinition( (
'a', PythonDefinition(
PythonDocstring('doc'), 'a',
['a'], PythonDocstring('doc'),
[],
),
PythonDefinition(
'a',
None,
[],
),
False,
), ),
True, (
), PythonDefinition(
( 'a',
PythonDefinition( PythonDocstring('doc'),
'a', [],
PythonDocstring('doc'), ),
['a', 'b'], PythonDefinition(
'a',
PythonDocstring('docs'),
[],
),
False,
), ),
PythonDefinition( (
'a', PythonDefinition(
PythonDocstring('doc'), 'a',
['a', 'b'], None,
['a'],
),
PythonDefinition(
'a',
None,
['b'],
),
False,
), ),
True, (
), PythonDefinition(
( 'a',
PythonDefinition( None,
'a', ['a'],
None, ),
[], PythonDefinition(
'a',
None,
[],
),
False,
), ),
PythonDefinition( (
'b', PythonDefinition(
None, 'a',
[], None,
['a'],
),
PythonDefinition(
'a',
None,
['a', 'b'],
),
False,
), ),
False, (
), PythonDefinition(
( 'a',
PythonDefinition( None,
'a', ['b', 'a'],
PythonDocstring('doc'), ),
[], PythonDefinition(
'a',
None,
['a', 'b'],
),
False,
), ),
PythonDefinition( (
'a', PythonDefinition(
None, 'a',
[], PythonDocstring('doc'),
[],
),
PythonDefinition(
'b',
None,
['a', 'b'],
),
False,
), ),
False, ],
), )
(
PythonDefinition(
'a',
PythonDocstring('doc'),
[],
),
PythonDefinition(
'a',
PythonDocstring('docs'),
[],
),
False,
),
(
PythonDefinition(
'a',
None,
['a'],
),
PythonDefinition(
'a',
None,
['b'],
),
False,
),
(
PythonDefinition(
'a',
None,
['a'],
),
PythonDefinition(
'a',
None,
[],
),
False,
),
(
PythonDefinition(
'a',
None,
['a'],
),
PythonDefinition(
'a',
None,
['a', 'b'],
),
False,
),
(
PythonDefinition(
'a',
None,
['b', 'a'],
),
PythonDefinition(
'a',
None,
['a', 'b'],
),
False,
),
(
PythonDefinition(
'a',
PythonDocstring('doc'),
[],
),
PythonDefinition(
'b',
None,
['a', 'b'],
),
False,
),
])
def test_PythonDefinition___eq__(definition1, definition2, expected): def test_PythonDefinition___eq__(definition1, definition2, expected):
assert (definition1 == definition2) == expected assert (definition1 == definition2) == expected
assert (definition2 == definition1) == expected assert (definition2 == definition1) == expected
@@ -12,41 +12,46 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from src.jcloud_docsgen.core.python.definitions import PythonFunctionDefinition import pytest
from src.jcloud_docsgen.core.python.arguments import ( from src.jcloud_docsgen.core.python.arguments import (
PythonFunctionArgument,
PythonArgumentKind, PythonArgumentKind,
PythonFunctionArgument,
) )
from src.jcloud_docsgen.core.python.definitions import PythonFunctionDefinition
from src.jcloud_docsgen.core.python.docstrings import PythonDocstring from src.jcloud_docsgen.core.python.docstrings import PythonDocstring
from src.jcloud_docsgen.exceptions import InvalidPythonIdentifierError from src.jcloud_docsgen.exceptions import InvalidPythonIdentifierError
from tests.utils.ast_node import ast_node from tests.utils.ast_node import ast_node
import pytest
@pytest.mark.parametrize('definition,expected', [
( @pytest.mark.parametrize(
PythonFunctionDefinition( 'definition,expected',
'a', [
[], (
None, PythonFunctionDefinition(
None, 'a',
[], [],
), None,
'PythonFunctionDefinition(\'a\', [], None, None, [])', None,
), [],
( ),
PythonFunctionDefinition( "PythonFunctionDefinition('a', [], None, None, [])",
'a',
[],
None,
PythonDocstring('Hello, World!'),
[],
), ),
( (
'PythonFunctionDefinition(\'a\', [], None,' PythonFunctionDefinition(
' PythonDocstring(\'Hello, World!\'), [])' 'a',
[],
None,
PythonDocstring('Hello, World!'),
[],
),
(
"PythonFunctionDefinition('a', [], None,"
" PythonDocstring('Hello, World!'), [])"
),
), ),
), ],
]) )
def test_PythonDefinition_string_representation(definition, expected): def test_PythonDefinition_string_representation(definition, expected):
assert repr(definition) == expected assert repr(definition) == expected
@@ -57,295 +62,296 @@ def test_PythonDefinition_string_representation(definition, expected):
'ed_exception,expected_exception_message' 'ed_exception,expected_exception_message'
), ),
[ [
( (
'', '',
[], [],
None, None,
None, None,
[], [],
InvalidPythonIdentifierError, InvalidPythonIdentifierError,
'invalid identifier', 'invalid identifier',
), ),
( (
'.', '.',
[], [],
None, None,
None, None,
[], [],
InvalidPythonIdentifierError, InvalidPythonIdentifierError,
'invalid identifier: .', 'invalid identifier: .',
), ),
( (
'#', '#',
[], [],
None, None,
None, None,
[], [],
InvalidPythonIdentifierError, InvalidPythonIdentifierError,
'invalid identifier: #', 'invalid identifier: #',
), ),
( (
'1a', '1a',
[], [],
None, None,
None, None,
[], [],
InvalidPythonIdentifierError, InvalidPythonIdentifierError,
'invalid identifier: 1a', 'invalid identifier: 1a',
), ),
( (
'1_', '1_',
[], [],
None, None,
None, None,
[], [],
InvalidPythonIdentifierError, InvalidPythonIdentifierError,
'invalid identifier: 1_', 'invalid identifier: 1_',
), ),
( (
'1,', '1,',
[], [],
None, None,
None, None,
[], [],
InvalidPythonIdentifierError, InvalidPythonIdentifierError,
'invalid identifier: 1,', 'invalid identifier: 1,',
), ),
( (
',', ',',
[], [],
None, None,
None, None,
[], [],
InvalidPythonIdentifierError, InvalidPythonIdentifierError,
'invalid identifier: ,', 'invalid identifier: ,',
), ),
( (
':', ':',
[], [],
None, None,
None, None,
[], [],
InvalidPythonIdentifierError, InvalidPythonIdentifierError,
'invalid identifier: :', 'invalid identifier: :',
), ),
( (
'-', '-',
[], [],
None, None,
None, None,
[], [],
InvalidPythonIdentifierError, InvalidPythonIdentifierError,
'invalid identifier: -', 'invalid identifier: -',
), ),
( (
';', ';',
[], [],
None, None,
None, None,
[], [],
InvalidPythonIdentifierError, InvalidPythonIdentifierError,
'invalid identifier: ;', 'invalid identifier: ;',
), ),
( (
'|', '|',
[], [],
None, None,
None, None,
[], [],
InvalidPythonIdentifierError, InvalidPythonIdentifierError,
'invalid identifier: |', 'invalid identifier: |',
), ),
( (
'<', '<',
[], [],
None, None,
None, None,
[], [],
InvalidPythonIdentifierError, InvalidPythonIdentifierError,
'invalid identifier: <', 'invalid identifier: <',
), ),
( (
'>', '>',
[], [],
None, None,
None, None,
[], [],
InvalidPythonIdentifierError, InvalidPythonIdentifierError,
'invalid identifier: >', 'invalid identifier: >',
), ),
( (
'a,', 'a,',
[], [],
None, None,
None, None,
[], [],
InvalidPythonIdentifierError, InvalidPythonIdentifierError,
'invalid identifier: a,', 'invalid identifier: a,',
), ),
( (
',a', ',a',
[], [],
None, None,
None, None,
[], [],
InvalidPythonIdentifierError, InvalidPythonIdentifierError,
'invalid identifier: ,a', 'invalid identifier: ,a',
), ),
( (
'a:', 'a:',
[], [],
None, None,
None, None,
[], [],
InvalidPythonIdentifierError, InvalidPythonIdentifierError,
'invalid identifier: a:', 'invalid identifier: a:',
), ),
( (
':a', ':a',
[], [],
None, None,
None, None,
[], [],
InvalidPythonIdentifierError, InvalidPythonIdentifierError,
'invalid identifier: :a', 'invalid identifier: :a',
), ),
( (
'a-', 'a-',
[], [],
None, None,
None, None,
[], [],
InvalidPythonIdentifierError, InvalidPythonIdentifierError,
'invalid identifier: a-', 'invalid identifier: a-',
), ),
( (
'-a', '-a',
[], [],
None, None,
None, None,
[], [],
InvalidPythonIdentifierError, InvalidPythonIdentifierError,
'invalid identifier: -a', 'invalid identifier: -a',
), ),
( (
'a;', 'a;',
[], [],
None, None,
None, None,
[], [],
InvalidPythonIdentifierError, InvalidPythonIdentifierError,
'invalid identifier: a;', 'invalid identifier: a;',
), ),
( (
';a', ';a',
[], [],
None, None,
None, None,
[], [],
InvalidPythonIdentifierError, InvalidPythonIdentifierError,
'invalid identifier: ;a', 'invalid identifier: ;a',
), ),
( (
'a|', 'a|',
[], [],
None, None,
None, None,
[], [],
InvalidPythonIdentifierError, InvalidPythonIdentifierError,
'invalid identifier: a|', 'invalid identifier: a|',
), ),
( (
'|a', '|a',
[], [],
None, None,
None, None,
[], [],
InvalidPythonIdentifierError, InvalidPythonIdentifierError,
'invalid identifier: |a', 'invalid identifier: |a',
), ),
( (
'a<', 'a<',
[], [],
None, None,
None, None,
[], [],
InvalidPythonIdentifierError, InvalidPythonIdentifierError,
'invalid identifier: a<', 'invalid identifier: a<',
), ),
( (
'<a', '<a',
[], [],
None, None,
None, None,
[], [],
InvalidPythonIdentifierError, InvalidPythonIdentifierError,
'invalid identifier: <a', 'invalid identifier: <a',
), ),
( (
'a>', 'a>',
[], [],
None, None,
None, None,
[], [],
InvalidPythonIdentifierError, InvalidPythonIdentifierError,
'invalid identifier: a>', 'invalid identifier: a>',
), ),
( (
'>a', '>a',
[], [],
None, None,
None, None,
[], [],
InvalidPythonIdentifierError, InvalidPythonIdentifierError,
'invalid identifier: >a', 'invalid identifier: >a',
), ),
( (
'class', 'class',
[], [],
None, None,
None, None,
[], [],
InvalidPythonIdentifierError, InvalidPythonIdentifierError,
'invalid identifier: class', 'invalid identifier: class',
), ),
( (
'def', 'def',
[], [],
None, None,
None, None,
[], [],
InvalidPythonIdentifierError, InvalidPythonIdentifierError,
'invalid identifier: def', 'invalid identifier: def',
), ),
( (
'lambda', 'lambda',
[], [],
None, None,
None, None,
[], [],
InvalidPythonIdentifierError, InvalidPythonIdentifierError,
'invalid identifier: lambda', 'invalid identifier: lambda',
), ),
( (
'None', 'None',
[], [],
None, None,
None, None,
[], [],
InvalidPythonIdentifierError, InvalidPythonIdentifierError,
'invalid identifier: None', 'invalid identifier: None',
), ),
( (
'True', 'True',
[], [],
None, None,
None, None,
[], [],
InvalidPythonIdentifierError, InvalidPythonIdentifierError,
'invalid identifier: True', 'invalid identifier: True',
), ),
]) ],
)
def test_PythonFunctionDefinition_exceptions( def test_PythonFunctionDefinition_exceptions(
name, name,
args, args,
@@ -360,100 +366,106 @@ def test_PythonFunctionDefinition_exceptions(
assert str(exc_info.value) == expected_exception_message 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', [ ast_node('def add(a: int, b: int) -> int: return a + b'),
PythonFunctionArgument( PythonFunctionDefinition(
'a', 'add',
PythonArgumentKind.NORMAL, [
None, PythonFunctionArgument(
'int', 'a',
), PythonArgumentKind.NORMAL,
PythonFunctionArgument( None,
'b', 'int',
PythonArgumentKind.NORMAL, ),
None, PythonFunctionArgument(
'int', 'b',
), PythonArgumentKind.NORMAL,
], None,
'int', 'int',
None, ),
[], ],
), 'int',
), None,
( [],
ast_node('@decorator\ndef add(a: int, b: int) -> int: return a + b'),
PythonFunctionDefinition(
'add',
[
PythonFunctionArgument(
'a',
PythonArgumentKind.NORMAL,
None,
'int',
),
PythonFunctionArgument(
'b',
PythonArgumentKind.NORMAL,
None,
'int',
),
],
'int',
None,
['decorator'],
),
),
(
ast_node(
(
'@decorator1\n@decorator2\ndef add(a'
': int, b: int) -> int: return a + b'
), ),
), ),
PythonFunctionDefinition( (
'add', ast_node(
[ '@decorator\ndef add(a: int, b: int) -> int: return a + b'
PythonFunctionArgument( ),
'a', PythonFunctionDefinition(
PythonArgumentKind.NORMAL, 'add',
None, [
'int', PythonFunctionArgument(
'a',
PythonArgumentKind.NORMAL,
None,
'int',
),
PythonFunctionArgument(
'b',
PythonArgumentKind.NORMAL,
None,
'int',
),
],
'int',
None,
['decorator'],
),
),
(
ast_node(
(
'@decorator1\n@decorator2\ndef add(a'
': int, b: int) -> int: return a + b'
), ),
PythonFunctionArgument( ),
'b', PythonFunctionDefinition(
PythonArgumentKind.NORMAL, 'add',
None, [
'int', PythonFunctionArgument(
), 'a',
], PythonArgumentKind.NORMAL,
'int', None,
None, 'int',
['decorator1', 'decorator2'], ),
PythonFunctionArgument(
'b',
PythonArgumentKind.NORMAL,
None,
'int',
),
],
'int',
None,
['decorator1', 'decorator2'],
),
), ),
), (
( ast_node('def func(): ...'),
ast_node('def func(): ...'), PythonFunctionDefinition(
PythonFunctionDefinition( 'func',
'func', [],
[], None,
None, None,
None, [],
[], ),
), ),
), (
( ast_node('@decorator\ndef func(): ...'),
ast_node('@decorator\ndef func(): ...'), PythonFunctionDefinition(
PythonFunctionDefinition( 'func',
'func', [],
[], None,
None, None,
None, ['decorator'],
['decorator'], ),
), ),
), ],
]) )
def test_PythonDefinition_from_node(node, expected): def test_PythonDefinition_from_node(node, expected):
assert PythonFunctionDefinition.from_node(node) == expected assert PythonFunctionDefinition.from_node(node) == expected
@@ -12,36 +12,53 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from src.jcloud_docsgen.core.python.namespaces import PythonModuleNamespace
import pytest import pytest
from src.jcloud_docsgen.core.python.namespaces import PythonModuleNamespace
class StrSubclass(str): ... class StrSubclass(str): ...
@pytest.mark.parametrize('namespace,expected_name', [
(PythonModuleNamespace('a'), 'a'), @pytest.mark.parametrize(
(PythonModuleNamespace('1'), '1'), 'namespace,expected_name',
(PythonModuleNamespace(StrSubclass('a')), StrSubclass('a')), [
]) (PythonModuleNamespace('a'), 'a'),
(PythonModuleNamespace('1'), '1'),
(PythonModuleNamespace(StrSubclass('a')), StrSubclass('a')),
],
)
def test_PythonModuleNamespace_name_attribute(namespace, expected_name): def test_PythonModuleNamespace_name_attribute(namespace, expected_name):
assert namespace.name == expected_name assert namespace.name == expected_name
@pytest.mark.parametrize('namespace1,namespace2,expected', [
(PythonModuleNamespace('a'), PythonModuleNamespace('a'), True), @pytest.mark.parametrize(
(PythonModuleNamespace('b'), PythonModuleNamespace('b'), True), 'namespace1,namespace2,expected',
(PythonModuleNamespace('1'), PythonModuleNamespace('1'), True), [
(PythonModuleNamespace('42'), PythonModuleNamespace('42'), True), (PythonModuleNamespace('a'), PythonModuleNamespace('a'), True),
(PythonModuleNamespace('a'), PythonModuleNamespace('b'), False), (PythonModuleNamespace('b'), PythonModuleNamespace('b'), True),
(PythonModuleNamespace('0'), PythonModuleNamespace('-0'), False), (PythonModuleNamespace('1'), PythonModuleNamespace('1'), True),
(PythonModuleNamespace('1'), PythonModuleNamespace('a'), False), (PythonModuleNamespace('42'), PythonModuleNamespace('42'), True),
]) (PythonModuleNamespace('a'), PythonModuleNamespace('b'), False),
(PythonModuleNamespace('0'), PythonModuleNamespace('-0'), False),
(PythonModuleNamespace('1'), PythonModuleNamespace('a'), False),
],
)
def test_PythonModuleNamespace___eq__(namespace1, namespace2, expected): def test_PythonModuleNamespace___eq__(namespace1, namespace2, expected):
assert (namespace1 == namespace2) == expected assert (namespace1 == namespace2) == expected
assert (namespace2 == namespace1) == expected assert (namespace2 == namespace1) == expected
@pytest.mark.parametrize('namespace,expected', [
(PythonModuleNamespace('a'), 'PythonModuleNamespace(\'a\')'), @pytest.mark.parametrize(
(PythonModuleNamespace('1'), 'PythonModuleNamespace(\'1\')'), 'namespace,expected',
(PythonModuleNamespace(StrSubclass('a')), 'PythonModuleNamespace(\'a\')'), [
]) (PythonModuleNamespace('a'), "PythonModuleNamespace('a')"),
(PythonModuleNamespace('1'), "PythonModuleNamespace('1')"),
(
PythonModuleNamespace(StrSubclass('a')),
"PythonModuleNamespace('a')",
),
],
)
def test_PythonModuleNamespace_string_representation(namespace, expected): def test_PythonModuleNamespace_string_representation(namespace, expected):
assert repr(namespace) == expected assert repr(namespace) == expected
@@ -12,23 +12,33 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from src.jcloud_docsgen.core.python.namespaces import PythonNamespace
import pytest import pytest
from src.jcloud_docsgen.core.python.namespaces import PythonNamespace
class StrSubclass(str): ... class StrSubclass(str): ...
@pytest.mark.parametrize('namespace,expected_name', [
(PythonNamespace('a'), 'a'), @pytest.mark.parametrize(
(PythonNamespace('1'), '1'), 'namespace,expected_name',
(PythonNamespace(StrSubclass('a')), StrSubclass('a')), [
]) (PythonNamespace('a'), 'a'),
(PythonNamespace('1'), '1'),
(PythonNamespace(StrSubclass('a')), StrSubclass('a')),
],
)
def test_PythonNamespace_name_attribute(namespace, expected_name): def test_PythonNamespace_name_attribute(namespace, expected_name):
assert namespace.name == expected_name assert namespace.name == expected_name
@pytest.mark.parametrize('namespace,expected', [
(PythonNamespace('a'), 'PythonNamespace(\'a\')'), @pytest.mark.parametrize(
(PythonNamespace('1'), 'PythonNamespace(\'1\')'), 'namespace,expected',
(PythonNamespace(StrSubclass('a')), 'PythonNamespace(\'a\')'), [
]) (PythonNamespace('a'), "PythonNamespace('a')"),
(PythonNamespace('1'), "PythonNamespace('1')"),
(PythonNamespace(StrSubclass('a')), "PythonNamespace('a')"),
],
)
def test_PythonNamespace_string_representation(namespace, expected): def test_PythonNamespace_string_representation(namespace, expected):
assert repr(namespace) == expected assert repr(namespace) == expected
@@ -12,48 +12,58 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from src.jcloud_docsgen.core.python.namespaces import (
PythonPackageNamespace,
PythonModuleNamespace,
)
import pytest import pytest
from src.jcloud_docsgen.exceptions import (
NamespaceNotFoundError, from src.jcloud_docsgen.core.python.namespaces import (
InvalidNamespaceError, PythonModuleNamespace,
PythonPackageNamespace,
) )
from src.jcloud_docsgen.exceptions import (
InvalidNamespaceError,
NamespaceNotFoundError,
)
class StrSubclass(str): ... class StrSubclass(str): ...
@pytest.mark.parametrize('namespace,expected_name', [
( @pytest.mark.parametrize(
PythonPackageNamespace( 'namespace,expected_name',
[
(
PythonPackageNamespace(
'a',
[],
),
'a', 'a',
[],
), ),
'a', (
), PythonPackageNamespace(
( '1',
PythonPackageNamespace( [],
),
'1', '1',
[],
), ),
'1', (
), PythonPackageNamespace(
( StrSubclass('a'),
PythonPackageNamespace( [],
),
StrSubclass('a'), StrSubclass('a'),
[],
), ),
StrSubclass('a'), ],
), )
])
def test_PythonPackageNamespace_name_attribute(namespace, expected_name): def test_PythonPackageNamespace_name_attribute(namespace, expected_name):
assert namespace.name == expected_name assert namespace.name == expected_name
namespace_b = PythonModuleNamespace('b') namespace_b = PythonModuleNamespace('b')
namespace_a = PythonPackageNamespace('a', [ namespace_a = PythonPackageNamespace(
namespace_b, 'a',
]) [
namespace_b,
],
)
namespace_c = PythonModuleNamespace('c') namespace_c = PythonModuleNamespace('c')
namespace_k = PythonModuleNamespace('k') namespace_k = PythonModuleNamespace('k')
namespace_j = PythonPackageNamespace('j', [namespace_k]) namespace_j = PythonPackageNamespace('j', [namespace_k])
@@ -64,47 +74,51 @@ namespace_f = PythonPackageNamespace('f', [namespace_g])
namespace_e = PythonPackageNamespace('e', [namespace_f]) namespace_e = PythonPackageNamespace('e', [namespace_f])
namespace_d = PythonPackageNamespace('d', [namespace_e]) namespace_d = PythonPackageNamespace('d', [namespace_e])
@pytest.mark.parametrize('namespace,sub_namespaces,expected', [
( @pytest.mark.parametrize(
PythonPackageNamespace( 'namespace,sub_namespaces,expected',
'name', [
[namespace_a, namespace_c], (
PythonPackageNamespace(
'name',
[namespace_a, namespace_c],
),
['c'],
namespace_c,
), ),
['c'], (
namespace_c, PythonPackageNamespace(
), 'name',
( [namespace_a, namespace_c],
PythonPackageNamespace( ),
'name', ['a'],
[namespace_a, namespace_c], namespace_a,
), ),
['a'], (
namespace_a, PythonPackageNamespace(
), 'name',
( [namespace_a, namespace_c],
PythonPackageNamespace( ),
'name', ['a', 'b'],
[namespace_a, namespace_c], namespace_b,
), ),
['a', 'b'], (
namespace_b, namespace_d,
), ['e', 'f', 'g', 'h', 'i'],
( namespace_i,
namespace_d, ),
['e', 'f', 'g', 'h', 'i'], (
namespace_i, namespace_d,
), ['e', 'f', 'g', 'h', 'i', 'j'],
( namespace_j,
namespace_d, ),
['e', 'f', 'g', 'h', 'i', 'j'], (
namespace_j, namespace_d,
), ['e', 'f', 'g', 'h', 'i', 'j', 'k'],
( namespace_k,
namespace_d, ),
['e', 'f', 'g', 'h', 'i', 'j', 'k'], ],
namespace_k, )
),
])
def test_PythonPackageNamespace_namespace( def test_PythonPackageNamespace_namespace(
namespace: PythonPackageNamespace, namespace: PythonPackageNamespace,
sub_namespaces, sub_namespaces,
@@ -112,6 +126,7 @@ def test_PythonPackageNamespace_namespace(
): ):
assert namespace.namespace(sub_namespaces) == expected assert namespace.namespace(sub_namespaces) == expected
@pytest.mark.parametrize( @pytest.mark.parametrize(
'namespace,sub_namespaces,expected_exception,expected_exception_msg', 'namespace,sub_namespaces,expected_exception,expected_exception_msg',
[ [
@@ -157,129 +172,145 @@ def test_PythonPackageNamespace_namespace_exceptions(
namespace.namespace(sub_namespaces) namespace.namespace(sub_namespaces)
assert str(exc_info.value) == expected_exception_msg assert str(exc_info.value) == expected_exception_msg
@pytest.mark.parametrize('namespace,expected', [
( @pytest.mark.parametrize(
PythonPackageNamespace( 'namespace,expected',
'a', [
[PythonModuleNamespace('b')], (
PythonPackageNamespace(
'a',
[PythonModuleNamespace('b')],
),
{'b': 'b'},
), ),
{'b': 'b'}, (
), PythonPackageNamespace(
( 'a',
PythonPackageNamespace( [
'a', PythonModuleNamespace('b'),
[ PythonPackageNamespace(
PythonModuleNamespace('b'), 'c',
PythonPackageNamespace( [PythonModuleNamespace('d')],
'c', ),
[PythonModuleNamespace('d')], ],
), ),
], {
'b': 'b',
'c': {'d': 'd'},
},
), ),
{ (
'b': 'b', PythonPackageNamespace('a', []),
'c': {'d': 'd'}, {},
}, ),
), ],
( )
PythonPackageNamespace('a', []),
{},
),
])
def test_PythonPackageNamespace_sub_namespace_names(namespace, expected): def test_PythonPackageNamespace_sub_namespace_names(namespace, expected):
assert namespace.sub_namespace_names() == expected assert namespace.sub_namespace_names() == expected
@pytest.mark.parametrize('namespace1,namespace2,expected', [
( @pytest.mark.parametrize(
PythonPackageNamespace( 'namespace1,namespace2,expected',
'a', [
[PythonModuleNamespace('b')], (
PythonPackageNamespace(
'a',
[PythonModuleNamespace('b')],
),
PythonPackageNamespace(
'a',
[PythonModuleNamespace('b')],
),
True,
), ),
PythonPackageNamespace( (
'a', PythonPackageNamespace(
[PythonModuleNamespace('b')], 'a',
[
PythonModuleNamespace('b'),
PythonPackageNamespace(
'c',
[PythonModuleNamespace('d')],
),
],
),
PythonPackageNamespace(
'a',
[
PythonModuleNamespace('b'),
PythonPackageNamespace(
'c',
[PythonModuleNamespace('d')],
),
],
),
True,
), ),
True, (
), PythonPackageNamespace('a', []),
( PythonPackageNamespace('a', []),
PythonPackageNamespace( True,
'a',
[
PythonModuleNamespace('b'),
PythonPackageNamespace(
'c',
[PythonModuleNamespace('d')],
),
],
), ),
PythonPackageNamespace( (
'a', PythonPackageNamespace('a', []),
[ PythonPackageNamespace('b', []),
PythonModuleNamespace('b'), False,
PythonPackageNamespace(
'c',
[PythonModuleNamespace('d')],
),
],
), ),
True, ],
), )
(
PythonPackageNamespace('a', []),
PythonPackageNamespace('a', []),
True,
),
(
PythonPackageNamespace('a', []),
PythonPackageNamespace('b', []),
False,
),
])
def test_PythonPackageNamespace___eq__(namespace1, namespace2, expected): def test_PythonPackageNamespace___eq__(namespace1, namespace2, expected):
assert (namespace1 == namespace2) == expected assert (namespace1 == namespace2) == expected
assert (namespace2 == namespace1) == expected assert (namespace2 == namespace1) == expected
@pytest.mark.parametrize('namespace,expected', [
( @pytest.mark.parametrize(
PythonPackageNamespace('a', []), 'namespace,expected',
'PythonPackageNamespace(\'a\', [])', [
),
(
PythonPackageNamespace('1', []),
'PythonPackageNamespace(\'1\', [])',
),
(
PythonPackageNamespace(StrSubclass('a'), []),
'PythonPackageNamespace(\'a\', [])',
),
(
namespace_a,
'PythonPackageNamespace(\'a\', [PythonModuleNamespace(\'b\')])',
),
(
namespace_d,
( (
'PythonPackageNamespace(\'d\', [PythonPackageNamespac' PythonPackageNamespace('a', []),
'e(\'e\', [PythonPackageNamespace(\'f\', [PythonPacka' "PythonPackageNamespace('a', [])",
'geNamespace(\'g\', [PythonPackageNamespace(\'h\', [P'
'ythonPackageNamespace(\'i\', [PythonPackageNamespace'
'(\'j\', [PythonModuleNamespace(\'k\')])])])])])])])'
), ),
), (
]) PythonPackageNamespace('1', []),
"PythonPackageNamespace('1', [])",
),
(
PythonPackageNamespace(StrSubclass('a'), []),
"PythonPackageNamespace('a', [])",
),
(
namespace_a,
"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')])])])])])])])"
),
),
],
)
def test_PythonPackageNamespace_string_representation(namespace, expected): def test_PythonPackageNamespace_string_representation(namespace, expected):
assert repr(namespace) == expected assert repr(namespace) == expected
@pytest.mark.parametrize('namespace,new_namespace,expected', [
( @pytest.mark.parametrize(
PythonPackageNamespace('a', []), 'namespace,new_namespace,expected',
PythonModuleNamespace('b'), [
PythonPackageNamespace( (
'a', PythonPackageNamespace('a', []),
[PythonModuleNamespace('b')], PythonModuleNamespace('b'),
PythonPackageNamespace(
'a',
[PythonModuleNamespace('b')],
),
), ),
), ],
]) )
def test_PythonPackageNamespace___getitem__( def test_PythonPackageNamespace___getitem__(
namespace, namespace,
new_namespace, new_namespace,
@@ -12,51 +12,64 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from src.jcloud_docsgen.utils import assert_that_is_instance
import pytest
import types import types
import pytest
from src.jcloud_docsgen.utils import assert_that_is_instance
class TestType: ... class TestType: ...
TestType.__name__ = 'NOT TestType' TestType.__name__ = 'NOT TestType'
@pytest.mark.parametrize('obj,class_or_tuple', [
(1, int), @pytest.mark.parametrize(
(1, (int,)), 'obj,class_or_tuple',
(1, str | int), [
(1, int | str | list), (1, int),
(TestType(), TestType), (1, (int,)),
(None, types.NoneType), (1, str | int),
]) (1, int | str | list),
(TestType(), TestType),
(None, types.NoneType),
],
)
def test_assert_that_is_instance(obj, class_or_tuple): def test_assert_that_is_instance(obj, class_or_tuple):
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(
1, 'obj,class_or_tuple,expected_exception_msg',
str, [
'expected \'str\', got \'int\'', (
), 1,
( str,
1, "expected 'str', got 'int'",
(str,), ),
'expected \'str\', got \'int\'', (
), 1,
( (str,),
1, "expected 'str', got 'int'",
str | float, ),
'expected either \'str\' or \'float\', got \'int\'', (
), 1,
( str | float,
1, "expected either 'str' or 'float', got 'int'",
str | float | list, ),
'expected either \'str\', \'float\' or \'list\', got \'int\'', (
), 1,
( str | float | list,
1, "expected either 'str', 'float' or 'list', got 'int'",
TestType, ),
'expected \'NOT TestType\', got \'int\'', (
), 1,
]) TestType,
"expected 'NOT TestType', got 'int'",
),
],
)
def test_assert_that_is_instance_exceptions( def test_assert_that_is_instance_exceptions(
obj, obj,
class_or_tuple, class_or_tuple,
@@ -12,65 +12,68 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from src.jcloud_docsgen.utils import get_relative_path_segments
import pytest
import pathlib import pathlib
@pytest.mark.parametrize('path,root,expected', [ import pytest
(pathlib.Path('/'), pathlib.Path('/'), []),
(pathlib.Path('/a'), pathlib.Path('/'), ['a']),
(pathlib.Path('/a/'), pathlib.Path('/'), ['a']),
(pathlib.Path('/a'), pathlib.Path('/a'), []),
(pathlib.Path('/a/'), pathlib.Path('/a'), []),
(pathlib.Path('/a'), pathlib.Path('/a/'), []),
(pathlib.Path('/a/'), pathlib.Path('/a/'), []),
(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('/a/b/'), 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('/a/b'), []),
(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('./'), []), from src.jcloud_docsgen.utils import get_relative_path_segments
(pathlib.Path('./a'), pathlib.Path('./'), ['a']),
(pathlib.Path('./a/'), pathlib.Path('./'), ['a']),
(pathlib.Path('./a'), pathlib.Path('./a'), []),
(pathlib.Path('./a/'), pathlib.Path('./a'), []),
(pathlib.Path('./a'), pathlib.Path('./a/'), []),
(pathlib.Path('./a/'), pathlib.Path('./a/'), []),
(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('./a/b/'), 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('./a/b'), []),
(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']),
(pathlib.Path('a'), pathlib.Path('a'), []),
(pathlib.Path('a/'), pathlib.Path('a'), []),
(pathlib.Path('a'), pathlib.Path('a/'), []),
(pathlib.Path('a/'), pathlib.Path('a/'), []),
(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('a/b/'), 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('a/b'), []),
(pathlib.Path('a/b/'), pathlib.Path('a/b'), []),
(pathlib.Path('a/b'), pathlib.Path('a/b/'), []),
(pathlib.Path('a/b/'), pathlib.Path('a/b/'), []),
]) @pytest.mark.parametrize(
'path,root,expected',
[
(pathlib.Path('/'), pathlib.Path('/'), []),
(pathlib.Path('/a'), pathlib.Path('/'), ['a']),
(pathlib.Path('/a/'), pathlib.Path('/'), ['a']),
(pathlib.Path('/a'), pathlib.Path('/a'), []),
(pathlib.Path('/a/'), pathlib.Path('/a'), []),
(pathlib.Path('/a'), pathlib.Path('/a/'), []),
(pathlib.Path('/a/'), pathlib.Path('/a/'), []),
(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('/a/b/'), 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('/a/b'), []),
(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']),
(pathlib.Path('./a'), pathlib.Path('./a'), []),
(pathlib.Path('./a/'), pathlib.Path('./a'), []),
(pathlib.Path('./a'), pathlib.Path('./a/'), []),
(pathlib.Path('./a/'), pathlib.Path('./a/'), []),
(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('./a/b/'), 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('./a/b'), []),
(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']),
(pathlib.Path('a'), pathlib.Path('a'), []),
(pathlib.Path('a/'), pathlib.Path('a'), []),
(pathlib.Path('a'), pathlib.Path('a/'), []),
(pathlib.Path('a/'), pathlib.Path('a/'), []),
(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('a/b/'), 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('a/b'), []),
(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): def test_get_relative_path_segments(path, root, expected):
assert get_relative_path_segments(path, root) == expected assert get_relative_path_segments(path, root) == expected
+46 -41
View File
@@ -12,48 +12,53 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from src.jcloud_docsgen.utils import human_readable_list
import pytest import pytest
@pytest.mark.parametrize('ls,final_separator,quotation_mark,expected', [ from src.jcloud_docsgen.utils import human_readable_list
([], '', '', ''),
([], 'and', '', ''),
([], 'and', '\'', ''), @pytest.mark.parametrize(
([1], '', '', '1'), 'ls,final_separator,quotation_mark,expected',
([-1], '', '', '-1'), [
([0], '', '', '0'), ([], '', '', ''),
([0.0], '', '', '0.0'), ([], 'and', '', ''),
([-0.0], '', '', '-0.0'), ([], 'and', "'", ''),
([4.2], '', '', '4.2'), ([1], '', '', '1'),
([-4.2], '', '', '-4.2'), ([-1], '', '', '-1'),
(['a'], '', '', 'a'), ([0], '', '', '0'),
([1], 'and', '', '1'), ([0.0], '', '', '0.0'),
(['a'], 'and', '', 'a'), ([-0.0], '', '', '-0.0'),
([1], 'and', '\'', '\'1\''), ([4.2], '', '', '4.2'),
(['a'], 'and', '\'', '\'a\''), ([-4.2], '', '', '-4.2'),
([1, 2], 'and', '', '1 and 2'), (['a'], '', '', 'a'),
([1, 2], 'and', '\'', '\'1\' and \'2\''), ([1], 'and', '', '1'),
([1, 'a'], 'and', '', '1 and a'), (['a'], 'and', '', 'a'),
([1, 'a'], 'and', '\'', '\'1\' and \'a\''), ([1], 'and', "'", "'1'"),
([1, 'a'], 'and', '', '1 and a'), (['a'], 'and', "'", "'a'"),
([1, 'a'], 'and', '\'', '\'1\' and \'a\''), ([1, 2], 'and', '', '1 and 2'),
([1, 'a'], 'and', '\'', '\'1\' and \'a\''), ([1, 2], 'and', "'", "'1' and '2'"),
([1, 'a'], '', '', '1 a'), ([1, 'a'], 'and', '', '1 and a'),
([1, 'a'], '', '\'', '\'1\' \'a\''), ([1, 'a'], 'and', "'", "'1' and 'a'"),
([1, 'a'], '', '', '1 a'), ([1, 'a'], 'and', '', '1 and a'),
([1, 'a'], '', '\'', '\'1\' \'a\''), ([1, 'a'], 'and', "'", "'1' and 'a'"),
([1, 'a'], '', '\'', '\'1\' \'a\''), ([1, 'a'], 'and', "'", "'1' and 'a'"),
([1, 2, 3], 'and', '', '1, 2 and 3'), ([1, 'a'], '', '', '1 a'),
([1, 2, 3], '', '', '1, 2 3'), ([1, 'a'], '', "'", "'1' 'a'"),
([1, 2, 3], 'and', '\'', '\'1\', \'2\' and \'3\''), ([1, 'a'], '', '', '1 a'),
([1, 2, 3], '', '\'', '\'1\', \'2\' \'3\''), ([1, 'a'], '', "'", "'1' 'a'"),
([1, 2, 3, 42, 'a', 23], 'and', '', '1, 2, 3, 42, a and 23'), ([1, 'a'], '', "'", "'1' 'a'"),
( ([1, 2, 3], 'and', '', '1, 2 and 3'),
[1, 2, 3, 42, 'a', 23], ([1, 2, 3], '', '', '1, 2 3'),
'and', ([1, 2, 3], 'and', "'", "'1', '2' and '3'"),
'\'', ([1, 2, 3], '', "'", "'1', '2' '3'"),
'\'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', 23],
'and',
"'",
"'1', '2', '3', '42', 'a' and '23'",
),
],
)
def test_human_readable_list(ls, final_separator, quotation_mark, expected): def test_human_readable_list(ls, final_separator, quotation_mark, expected):
assert human_readable_list(ls, final_separator, quotation_mark) == expected assert human_readable_list(ls, final_separator, quotation_mark) == expected
+17 -12
View File
@@ -12,19 +12,24 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from src.jcloud_docsgen.utils import non_empty_str
import pytest import pytest
@pytest.mark.parametrize('value,expected', [ from src.jcloud_docsgen.utils import non_empty_str
(None, None),
('', None),
(' ', None), @pytest.mark.parametrize(
(' ', None), 'value,expected',
(' ', None), [
(' a ', ' a '), (None, None),
(' a', ' a'), ('', None),
('a ', 'a '), (' ', None),
('a', 'a'), (' ', None),
]) (' ', None),
(' a ', ' a '),
(' a', ' a'),
('a ', 'a '),
('a', 'a'),
],
)
def test_non_empty_str(value, expected): def test_non_empty_str(value, expected):
assert non_empty_str(value) == expected assert non_empty_str(value) == expected
+6 -8
View File
@@ -12,23 +12,21 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from typing import Union
import ast import ast
__all__ = [ __all__ = ['ast_node']
'ast_node'
]
def ast_node(statement: str) -> Union[ast.stmt, None]:
''' def ast_node(statement: str) -> ast.stmt | None:
"""
Returns an AST node from the statement. Returns an AST node from the statement.
:param statement: The statement. :param statement: The statement.
:type statement: str :type statement: str
:return: The node. :return: The node.
:rtype: Union[ast.stmt, None] :rtype: ast.stmt | None
''' """
body = ast.parse(statement).body body = ast.parse(statement).body
if not body: if not body: