generated from jCloud/repository-template
Compare commits
29
Commits
62cadcc7ae
...
v0.2.0-rc0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2a8e54766
|
||
|
|
773294f3d9
|
||
|
|
200221e324
|
||
|
|
e8bcf4feb1
|
||
|
|
ff3bddd366
|
||
|
|
e94365faf8
|
||
|
|
95da53898c
|
||
|
|
4d2e63d560
|
||
|
|
2187590468
|
||
|
|
37308227d9
|
||
|
|
f796263c0e
|
||
|
|
896bfcc2dd
|
||
|
|
b2a7c9a0e3
|
||
|
|
ac72eb283c
|
||
|
|
21c5c20cb5
|
||
|
|
3865db7919
|
||
|
|
ec262ef99e
|
||
|
|
f102fa80ce
|
||
|
|
6d2b32a736
|
||
|
|
934624fce3
|
||
|
|
b043a4ea17
|
||
|
|
f2b9f47e09
|
||
|
|
f0c271f8fd
|
||
|
|
d8508e0fa3
|
||
|
|
5791358b41
|
||
|
|
0261a7b3a6
|
||
|
|
2fe342cbc6
|
||
|
|
53ab0e3829
|
||
|
|
c40e965d8d
|
No files matched your search
@@ -39,8 +39,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate documentation
|
- name: Generate documentation
|
||||||
run: |
|
run: |
|
||||||
rm -rf docs/reference
|
jcloud-docsgen python -T -f -o docs/reference ./src
|
||||||
jcloud-docsgen python -T -o docs/reference .
|
|
||||||
|
|
||||||
- name: Commit and push
|
- name: Commit and push
|
||||||
run: |
|
run: |
|
||||||
@@ -49,7 +48,7 @@ jobs:
|
|||||||
|
|
||||||
git add docs/reference
|
git add docs/reference
|
||||||
|
|
||||||
if git diff --chached --quit; then
|
if git diff --cached --quiet; then
|
||||||
echo "No changes"
|
echo "No changes"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|||||||
+3
-1
@@ -1,17 +1,19 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [Unreleased]
|
## [0.2.0] - 2026-09-28
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- CLI for `jcloud-docsgen` that can be called without `python3 -m ...`
|
- CLI for `jcloud-docsgen` that can be called without `python3 -m ...`
|
||||||
- CI pipeline to generate API reference on every push or merge on `main`
|
- CI pipeline to generate API reference on every push or merge on `main`
|
||||||
- Python generator output directory creation
|
- Python generator output directory creation
|
||||||
|
- Protection against accidental overwriting of the output directory
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Replaced the installation instructions, which used the old registry, with a reference to the Gitea Package Registry in README
|
- Replaced the installation instructions, which used the old registry, with a reference to the Gitea Package Registry in README
|
||||||
- Made Python function signatures (argument default values) compliant with PEP 8
|
- Made Python function signatures (argument default values) compliant with PEP 8
|
||||||
|
- Removed implicit Python project directory structure assumptions
|
||||||
|
|
||||||
### Deprecated
|
### Deprecated
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "jcloud-docsgen"
|
name = "jcloud-docsgen"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
description = "A tool that generates Markdown documentation for software project source code"
|
description = "A tool that generates Markdown documentation for software project source code"
|
||||||
dependencies = ["docstring-parser"]
|
dependencies = ["docstring-parser"]
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from . import cli
|
from . import cli
|
||||||
from .core import *
|
from .core import * # noqa: F403
|
||||||
from .core import __all__ as _core__all__
|
from .core import __all__ as _core__all__
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import argparse
|
|||||||
import pathlib
|
import pathlib
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from ..exceptions import DirectoryNotEmptyError
|
||||||
from .languages.python import VALID_SECTIONS as VALID_PYTHON_SECTIONS
|
from .languages.python import VALID_SECTIONS as VALID_PYTHON_SECTIONS
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -81,6 +82,15 @@ def create_argument_parser() -> argparse.ArgumentParser:
|
|||||||
default='docs/reference',
|
default='docs/reference',
|
||||||
help='the output directory the documentation will be written to',
|
help='the output directory the documentation will be written to',
|
||||||
)
|
)
|
||||||
|
common_parser.add_argument(
|
||||||
|
'-f',
|
||||||
|
'--force',
|
||||||
|
action='store_true',
|
||||||
|
help=(
|
||||||
|
'generate documentation even if the output directory is not '
|
||||||
|
'empty. Warning: data can be lost when setting this flag.'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
argument_parser = argparse.ArgumentParser()
|
argument_parser = argparse.ArgumentParser()
|
||||||
|
|
||||||
@@ -150,6 +160,7 @@ def main(argv=None) -> int:
|
|||||||
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,
|
||||||
|
force=args.force,
|
||||||
)
|
)
|
||||||
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')
|
||||||
@@ -157,6 +168,8 @@ def main(argv=None) -> int:
|
|||||||
return error(f'{exc.filename}: not a directory')
|
return error(f'{exc.filename}: not a directory')
|
||||||
except IsADirectoryError as exc:
|
except IsADirectoryError as exc:
|
||||||
return error(f'{exc.filename}: is a directory')
|
return error(f'{exc.filename}: is a directory')
|
||||||
|
except DirectoryNotEmptyError as exc:
|
||||||
|
return error(f'{exc.filename}: directory not empty')
|
||||||
except FileExistsError as exc:
|
except FileExistsError as exc:
|
||||||
return error(f'{exc.filename}: file already exists')
|
return error(f'{exc.filename}: file already exists')
|
||||||
except PermissionError as exc:
|
except PermissionError as exc:
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ def _generate(
|
|||||||
),
|
),
|
||||||
allow_html: bool = False,
|
allow_html: bool = False,
|
||||||
allow_tables: bool = False,
|
allow_tables: bool = False,
|
||||||
|
force: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Generates the documentation for a Python project.
|
Generates the documentation for a Python project.
|
||||||
@@ -94,6 +95,9 @@ 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.
|
||||||
|
:param force: Indicates whether the output directory shall be
|
||||||
|
overwritten if it is not empty.
|
||||||
|
:type force: bool
|
||||||
"""
|
"""
|
||||||
|
|
||||||
docs_generator = PythonDocumentationGenerator(
|
docs_generator = PythonDocumentationGenerator(
|
||||||
@@ -103,7 +107,7 @@ def _generate(
|
|||||||
allow_html=allow_html,
|
allow_html=allow_html,
|
||||||
allow_tables=allow_tables,
|
allow_tables=allow_tables,
|
||||||
)
|
)
|
||||||
docs_generator.generate_documentation()
|
docs_generator.generate_documentation(force)
|
||||||
|
|
||||||
|
|
||||||
def generate(
|
def generate(
|
||||||
@@ -114,6 +118,7 @@ def generate(
|
|||||||
exclude_sections: list[str] = [],
|
exclude_sections: list[str] = [],
|
||||||
allow_html: bool = False,
|
allow_html: bool = False,
|
||||||
allow_tables: bool = False,
|
allow_tables: bool = False,
|
||||||
|
force: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Generates the documentation for a Python project.
|
Generates the documentation for a Python project.
|
||||||
@@ -133,6 +138,9 @@ 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.
|
||||||
|
:param force: Indicates whether the output directory shall be
|
||||||
|
overwritten if it is not empty.
|
||||||
|
:type force: bool
|
||||||
"""
|
"""
|
||||||
|
|
||||||
sections = parse_sections_list(include_sections, exclude_sections)
|
sections = parse_sections_list(include_sections, exclude_sections)
|
||||||
@@ -143,4 +151,5 @@ def generate(
|
|||||||
include_sections=sections,
|
include_sections=sections,
|
||||||
allow_html=allow_html,
|
allow_html=allow_html,
|
||||||
allow_tables=allow_tables,
|
allow_tables=allow_tables,
|
||||||
|
force=force,
|
||||||
)
|
)
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from . import namespaces
|
from . import namespaces
|
||||||
from ._core import *
|
from ._core import * # noqa: F403
|
||||||
from ._core import __all__ as _core__all__
|
from ._core import __all__ as _core__all__
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
|||||||
@@ -16,13 +16,18 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import ast
|
import ast
|
||||||
import pathlib
|
import pathlib
|
||||||
|
import shutil
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TypeAlias
|
from typing import TypeAlias
|
||||||
|
|
||||||
import docstring_parser
|
import docstring_parser
|
||||||
|
|
||||||
from ...exceptions import InvalidNamespaceError, PythonArgumentStructureError
|
from ...exceptions import (
|
||||||
|
DirectoryNotEmptyError,
|
||||||
|
InvalidNamespaceError,
|
||||||
|
PythonArgumentStructureError,
|
||||||
|
)
|
||||||
from ...utils import get_relative_path_segments, non_empty_str
|
from ...utils import get_relative_path_segments, non_empty_str
|
||||||
from .arguments import PythonArgumentKind, PythonFunctionArgument
|
from .arguments import PythonArgumentKind, PythonFunctionArgument
|
||||||
from .definitions import (
|
from .definitions import (
|
||||||
@@ -244,9 +249,8 @@ def _markdown_table(
|
|||||||
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" if header_len > 1 else ""
|
f'{"s" 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)
|
||||||
@@ -272,9 +276,8 @@ def _markdown_table(
|
|||||||
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" if header_len > 1 else ""
|
f'{"s" 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)
|
||||||
@@ -596,11 +599,10 @@ class PythonDefinitionDocumentationGenerator:
|
|||||||
|
|
||||||
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{
|
md += (
|
||||||
str(self.level + 1)
|
f'<details><summary><h{str(self.level + 1)} style="displa'
|
||||||
} style="display:inline">Docstring</h{
|
f'y:inline">Docstring</h{str(self.level + 1)}></summary>'
|
||||||
str(self.level + 1)
|
)
|
||||||
}></summary>'
|
|
||||||
else:
|
else:
|
||||||
md += self.level * '#' + '# Docstring\n\n'
|
md += self.level * '#' + '# Docstring\n\n'
|
||||||
|
|
||||||
@@ -1114,12 +1116,29 @@ class PythonDocumentationGenerator:
|
|||||||
|
|
||||||
return self._namespace(self.project_directory)
|
return self._namespace(self.project_directory)
|
||||||
|
|
||||||
def generate_documentation(self) -> None:
|
def generate_documentation(self, force: bool = False) -> 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.
|
||||||
|
|
||||||
|
:param force: Indicates whether existing contents of the output
|
||||||
|
directory shall be overwritten. If ``True``, all
|
||||||
|
contents of the output directory will be deleted
|
||||||
|
first.
|
||||||
|
:type force: bool
|
||||||
|
|
||||||
|
:raises DirectoryNotEmptyError: If ``force`` is ``False`` and
|
||||||
|
the output directory is not
|
||||||
|
empty.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if self.docs_directory.exists() and list(
|
||||||
|
self.docs_directory.iterdir()
|
||||||
|
):
|
||||||
|
if force:
|
||||||
|
shutil.rmtree(self.docs_directory)
|
||||||
|
else:
|
||||||
|
raise DirectoryNotEmptyError(str(self.docs_directory))
|
||||||
self.docs_directory.mkdir(parents=True, exist_ok=True)
|
self.docs_directory.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
_generate_package_namespace_documentation(
|
_generate_package_namespace_documentation(
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ class PythonPackageNamespace(PythonNamespace):
|
|||||||
# For performance reasons, is not called self.sub_namespace_names
|
# For performance reasons, is not called self.sub_namespace_names
|
||||||
# if the name attribute does not match, which can be quickly
|
# if the name attribute does not match, which can be quickly
|
||||||
# verified
|
# verified
|
||||||
if self.name != value.name:
|
if not (isinstance(value, type(self)) and self.name == value.name):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return value.sub_namespace_names() == self.sub_namespace_names()
|
return value.sub_namespace_names() == self.sub_namespace_names()
|
||||||
@@ -223,4 +223,4 @@ class PythonModuleNamespace(PythonNamespace):
|
|||||||
super().__init__(name)
|
super().__init__(name)
|
||||||
|
|
||||||
def __eq__(self, value: PythonModuleNamespace) -> bool:
|
def __eq__(self, value: PythonModuleNamespace) -> bool:
|
||||||
return self.name == value.name
|
return isinstance(value, type(self)) and self.name == value.name
|
||||||
@@ -12,6 +12,8 @@
|
|||||||
# 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 errno
|
||||||
|
|
||||||
|
|
||||||
class NamespaceError(ValueError):
|
class NamespaceError(ValueError):
|
||||||
"""
|
"""
|
||||||
@@ -26,9 +28,11 @@ class NamespaceError(ValueError):
|
|||||||
if not self.args:
|
if not self.args:
|
||||||
return ''
|
return ''
|
||||||
else:
|
else:
|
||||||
return f'{self.args[0]}{
|
return (
|
||||||
": " if self.namespace_identifier and self.args[0] else ""
|
self.args[0]
|
||||||
}{self.namespace_identifier if self.args[0] else ""}'
|
+ (': ' if self.namespace_identifier and self.args[0] else '')
|
||||||
|
+ (self.namespace_identifier if self.args[0] else '')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class InvalidNamespaceError(NamespaceError): ...
|
class InvalidNamespaceError(NamespaceError): ...
|
||||||
@@ -53,9 +57,11 @@ class PythonIdentifierError(ValueError):
|
|||||||
if not self.args:
|
if not self.args:
|
||||||
return ''
|
return ''
|
||||||
else:
|
else:
|
||||||
return f'{self.args[0]}{
|
return (
|
||||||
": " if self.identifier and self.args[0] else ""
|
self.args[0]
|
||||||
}{self.identifier if self.args[0] else ""}'
|
+ (': ' if self.identifier and self.args[0] else '')
|
||||||
|
+ (self.identifier if self.args[0] else '')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class InvalidPythonIdentifierError(PythonIdentifierError): ...
|
class InvalidPythonIdentifierError(PythonIdentifierError): ...
|
||||||
@@ -74,9 +80,11 @@ class PythonAnnotationError(ValueError):
|
|||||||
if not self.args:
|
if not self.args:
|
||||||
return ''
|
return ''
|
||||||
else:
|
else:
|
||||||
return f'{self.args[0]}{
|
return (
|
||||||
": " if self.annotation and self.args[0] else ""
|
self.args[0]
|
||||||
}{self.annotation if self.args[0] else ""}'
|
+ (': ' if self.annotation and self.args[0] else '')
|
||||||
|
+ (self.annotation if self.args[0] else '')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class InvalidPythonAnnotationError(PythonAnnotationError): ...
|
class InvalidPythonAnnotationError(PythonAnnotationError): ...
|
||||||
@@ -95,9 +103,20 @@ class PythonDefinitionError(ValueError):
|
|||||||
if not self.args:
|
if not self.args:
|
||||||
return ''
|
return ''
|
||||||
else:
|
else:
|
||||||
return f'{self.args[0]}{
|
return (
|
||||||
": " if self.definition and self.args[0] else ""
|
self.args[0]
|
||||||
}{self.definition if self.args[0] else ""}'
|
+ (': ' if self.definition and self.args[0] else '')
|
||||||
|
+ (self.definition if self.args[0] else '')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PythonArgumentStructureError(PythonDefinitionError): ...
|
class PythonArgumentStructureError(PythonDefinitionError): ...
|
||||||
|
|
||||||
|
|
||||||
|
class DirectoryNotEmptyError(OSError):
|
||||||
|
"""
|
||||||
|
Directory is not empty.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, filename: str | None = None) -> None:
|
||||||
|
super().__init__(errno.ENOTEMPTY, 'Directory not empty', filename)
|
||||||
@@ -20,7 +20,6 @@ import errno
|
|||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import types
|
import types
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from warnings import deprecated
|
from warnings import deprecated
|
||||||
@@ -154,10 +153,10 @@ def assert_that_is_instance(
|
|||||||
obj: object,
|
obj: object,
|
||||||
class_or_tuple: type | types.UnionType | tuple[type, ...],
|
class_or_tuple: type | types.UnionType | tuple[type, ...],
|
||||||
) -> None:
|
) -> None:
|
||||||
if not isinstance(class_or_tuple, Union[type, types.UnionType, tuple]):
|
if not isinstance(class_or_tuple, (type, types.UnionType, tuple)):
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
f"class_or_tuple: expected 'Union[type, types.UnionType, "
|
f"class_or_tuple: expected 'type | types.UnionType, tupl"
|
||||||
f"tuple[type, ...]]', 'got {type(class_or_tuple).__name__}'",
|
f"e[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)):
|
||||||
@@ -175,9 +174,8 @@ def assert_that_is_instance(
|
|||||||
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 '{
|
f'expected {exception_message_expected}'
|
||||||
type(obj).__name__
|
f", got '{type(obj).__name__}'"
|
||||||
}'",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
File renamed without changes.
@@ -13,6 +13,7 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import pathlib
|
import pathlib
|
||||||
|
import tempfile
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -174,7 +175,30 @@ def test_sections_success():
|
|||||||
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
|
||||||
|
(output_dir / 'test_dir').mkdir()
|
||||||
input_dir = pathlib.Path(__file__).parent / 'python_project/project'
|
input_dir = pathlib.Path(__file__).parent / 'python_project/project'
|
||||||
|
assert (
|
||||||
|
cli_main(
|
||||||
|
[
|
||||||
|
'python',
|
||||||
|
'-s',
|
||||||
|
'params',
|
||||||
|
'docstring',
|
||||||
|
'-HTfo',
|
||||||
|
str(output_dir),
|
||||||
|
str(input_dir),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
== 0
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_docs_generation_error():
|
||||||
|
with tempfile.TemporaryDirectory() as output_dir:
|
||||||
|
output_dir = pathlib.Path(output_dir)
|
||||||
|
(output_dir / 'test_dir').mkdir()
|
||||||
|
input_dir = pathlib.Path(__file__).parent / 'python_project/project'
|
||||||
|
assert (
|
||||||
cli_main(
|
cli_main(
|
||||||
[
|
[
|
||||||
'python',
|
'python',
|
||||||
@@ -186,3 +210,5 @@ def test_docs_generation(expectdir):
|
|||||||
str(input_dir),
|
str(input_dir),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
== 1
|
||||||
|
)
|
||||||
@@ -13,16 +13,16 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import pathlib
|
import pathlib
|
||||||
|
import tempfile
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
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,
|
||||||
)
|
)
|
||||||
|
from src.jcloud_docsgen.exceptions import DirectoryNotEmptyError
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@@ -31,7 +31,7 @@ from src.jcloud_docsgen.core.python.namespaces import (
|
|||||||
(
|
(
|
||||||
PythonDocumentationGenerator(
|
PythonDocumentationGenerator(
|
||||||
pathlib.Path(
|
pathlib.Path(
|
||||||
'tests/unit/core/python/_core/test_project_dirs/pdir_1'
|
'tests/unit/core/python/_core/test_project_dirs/pdir_1/src'
|
||||||
),
|
),
|
||||||
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/docs'
|
||||||
@@ -42,7 +42,7 @@ from src.jcloud_docsgen.core.python.namespaces import (
|
|||||||
(
|
(
|
||||||
PythonDocumentationGenerator(
|
PythonDocumentationGenerator(
|
||||||
pathlib.Path(
|
pathlib.Path(
|
||||||
'tests/unit/core/python/_core/test_project_dirs/pdir_2'
|
'tests/unit/core/python/_core/test_project_dirs/pdir_2/src'
|
||||||
),
|
),
|
||||||
pathlib.Path(
|
pathlib.Path(
|
||||||
'tests/unit/core/python/_core/test_project_dirs/pdir_2/docs'
|
'tests/unit/core/python/_core/test_project_dirs/pdir_2/docs'
|
||||||
@@ -58,7 +58,7 @@ from src.jcloud_docsgen.core.python.namespaces import (
|
|||||||
(
|
(
|
||||||
PythonDocumentationGenerator(
|
PythonDocumentationGenerator(
|
||||||
pathlib.Path(
|
pathlib.Path(
|
||||||
'tests/unit/core/python/_core/test_project_dirs/pdir_3'
|
'tests/unit/core/python/_core/test_project_dirs/pdir_3/src'
|
||||||
),
|
),
|
||||||
pathlib.Path(
|
pathlib.Path(
|
||||||
'tests/unit/core/python/_core/test_project_dirs/pdir_3/docs'
|
'tests/unit/core/python/_core/test_project_dirs/pdir_3/docs'
|
||||||
@@ -85,7 +85,9 @@ 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().sub_namespaces
|
||||||
|
) == expected
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@@ -120,7 +122,7 @@ def test_generate_documentation(dir, expectdir):
|
|||||||
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(True) # simplify the tests
|
||||||
|
|
||||||
|
|
||||||
def test_creates_directory():
|
def test_creates_directory():
|
||||||
@@ -130,7 +132,7 @@ def test_creates_directory():
|
|||||||
project_dir = pathlib.Path(project_dir)
|
project_dir = pathlib.Path(project_dir)
|
||||||
|
|
||||||
# The directory of the package
|
# The directory of the package
|
||||||
package_dir = project_dir / 'src' / 'pkg'
|
package_dir = project_dir / 'pkg'
|
||||||
assert not package_dir.exists()
|
assert not package_dir.exists()
|
||||||
package_dir.mkdir(parents=True, exist_ok=True)
|
package_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
@@ -144,3 +146,51 @@ def test_creates_directory():
|
|||||||
)
|
)
|
||||||
docs_generator.generate_documentation()
|
docs_generator.generate_documentation()
|
||||||
assert docs_dir.exists()
|
assert docs_dir.exists()
|
||||||
|
|
||||||
|
|
||||||
|
class TestForce:
|
||||||
|
def _prepare(self, directory):
|
||||||
|
directory = pathlib.Path(directory)
|
||||||
|
src_directory = directory / 'src'
|
||||||
|
src_directory.mkdir()
|
||||||
|
docs_directory = directory / 'docs'
|
||||||
|
docs_directory.mkdir()
|
||||||
|
return directory, src_directory, docs_directory
|
||||||
|
|
||||||
|
def test_overwrites_output_directory(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
directory, src_directory, docs_directory = self._prepare(directory)
|
||||||
|
test_dir = docs_directory / 'test_dir'
|
||||||
|
test_dir.mkdir()
|
||||||
|
documentation_generator = PythonDocumentationGenerator(
|
||||||
|
src_directory, docs_directory
|
||||||
|
)
|
||||||
|
documentation_generator.generate_documentation(True)
|
||||||
|
assert not test_dir.exists()
|
||||||
|
|
||||||
|
def test_error(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
directory, src_directory, docs_directory = self._prepare(directory)
|
||||||
|
(docs_directory / 'test_dir').mkdir()
|
||||||
|
documentation_generator = PythonDocumentationGenerator(
|
||||||
|
src_directory, docs_directory
|
||||||
|
)
|
||||||
|
with pytest.raises(DirectoryNotEmptyError) as exc_info:
|
||||||
|
documentation_generator.generate_documentation()
|
||||||
|
assert exc_info.value.filename == str(docs_directory)
|
||||||
|
|
||||||
|
def test_nothing_force(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
directory, src_directory, docs_directory = self._prepare(directory)
|
||||||
|
documentation_generator = PythonDocumentationGenerator(
|
||||||
|
src_directory, docs_directory
|
||||||
|
)
|
||||||
|
documentation_generator.generate_documentation(True)
|
||||||
|
|
||||||
|
def test_nothing_not_force(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
directory, src_directory, docs_directory = self._prepare(directory)
|
||||||
|
documentation_generator = PythonDocumentationGenerator(
|
||||||
|
src_directory, docs_directory
|
||||||
|
)
|
||||||
|
documentation_generator.generate_documentation(True)
|
||||||
File renamed without changes.
File renamed without changes.
+2
-2
@@ -125,12 +125,12 @@ Returns all users.
|
|||||||
##### Method signature
|
##### Method signature
|
||||||
|
|
||||||
```python
|
```python
|
||||||
list_all(self) -> List[User]
|
list_all(self) -> list[User]
|
||||||
```
|
```
|
||||||
|
|
||||||
##### Returns
|
##### Returns
|
||||||
|
|
||||||
Return type: `List[User]`
|
Return type: `list[User]`
|
||||||
|
|
||||||
##### Parameters
|
##### Parameters
|
||||||
|
|
||||||
|
|||||||
File renamed without changes.
+1
-2
@@ -1,5 +1,4 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -11,4 +10,4 @@ class User:
|
|||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
email: str
|
email: str
|
||||||
age: Optional[int] = None
|
age: int | None = None
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
|
from .exceptions import InvalidEmailError, UserNotFoundError
|
||||||
from .models import User
|
from .models import User
|
||||||
from .storage import InMemoryUserStorage
|
from .storage import InMemoryUserStorage
|
||||||
from .exceptions import UserNotFoundError, InvalidEmailError
|
|
||||||
from .utils import is_valid_email, normalize_name
|
from .utils import is_valid_email, normalize_name
|
||||||
|
|
||||||
|
|
||||||
+2
-3
@@ -1,4 +1,3 @@
|
|||||||
from typing import Dict, List
|
|
||||||
from .models import User
|
from .models import User
|
||||||
|
|
||||||
|
|
||||||
@@ -9,7 +8,7 @@ class InMemoryUserStorage:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._users: Dict[str, User] = {}
|
self._users: dict[str, User] = {}
|
||||||
|
|
||||||
def add(self, user: User) -> None:
|
def add(self, user: User) -> None:
|
||||||
"""Adds a user."""
|
"""Adds a user."""
|
||||||
@@ -23,6 +22,6 @@ class InMemoryUserStorage:
|
|||||||
"""Deletes a user."""
|
"""Deletes a user."""
|
||||||
del self._users[user_id]
|
del self._users[user_id]
|
||||||
|
|
||||||
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())
|
||||||
File renamed without changes.
@@ -292,22 +292,25 @@ def test_PythonDocumentationGenerator_collect_modules(
|
|||||||
None,
|
None,
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
"""pytest.mark.parametrize('python_documentation_gener"""
|
"pytest.mark.parametrize('python_documentatio"
|
||||||
"""ator,expected', [(PythonDocumentationGenerator(path"""
|
"n_generator,expected', [(PythonDocumentation"
|
||||||
"""lib.Path('tests/unit/core/python/_core/test_project"""
|
"Generator(pathlib.Path('tests/unit/core/pyth"
|
||||||
"""_dirs/pdir_1'), pathlib.Path('tests/unit/core/pytho"""
|
"on/_core/test_project_dirs/pdir_1'), pathlib"
|
||||||
"""n/_core/test_project_dirs/pdir_1/docs')), []), (Pyt"""
|
".Path('tests/unit/core/python/_core/test_pro"
|
||||||
"""honDocumentationGenerator(pathlib.Path('tests/unit/"""
|
"ject_dirs/pdir_1/docs')), []), (PythonDocume"
|
||||||
"""core/python/_core/test_project_dirs/pdir_2'), pathl"""
|
"ntationGenerator(pathlib.Path('tests/unit/co"
|
||||||
"""ib.Path('tests/unit/core/python/_core/test_project_"""
|
"re/python/_core/test_project_dirs/pdir_2'), "
|
||||||
"""dirs/pdir_2/docs')), [PythonPackageNamespace('pkg',"""
|
"pathlib.Path('tests/unit/core/python/_core/t"
|
||||||
""" [PythonModuleNamespace('module')])]), (PythonDocum"""
|
"est_project_dirs/pdir_2/docs')), [PythonPack"
|
||||||
"""entationGenerator(pathlib.Path('tests/unit/core/pyt"""
|
"ageNamespace('pkg', [PythonModuleNamespace('"
|
||||||
"""hon/_core/test_project_dirs/pdir_3'), pathlib.Path("""
|
"module')])]), (PythonDocumentationGenerator("
|
||||||
"""'tests/unit/core/python/_core/test_project_dirs/pdi"""
|
"pathlib.Path('tests/unit/core/python/_core/t"
|
||||||
"""r_3/docs')), [PythonPackageNamespace('pkg', [Python"""
|
"est_project_dirs/pdir_3'), pathlib.Path('tes"
|
||||||
"""ModuleNamespace(\'module'), PythonPackageNamespace("""
|
'ts/unit/core/python/_core/test_project_dirs/'
|
||||||
"""\'pkg\', [PythonModuleNamespace(\'module2')])])])])"""
|
"pdir_3/docs')), [PythonPackageNamespace('pkg"
|
||||||
|
"', [PythonModuleNamespace('module'), Python"
|
||||||
|
"PackageNamespace('pkg', [PythonModuleNames"
|
||||||
|
"pace('module2')])])])])"
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -14,7 +14,11 @@
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from src.jcloud_docsgen.core.python.namespaces import PythonModuleNamespace
|
from src.jcloud_docsgen.core.python.namespaces import (
|
||||||
|
PythonModuleNamespace,
|
||||||
|
PythonNamespace,
|
||||||
|
PythonPackageNamespace,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class StrSubclass(str): ...
|
class StrSubclass(str): ...
|
||||||
@@ -42,6 +46,10 @@ def test_PythonModuleNamespace_name_attribute(namespace, expected_name):
|
|||||||
(PythonModuleNamespace('a'), PythonModuleNamespace('b'), False),
|
(PythonModuleNamespace('a'), PythonModuleNamespace('b'), False),
|
||||||
(PythonModuleNamespace('0'), PythonModuleNamespace('-0'), False),
|
(PythonModuleNamespace('0'), PythonModuleNamespace('-0'), False),
|
||||||
(PythonModuleNamespace('1'), PythonModuleNamespace('a'), False),
|
(PythonModuleNamespace('1'), PythonModuleNamespace('a'), False),
|
||||||
|
(PythonModuleNamespace('a'), 'a', False),
|
||||||
|
(PythonModuleNamespace('a'), 'b', False),
|
||||||
|
(PythonModuleNamespace('a'), PythonPackageNamespace('a', []), False),
|
||||||
|
(PythonModuleNamespace('a'), PythonNamespace('a'), False),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_PythonModuleNamespace___eq__(namespace1, namespace2, expected):
|
def test_PythonModuleNamespace___eq__(namespace1, namespace2, expected):
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import pytest
|
|||||||
|
|
||||||
from src.jcloud_docsgen.core.python.namespaces import (
|
from src.jcloud_docsgen.core.python.namespaces import (
|
||||||
PythonModuleNamespace,
|
PythonModuleNamespace,
|
||||||
|
PythonNamespace,
|
||||||
PythonPackageNamespace,
|
PythonPackageNamespace,
|
||||||
)
|
)
|
||||||
from src.jcloud_docsgen.exceptions import (
|
from src.jcloud_docsgen.exceptions import (
|
||||||
@@ -256,6 +257,26 @@ def test_PythonPackageNamespace_sub_namespace_names(namespace, expected):
|
|||||||
PythonPackageNamespace('b', []),
|
PythonPackageNamespace('b', []),
|
||||||
False,
|
False,
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
PythonPackageNamespace('a', []),
|
||||||
|
'a',
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
PythonPackageNamespace('a', []),
|
||||||
|
'b',
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
PythonPackageNamespace('a', []),
|
||||||
|
PythonModuleNamespace('a'),
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
PythonPackageNamespace('a', []),
|
||||||
|
PythonNamespace('a'),
|
||||||
|
False,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_PythonPackageNamespace___eq__(namespace1, namespace2, expected):
|
def test_PythonPackageNamespace___eq__(namespace1, namespace2, expected):
|
||||||
|
|||||||
Reference in new issue
Block a user