80 Commits
Author SHA1 Message Date
jakob.scheid 44f80dbb6a Update licenses 2026-04-30 19:28:53 +02:00
jakob.scheid 14380b6515 Update licenses 2026-04-30 19:27:32 +02:00
jakob.scheid d3bb51eb61 Rephrase a sentence 2026-04-27 23:01:39 +02:00
jakob.scheid b4f4c4860a Rephrase a sentence 2026-04-27 23:00:54 +02:00
jakob.scheid 86c012ef5c Fatal bug fix: Add quotation mark 2026-04-27 22:47:08 +02:00
jakob.scheid a74a500e7e Prepare first release 2026-04-27 22:45:45 +02:00
jakob.scheid 16ce11b852 Prepare first release 2026-04-27 22:44:30 +02:00
jakob.scheid cf8875463c Add README 2026-04-27 22:43:52 +02:00
jakob.scheid 81b2be7efc Add API reference 2026-04-27 22:43:26 +02:00
jakob.scheid 55c4c74ee8 Remove test dependencies 2026-04-27 22:30:24 +02:00
jakob.scheid 3315448fc5 Add tests for the Python project documentation generator 2026-04-27 22:17:29 +02:00
jakob.scheid ce244df494 Add Python project documentation generator 2026-04-27 22:15:40 +02:00
jakob.scheid d12263f552 Add license for pytest-expectdir 2026-04-27 22:13:22 +02:00
jakob.scheid 22f3233381 Add internal function to generate the documentation of a package namespace 2026-04-26 22:23:18 +02:00
jakob.scheid a17e56fb3d Add Python module documentation generator 2026-04-25 19:34:34 +02:00
jakob.scheid ecd3f7e76b Generalize tests for core.python.PythonDocumentationGenerator exception 2026-04-19 19:51:50 +02:00
jakob.scheid f581774649 Add function to get the path segments of a path relative to another 2026-04-19 19:38:30 +02:00
jakob.scheid 3fd51d126f Add Python definition documentation generator 2026-04-19 19:12:33 +02:00
jakob.scheid 3b45c1a8ed Add function to ensure a non-empty string. 2026-04-19 19:10:45 +02:00
jakob.scheid b71b93000e Add feature to parse Python docstrings 2026-04-13 14:44:04 +02:00
jakob.scheid 16852f3d68 Add class for Python docstrings 2026-04-13 14:15:49 +02:00
jakob.scheid 7f02b5b7ec Add tests for core.python.arguments.PythonFunctionArgument.signature_repr 2026-04-13 12:24:52 +02:00
jakob.scheid a8626418da Add feature to get the signature representation of a Python function argument 2026-04-13 12:19:08 +02:00
jakob.scheid 10707d680d Update licenses 2026-04-13 10:45:05 +02:00
jakob.scheid 42a568223b Update licenses 2026-04-13 09:43:40 +02:00
jakob.scheid 72b1024a16 Bug fix: update NOTICE 2026-04-13 09:33:46 +02:00
jakob.scheid 3bc7953541 Bug fix: update NOTICE 2026-04-13 09:33:23 +02:00
jakob.scheid d5f9368d04 Bug fix: update pyproject.toml 2026-04-13 09:32:51 +02:00
jakob.scheid a79b538de8 Add class for Python modules 2026-04-13 09:24:36 +02:00
jakob.scheid 8c3866a948 Bug fix: remove invalid test cases 2026-04-12 19:40:32 +02:00
jakob.scheid ed3b47bd81 Bug fix: add support for default values in core.python.arguments.PythonASTArgumentsListParser.to_argument_list 2026-04-12 19:30:04 +02:00
jakob.scheid e4fe88b60b Remove core.python.arguments.PythonFunctionArgumentDefault; use strings instead 2026-04-12 19:13:39 +02:00
jakob.scheid 73730520c1 Remove core.python.arguments.PythonFunctionArgumentDefault; use strings instead 2026-04-12 18:54:51 +02:00
jakob.scheid 076e04a67a Add classes for Python definitions 2026-04-12 18:49:32 +02:00
jakob.scheid d6a35ea222 Add classes for Python definitions 2026-04-12 18:49:23 +02:00
jakob.scheid d8fd2b1948 Bug fix: core.python.arguments.PythonASTArgumentsListParser.to_argument_list failed if a type annotation is not a single name 2026-04-11 15:56:14 +02:00
jakob.scheid 29714798f7 Fix spelling mistakes 2026-04-11 15:21:34 +02:00
jakob.scheid 91cecae230 Add classes for Python function arguments 2026-04-11 13:33:42 +02:00
jakob.scheid d0e4ae54c3 Bug fix: remove debug output 2026-04-10 17:19:35 +02:00
jakob.scheid d3e022fb13 Add licenses to the tests 2026-04-10 12:37:59 +02:00
jakob.scheid 3e96072361 Add class for existing files 2026-04-09 19:52:46 +02:00
jakob.scheid a550ba6832 Add class for existing files 2026-04-09 19:51:39 +02:00
jakob.scheid 74cb599ed6 Bug fix: add .gitkeep files in the empty directory in tests/unit/utils/test_directory 2026-04-09 19:45:26 +02:00
jakob.scheid 9d86a64418 Bug fix: add .gitkeep files in every empty directory in tests/unit/core/python/_core/test_project_dirs 2026-04-09 19:40:31 +02:00
jakob.scheid 8f48f591e6 Convert project and docs directory to pathlib.Path objects at the constructor of core.python._core.PythonDocumentationGenerator 2026-04-09 19:39:15 +02:00
jakob.scheid ff45f836b3 Add module cli 2026-04-09 19:21:25 +02:00
jakob.scheid 9e2fe5f2df Change python package namespace sub namespaces to list 2026-04-09 17:32:54 +02:00
jakob.scheid 9e524bec03 Change python package namespace sub namespaces to list 2026-04-09 16:08:07 +02:00
jakob.scheid 512a07a773 Bug fix 2026-04-09 15:59:31 +02:00
jakob.scheid d0e2855b58 Add PythonDocumentationGenerator method to collect all namespaces 2026-04-08 14:24:44 +02:00
jakob.scheid 54e3a30ef0 Add string representation for python namespace objects 2026-04-08 14:13:12 +02:00
jakob.scheid 66f06960a9 Add feature to compare core.python.namespaces.PythonPackageNamespace instances or core.python.namespaces.PythonModuleNamespace instances 2026-04-08 13:33:12 +02:00
jakob.scheid 286ba68c34 Do not permit empty python namespace names 2026-04-08 13:28:48 +02:00
jakob.scheid 2cac1727e1 Add feature to get the names of all sub-namespaces of a core.python.namespaces.PythonPackageNamespace 2026-04-08 13:26:35 +02:00
jakob.scheid 0dcf34c068 Add tests for core.python.namespaces.PythonPackageNamespace 2026-04-07 22:58:29 +02:00
jakob.scheid 6cb5d25f5c Add checking whether sub namespaces are not empty in core.python.namespaces.PythonPackageNamespace.namespace 2026-04-07 22:57:18 +02:00
jakob.scheid fa75b6f036 Update docstring of core.python.namespaces.PythonPackageNamespace.namespace 2026-04-07 16:25:01 +02:00
jakob.scheid 1c0f0ba81a Bug fix: add exceptions for namespaces 2026-04-07 16:06:18 +02:00
jakob.scheid 45b488b8c2 Add tests for core.python.namespaces.PythonModuleNamespace 2026-04-07 16:03:23 +02:00
jakob.scheid f3523549a9 Add tests for core.python.namespaces.PythonNamespace 2026-04-07 16:03:06 +02:00
jakob.scheid e2a4db89ba Add tests for core.python.namespaces.PythonNamespace 2026-04-07 15:58:49 +02:00
jakob.scheid b0a0d3e45b Add type checking for the name attribute at the constructor of core.python.namespaces.PythonNamespace 2026-04-07 14:22:09 +02:00
jakob.scheid 0f817e961c Add python namespaces classes 2026-04-07 14:18:54 +02:00
jakob.scheid e2ebde7cc4 Add __all__ variable in core.python.namespaces 2026-04-07 14:17:54 +02:00
jakob.scheid 57a395727d Add tests for the PythonDocumentationGenerator class constructor type exceptions 2026-04-07 14:14:47 +02:00
jakob.scheid 6119c69a3c Add namespaces classes 2026-04-07 14:13:15 +02:00
jakob.scheid 6f23068e0b Change package structure 2026-04-07 14:11:48 +02:00
jakob.scheid 2f6e373ba7 Change package structure 2026-04-07 14:11:15 +02:00
jakob.scheid 5087645069 Add python project documentation generator class 2026-04-07 12:28:38 +02:00
jakob.scheid 4988491018 Add __init__.py 2026-04-07 12:26:31 +02:00
jakob.scheid 9af9e18304 Rename module _utils to utils 2026-04-07 12:23:30 +02:00
jakob.scheid 7f5430188d Add function to assert that an object is an instance of a specific type 2026-04-07 12:16:07 +02:00
jakob.scheid 47ee0f23b3 Add pyproject.toml 2026-04-07 12:15:14 +02:00
jakob.scheid 7423f19cfd Rename module _utils to utils 2026-04-07 12:14:43 +02:00
jakob.scheid 1a6208888e Add function to assert that an object is an instance of a specific type 2026-04-07 12:14:01 +02:00
jakob.scheid bdf7cb4af7 Add function to format a list human-readable 2026-04-06 19:04:09 +02:00
jakob.scheid 157c8712b2 Rename module _utils to utils 2026-04-06 13:36:25 +02:00
jakob.scheid cc346c983c Rename module _utils to utils 2026-04-06 12:45:33 +02:00
jakob.scheid d79a22e98f Add subclass of pathlib.Path for existing directories 2026-04-05 20:15:33 +02:00
jakob.scheid 00fbd89746 Add licenses 2026-04-05 20:06:05 +02:00
92 changed files with 10156 additions and 1 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright 2026 jCloud Services GbR
Copyright 2026 jCloud
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+41
View File
@@ -0,0 +1,41 @@
Third-Party Notices
===================
This software uses the following third-party libraries. Full license texts are available in the 'licenses/' directory.
1. docstring-parser 0.17.0
Author: Marcin Kurczewski
License: MIT License
See licenses/docstring-parser.txt for full license text.
2. iniconfig 2.3.0
Author: Holger Krekel and others
License: MIT License
See licenses/iniconfig.txt for full license text.
3. packaging 26.0
Author: Donald Stufft and individual contributors
License: Apache License 2.0
URL: None
See licenses/packaging.txt for full license text.
4. pluggy 1.6.0
Author: Holger Krekel
License: MIT License
See licenses/pluggy.txt for full license text.
5. pygments 2.18.0
Author: Georg Brandl, Tim Hatch, Armin Ronacher, Others
License: BSD-2-Clause
See licenses/pygments.txt for full license text.
6. pytest 9.0.2
Author: Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin, Others
License: MIT License
See licenses/pytest.txt for full license text.
7. pytest-expectdir 1.2.0
Author: Léo Falventin Hauchecorne
License: MIT
URL: https://github.com/hl037/pytest-expectdir
See licenses/pytest-expectdir.txt for full license text.
+22
View File
@@ -0,0 +1,22 @@
# jcloud-docsgen
A tool that generates Markdown documentation from software project source code.
Currently supported programming languages:
- Python
## Installation
You can install it using pip:
```bash
pip install jcloud-docsgen --index-url https://repo.jcloud-services.ddns.net/simple/ --extra-index-url https://pypi.org/simple/
```
## Reference
API reference and generated documentation can be found in [docs/reference](docs/reference).
## Notice
This project is under active development. The documentation will be expanded over time.
+29
View File
@@ -0,0 +1,29 @@
# Changelog
## [Unreleased]
### Added
- Add class for existing directories
- Add function to format a list human-readable
- Add function to assert that an object is an instance of a specific type
- Add Python project documentation generator class
- Add Python namespaces classes
- Add type checking for the `name` attribute at the constructor of `core.python.namespaces.PythonNamespace`
- Add check whether sub-namespaces are not empty in `core.python.namespaces.PythonPackageNamespace.namespace`
- Add feature to get the names of all sub-namespaces of a `core.python.namespaces.PythonPackageNamespace`
- Add feature to compare `core.python.namespaces.PythonPackageNamespace` instances or `core.python.namespaces.PythonModuleNamespace` instances
- Add string representation for Python namespace objects
- Add `PythonDocumentationGenerator` method to collect all namespaces
- Add class for existing files
- Add classes for Python function arguments
- Add classes for Python definitions
- Add class for Python modules
- Add feature to get the signature representation of a Python function argument
- Add class for Python docstrings
- Add feature to parse Python docstrings
- Add function to ensure a non-empty string.
- Add Python definition documentation generator
- Add function to get the path segments of a path relative to another
- Add Python module documentation generator
- Add Python project documentation generator
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,205 @@
# Module `jcloud_docsgen.core.python.arguments`
## Class `PythonArgumentKind`
### Base classes
- `Enum`
## Class `PythonFunctionArgument`
Represents an argument of a Python function.
### Body
#### Constructor (method `__init__`)
##### Method signature
```python
__init__(self, name: str, kind: PythonArgumentKind, default: Union[str, None], annotation: Union[str, None]) -> None
```
##### Returns
Return type: `None`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
| `name` | `str` | normal | -- | -- |
| `kind` | `PythonArgumentKind` | normal | -- | -- |
| `default` | `Union[str, None]` | normal | -- | -- |
| `annotation` | `Union[str, None]` | normal | -- | -- |
#### Method `__eq__`
##### Method signature
```python
__eq__(self, value: PythonFunctionArgument) -> bool
```
##### Returns
Return type: `bool`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
| `value` | `PythonFunctionArgument` | normal | -- | -- |
#### Method `__repr__`
##### Method signature
```python
__repr__(self) -> str
```
##### Returns
Return type: `str`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
#### Method `signature_repr`
Returns the function signature representation of the argument.
##### Method signature
```python
signature_repr(self) -> str
```
##### Returns
The string representation of the argument.
Return type: `str`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
##### Docstring
Returns the function signature representation of the argument.
:return: The string representation of the argument.
:rtype: str
### Docstring
Represents an argument of a Python function.
:param name: The name of the argument.
:type name: str
:param kind: The kind of the argument.
:type kind: ArgumentKind
:param default: The default value of the argument.
:type default: Union[str, None]
:param annotation: The type annotation of the argument.
:type annotation: Union[str, None]
## Class `PythonASTArgumentsListParser`
A parser for making ``PythonFunctionArgument`` objects from an
``ast.arguments`` arguments list.
### Body
#### Constructor (method `__init__`)
##### Method signature
```python
__init__(self, ast_arguments_list: ast.arguments) -> None
```
##### Returns
Return type: `None`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
| `ast_arguments_list` | `ast.arguments` | normal | -- | -- |
#### Method `to_argument_list`
Converts the AST arguments list to a list of Python function
argument objects.
##### Method signature
```python
to_argument_list(self) -> list[PythonFunctionArgument]
```
##### Returns
The list of Python function argument objects.
Return type: `list[PythonFunctionArgument]`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
##### Docstring
Converts the AST arguments list to a list of Python function
argument objects.
:return: The list of Python function argument objects.
:rtype: list[PythonFunctionArgument]
### Docstring
A parser for making ``PythonFunctionArgument`` objects from an
``ast.arguments`` arguments list.
:param ast_arguments_list: The ``ast.arguments`` arguments list.
:type ast_arguments_list: ast.arguments
@@ -0,0 +1,445 @@
# Module `jcloud_docsgen.core.python.definitions`
## Function `_python_docstring`
Returns the PythonDocstring object if the docstring is a string and
not ``None``.
### Function signature
```python
_python_docstring(doc: Union[str, None]) -> Union[PythonDocstring, None]
```
### Returns
The PythonDocstring object or ``None``.
Return type: `Union[PythonDocstring, None]`
### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `doc` | `Union[str, None]` | normal | -- | The docstring. |
### Docstring
Returns the PythonDocstring object if the docstring is a string and
not ``None``.
:param doc: The docstring.
:type doc: Union[str, None]
:return: The PythonDocstring object or ``None``.
:rtype: Union[PythonDocstring, None]
## Class `PythonDefinition`
A base class for Python definition (a class, function or asynchronous
function definition) classes.
### Body
#### Constructor (method `__init__`)
##### Method signature
```python
__init__(self, name: str, doc: Union[PythonDocstring, None], decorators: list[str]) -> None
```
##### Returns
Return type: `None`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
| `name` | `str` | normal | -- | -- |
| `doc` | `Union[PythonDocstring, None]` | normal | -- | -- |
| `decorators` | `list[str]` | normal | -- | -- |
#### Method `_arg_list`
##### Method signature
```python
_arg_list(self) -> tuple
```
##### Returns
Return type: `tuple`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
#### Method `__repr__`
##### Method signature
```python
__repr__(self) -> str
```
##### Returns
Return type: `str`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
#### Method `from_node`
From an AST node.
##### Method signature
```python
from_node(cls, node: ast.stmt) -> PythonDefinition
```
##### Returns
Return type: `PythonDefinition`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `cls` | -- | normal | -- | -- |
| `node` | `ast.stmt` | normal | -- | The AST node. |
##### Decorators
```python
classmethod
```
##### Docstring
From an AST node.
:param node: The AST node.
:type node: ast.stmt
#### Method `__eq__`
##### Method signature
```python
__eq__(self, value: object) -> bool
```
##### Returns
Return type: `bool`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
| `value` | `object` | normal | -- | -- |
### Docstring
A base class for Python definition (a class, function or asynchronous
function definition) classes.
:param name: The name of the definition.
:type name: str
:param doc: The docstring.
:type doc: Union[PythonDocstring, None]
:param decorators: The definition decorators.
:type decorators: list[str]
:raises InvalidPythonIdentifierError: If the name is not a valid
Python identifier.
## Class `PythonFunctionDefinition`
A PythonDefinition subclass representing a Python function
definition.
### Base classes
- `PythonDefinition`
### Body
#### Constructor (method `__init__`)
##### Method signature
```python
__init__(self, name: str, args: list[PythonFunctionArgument], returns: Union[str, None], doc: Union[PythonDocstring, None], decorators: list[str]) -> None
```
##### Returns
Return type: `None`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
| `name` | `str` | normal | -- | -- |
| `args` | `list[PythonFunctionArgument]` | normal | -- | -- |
| `returns` | `Union[str, None]` | normal | -- | -- |
| `doc` | `Union[PythonDocstring, None]` | normal | -- | -- |
| `decorators` | `list[str]` | normal | -- | -- |
#### Method `_arg_list`
##### Method signature
```python
_arg_list(self) -> tuple
```
##### Returns
Return type: `tuple`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
#### Method `from_node`
From an AST node.
##### Method signature
```python
from_node(cls, node: ast.stmt) -> PythonFunctionDefinition
```
##### Returns
Return type: `PythonFunctionDefinition`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `cls` | -- | normal | -- | -- |
| `node` | `ast.stmt` | normal | -- | The AST node. |
##### Decorators
```python
classmethod
```
##### Docstring
From an AST node.
:param node: The AST node.
:type node: ast.stmt
### Docstring
A PythonDefinition subclass representing a Python function
definition.
:param name: The name of the definition.
:type name: str
:param args: The arguments.
:type args: list[PythonFunctionArgument]
:param returns: The return type of the function or class.
:type returns: Union[str, None]
:param doc: The docstring.
:type doc: Union[PythonDocstring, None]
:param decorators: The definition decorators.
:type decorators: list[str]
:raises InvalidPythonIdentifierError: If the name is not a valid
Python identifier.
## Class `PythonAsyncFunctionDefinition`
A PythonDefinition subclass representing an asynchronous Python function
definition.
### Base classes
- `PythonFunctionDefinition`
### Docstring
A PythonDefinition subclass representing an asynchronous Python function
definition.
:param name: The name of the definition.
:type name: str
:param args: The arguments.
:type args: list[PythonFunctionArgument]
:param returns: The return type of the function or class.
:type returns: Union[str, None]
:param doc: The docstring.
:type doc: Union[PythonDocstring, None]
:param decorators: The definition decorators.
:type decorators: list[str]
:raises InvalidPythonIdentifierError: If the name is not a valid
Python identifier.
## Class `PythonClassDefinition`
A PythonDefinition subclass representing a Python class definition.
### Base classes
- `PythonDefinition`
### Body
#### Constructor (method `__init__`)
##### Method signature
```python
__init__(self, name: str, bases: list[str], doc: Union[PythonDocstring, None], decorators: list[str], body: list[PythonDefinition]) -> None
```
##### Returns
Return type: `None`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
| `name` | `str` | normal | -- | -- |
| `bases` | `list[str]` | normal | -- | -- |
| `doc` | `Union[PythonDocstring, None]` | normal | -- | -- |
| `decorators` | `list[str]` | normal | -- | -- |
| `body` | `list[PythonDefinition]` | normal | -- | -- |
#### Method `_arg_list`
##### Method signature
```python
_arg_list(self) -> tuple
```
##### Returns
Return type: `tuple`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
#### Method `from_node`
From an AST node.
##### Method signature
```python
from_node(cls, node: ast.stmt) -> PythonClassDefinition
```
##### Returns
Return type: `PythonClassDefinition`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `cls` | -- | normal | -- | -- |
| `node` | `ast.stmt` | normal | -- | The AST node. |
##### Decorators
```python
classmethod
```
##### Docstring
From an AST node.
:param node: The AST node.
:type node: ast.stmt
### Docstring
A PythonDefinition subclass representing a Python class definition.
:param name: The name of the definition.
:type name: str
:param bases: The base classes.
:type bases: list[str]
:param doc: The docstring.
:type doc: Union[PythonDocstring, None]
:param decorators: The definition decorators.
:type decorators: list[str]
:param body:
:type body: list[PythonDefinition]
:raises InvalidPythonIdentifierError: If the name is not a valid
Python identifier.
@@ -0,0 +1,113 @@
# Module `jcloud_docsgen.core.python.docstrings`
## Class `PythonDocstring`
Represents a Python docstring.
### Body
#### Constructor (method `__init__`)
##### Method signature
```python
__init__(self, docstring: str) -> None
```
##### Returns
Return type: `None`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
| `docstring` | `str` | normal | -- | -- |
#### Method `__repr__`
##### Method signature
```python
__repr__(self) -> str
```
##### Returns
Return type: `str`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
#### Method `__eq__`
##### Method signature
```python
__eq__(self, value) -> bool
```
##### Returns
Return type: `bool`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
| `value` | -- | normal | -- | -- |
#### Method `parse`
Parses the docstring.
##### Method signature
```python
parse(self) -> docstring_parser.Docstring
```
##### Returns
The parsed docstring.
Return type: `docstring_parser.Docstring`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
##### Docstring
Parses the docstring.
:return: The parsed docstring.
:rtype: docstring_parser.Docstring
### Docstring
Represents a Python docstring.
:param docstring: The docstring as a string.
:type docstring: str
@@ -0,0 +1,442 @@
# Module `jcloud_docsgen.core.python.namespaces`
## Class `PythonNamespace`
The base class for all Python namespaces such as modules or packages.
### 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 `__repr__`
##### Method signature
```python
__repr__(self) -> str
```
##### Returns
Return type: `str`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
### Docstring
The base class for all Python namespaces such as modules or packages.
:param name: The name of the namespace.
:type name: str
## Class `PythonPackageNamespace`
A subclass of ``PythonNamespace`` for Python package namespaces.
### Base classes
- `PythonNamespace`
### Body
#### Constructor (method `__init__`)
##### Method signature
```python
__init__(self, name: str, sub_namespaces: List[PythonNamespace]) -> None
```
##### Returns
Return type: `None`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
| `name` | `str` | normal | -- | -- |
| `sub_namespaces` | `List[PythonNamespace]` | normal | -- | -- |
#### Method `namespace`
Returns the namespace object with a specific identifier, such as
``['package', 'subpackage', 'module']``.
##### Method signature
```python
namespace(self, sub_namespaces: List[str]) -> PythonNamespace
```
##### Returns
The namespace.
Return type: `PythonNamespace`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
| `sub_namespaces` | `List[str]` | normal | -- | The identifier of the namespace. |
##### Exceptions
| Exception | Description |
| --- | --- |
| `InvalidNamespaceError` | If the namespace identifier is invalid. |
| `NamespaceNotFoundError` | If the namespace does not exist. |
##### Docstring
Returns the namespace object with a specific identifier, such as
``['package', 'subpackage', 'module']``.
:param sub_namespaces: The identifier of the namespace.
:type sub_namespaces: list[str]
:raises InvalidNamespaceError: If the namespace identifier is
invalid.
:raises NamespaceNotFoundError: If the namespace does not exist.
:return: The namespace.
:rtype: PythonNamespace
#### Method `_sub_namespaces_names`
Returns the names of all sub-namespaces of the specified package
namespace and keeps the package structure.
##### Method signature
```python
_sub_namespaces_names(self, package: PythonPackageNamespace) -> PythonPackageNamespaceDictType
```
##### Returns
The names if all sub-namespaces.
Return type: `PythonPackageNamespaceDictType`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
| `package` | `PythonPackageNamespace` | normal | -- | The package. |
##### Docstring
Returns the names of all sub-namespaces of the specified package
namespace and keeps the package structure.
:param package: The package.
:type package: PythonPackageNamespace
:return: The names if all sub-namespaces.
:rtype: PythonPackageNamespaceDictType
#### Method `sub_namespace_names`
Returns the names of all sub-namespaces and keeps the package
structure.
##### Method signature
```python
sub_namespace_names(self) -> PythonPackageNamespaceDictType
```
##### Returns
The names if all sub-namespaces.
Return type: `PythonPackageNamespaceDictType`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
##### Docstring
Returns the names of all sub-namespaces and keeps the package
structure.
:return: The names if all sub-namespaces.
:rtype: PythonPackageNamespaceDictType
#### Method `__eq__`
##### Method signature
```python
__eq__(self, value: PythonPackageNamespace) -> bool
```
##### Returns
Return type: `bool`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
| `value` | `PythonPackageNamespace` | normal | -- | -- |
#### Method `__repr__`
##### Method signature
```python
__repr__(self) -> str
```
##### Returns
Return type: `str`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
#### Method `__getitem__`
Returns a namespace with the specified name.
##### Method signature
```python
__getitem__(self, name: str) -> PythonNamespace
```
##### Returns
The namespace
Return type: `PythonNamespace`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
| `name` | `str` | normal | -- | The name of the namespace |
##### Exceptions
| Exception | Description |
| --- | --- |
| `NamespaceNotFoundError` | If the namespace does not exist. |
##### Docstring
Returns a namespace with the specified name.
:param name: The name of the namespace
:type name: str
:raises NamespaceNotFoundError: If the namespace does not exist.
:return: The namespace
:rtype: PythonNamespace
#### Method `add`
Adds a namespace.
##### Method signature
```python
add(self, namespace: PythonNamespace) -> None
```
##### Returns
Return type: `None`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
| `namespace` | `PythonNamespace` | normal | -- | The namespace to add. |
##### Exceptions
| Exception | Description |
| --- | --- |
| `NamespaceExistsError` | If a namespace with the name of the namespace that is attempted to add already exists. |
##### Docstring
Adds a namespace.
:param namespace: The namespace to add.
:type namespace: PythonNamespace
:raises NamespaceExistsError: If a namespace with the name of the
namespace that is attempted to add
already exists.
#### Method `sub_namespaces`
##### Method signature
```python
sub_namespaces(self) -> List[PythonNamespace]
```
##### Returns
Return type: `List[PythonNamespace]`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
##### Decorators
```python
property
```
### Docstring
A subclass of ``PythonNamespace`` for Python package namespaces.
:param name: The name of the namespace.
:type name: str
:param sub_namespaces: The sub-namespaces.
:type sub_namespaces: list[PythonNamespace]
## Class `PythonModuleNamespace`
A subclass of ``PythonNamespace`` for Python module namespaces.
### Base classes
- `PythonNamespace`
### 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 `__eq__`
##### Method signature
```python
__eq__(self, value: PythonModuleNamespace) -> bool
```
##### Returns
Return type: `bool`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
| `value` | `PythonModuleNamespace` | normal | -- | -- |
### Docstring
A subclass of ``PythonNamespace`` for Python module namespaces.
:param name: The name of the namespace.
:type name: str
+253
View File
@@ -0,0 +1,253 @@
# Module `jcloud_docsgen.exceptions`
## Class `NamespaceError`
Base class for namespace errors.
### Base classes
- `ValueError`
### Body
#### Constructor (method `__init__`)
##### Method signature
```python
__init__(self, *args: object, namespace_identifier: str = '') -> None
```
##### Returns
Return type: `None`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
| `args` | `object` | vararg | -- | -- |
| `namespace_identifier` | `str` | keyword-only | `''` | -- |
#### Method `__str__`
##### Method signature
```python
__str__(self)
```
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
### Docstring
Base class for namespace errors.
## Class `InvalidNamespaceError`
### Base classes
- `NamespaceError`
## Class `NamespaceNotFoundError`
### Base classes
- `NamespaceError`
## Class `NamespaceExistsError`
### Base classes
- `NamespaceError`
## Class `PythonIdentifierError`
Base class for Python identifier errors.
### Base classes
- `ValueError`
### Body
#### Constructor (method `__init__`)
##### Method signature
```python
__init__(self, *args: object, identifier: str = '') -> None
```
##### Returns
Return type: `None`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
| `args` | `object` | vararg | -- | -- |
| `identifier` | `str` | keyword-only | `''` | -- |
#### Method `__str__`
##### Method signature
```python
__str__(self)
```
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
### Docstring
Base class for Python identifier errors.
## Class `InvalidPythonIdentifierError`
### Base classes
- `PythonIdentifierError`
## Class `PythonAnnotationError`
Base class for Python annotation errors.
### Base classes
- `ValueError`
### Body
#### Constructor (method `__init__`)
##### Method signature
```python
__init__(self, *args: object, annotation: str = '') -> None
```
##### Returns
Return type: `None`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
| `args` | `object` | vararg | -- | -- |
| `annotation` | `str` | keyword-only | `''` | -- |
#### Method `__str__`
##### Method signature
```python
__str__(self)
```
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
### Docstring
Base class for Python annotation errors.
## Class `InvalidPythonAnnotationError`
### Base classes
- `PythonAnnotationError`
## Class `PythonDefinitionError`
Base class for Python definition errors.
### Base classes
- `ValueError`
### Body
#### Constructor (method `__init__`)
##### Method signature
```python
__init__(self, *args: object, definition: str = '') -> None
```
##### Returns
Return type: `None`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
| `args` | `object` | vararg | -- | -- |
| `definition` | `str` | keyword-only | `''` | -- |
#### Method `__str__`
##### Method signature
```python
__str__(self)
```
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
### Docstring
Base class for Python definition errors.
## Class `PythonArgumentStructureError`
### Base classes
- `PythonDefinitionError`
+335
View File
@@ -0,0 +1,335 @@
# Module `jcloud_docsgen.utils`
The utilities for jcloud_docsgen
## Class `ExistingDirectory`
pathlib.Path subclass that is a directory.
ExistingDirectory represents a filesystem path but it also checks
whether the path exists and is not a file but a directory. It
inherits from pathlib.Path.
### Base classes
- `pathlib.Path`
### Body
#### Constructor (method `__init__`)
##### Method signature
```python
__init__(self, path, *args, **kwargs) -> None
```
##### Returns
Return type: `None`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
| `path` | -- | normal | -- | -- |
| `args` | -- | vararg | -- | -- |
| `kwargs` | -- | kwargs | -- | -- |
### Docstring
pathlib.Path subclass that is a directory.
ExistingDirectory represents a filesystem path but it also checks
whether the path exists and is not a file but a directory. It
inherits from pathlib.Path.
## Class `ExistingFile`
pathlib.Path subclass that is a file.
ExistingFile represents a filesystem path but it also checks whether
the path exists and is not a directory but a file. It inherits from
pathlib.Path.
### Base classes
- `pathlib.Path`
### Body
#### Constructor (method `__init__`)
##### Method signature
```python
__init__(self, path, *args, **kwargs) -> None
```
##### Returns
Return type: `None`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
| `path` | -- | normal | -- | -- |
| `args` | -- | vararg | -- | -- |
| `kwargs` | -- | kwargs | -- | -- |
### Docstring
pathlib.Path subclass that is a file.
ExistingFile represents a filesystem path but it also checks whether
the path exists and is not a directory but a file. It inherits from
pathlib.Path.
## Function `_quote`
Quotes a string.
### Function signature
```python
_quote(string: str, quotation_mark: str) -> str
```
### Returns
The quoted string
Return type: `str`
### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `string` | `str` | normal | -- | The string to quote |
| `quotation_mark` | `str` | normal | -- | The quotation mark |
### Docstring
Quotes a string.
:param string: The string to quote
:type string: str
:param quotation_mark: The quotation mark
:type quotation_mark: str
:return: The quoted string
:rtype: str
## Function `human_readable_list`
Generates a human-readable list from a list.
### Function signature
```python
human_readable_list(ls: list, final_separator: str = 'and', quotation_mark: str = '') -> str
```
### Returns
The human-readable list.
Return type: `str`
### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `ls` | `list` | normal | -- | The list. |
| `final_separator` | `str` | normal | `'and'` | The seperator between the last and the second-to-last element of the list. Default is 'and'. |
| `quotation_mark` | `str` | normal | `''` | The characters that are inserted before and after an element. Default is ''. |
### Docstring
Generates a human-readable list from a list.
:param ls: The list.
:type ls: list
:param final_separator: The seperator between the last and the
second-to-last element of the list. Default
is 'and'.
:type final_separator: str
:param quotation_mark: The characters that are inserted before and
after an element. Default is ''.
:return: The human-readable list.
:rtype: str
## Function `_list_type_names`
Converts a list of types into a list of their names (``__name__``
attribute)
### Function signature
```python
_list_type_names(types: list[type]) -> list[str]
```
### Returns
The list of the names of the types
Return type: `list[str]`
### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `types` | `list[type]` | normal | -- | The list of types |
### Docstring
Converts a list of types into a list of their names (``__name__``
attribute)
:param types: The list of types
:type types: list[type]
:return: The list of the names of the types
:rtype: list[str]
## Function `assert_that_is_instance`
### Function signature
```python
assert_that_is_instance(obj: object, class_or_tuple: Union[type, types.UnionType, tuple[type, ...]]) -> None
```
### Returns
Return type: `None`
### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `obj` | `object` | normal | -- | -- |
| `class_or_tuple` | `Union[type, types.UnionType, tuple[type, ...]]` | normal | -- | -- |
## Function `non_empty_str`
Returns the value or ``None``.
Returns the value if it is not empty and not `Ǹone``; otherwise,
returns ``None``.
Please note: values are stripped, i. e. if the value consists only
of spaces, it is also considered empty.
### Function signature
```python
non_empty_str(value: Union[str, None]) -> Union[str, None]
```
### Returns
The value or ``None``.
Return type: `Union[str, None]`
### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `value` | `Union[str, None]` | normal | -- | The value. |
### Docstring
Returns the value or ``None``.
Returns the value if it is not empty and not `Ǹone``; otherwise,
returns ``None``.
Please note: values are stripped, i. e. if the value consists only
of spaces, it is also considered empty.
:param value: The value.
:type value: Union[str, None]
:return: The value or ``None``.
:rtype: Union[str, None]
## Function `get_relative_path_segments`
Returns the path segments relative to ``root``.
### Function signature
```python
get_relative_path_segments(path: pathlib.Path, root: pathlib.Path) -> list[str]
```
### Returns
The path segments relative to ``root``.
Return type: `list[str]`
### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `path` | `pathlib.Path` | normal | -- | The path. |
| `root` | `pathlib.Path` | normal | -- | The root path. |
### Docstring
Returns the path segments relative to ``root``.
:param path: The path.
:type path: pathlib.Path
:param root: The root path.
:type root: pathlib.Path
:return: The path segments relative to ``root``.
:rtype: list[str]
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2018 Marcin Kurczewski
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2010 - 2023 Holger Krekel and others
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+177
View File
@@ -0,0 +1,177 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 holger krekel (rather uses bitbucket/hpk42)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+25
View File
@@ -0,0 +1,25 @@
Copyright (c) 2006-2022 by the respective authors (see AUTHORS file).
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+19
View File
@@ -0,0 +1,19 @@
This is the MIT license: http://www.opensource.org/licenses/mit-license.php
Copyright (c) 2015-2022 the pytest-datadir authors and contributors <see AUTHORS file>.
Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or
substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2004 Holger Krekel and others
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+16
View File
@@ -0,0 +1,16 @@
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "jcloud-docsgen"
version = "1.0.0"
description = "A tool that generates Markdown documentation for software project source code"
dependencies = ["docstring-parser"]
license = "Apache-2.0"
[tool.setuptools]
package-dir = {"" = "src"}
[tool.setuptools.packages.find]
where = ["src"]
+22
View File
@@ -0,0 +1,22 @@
# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from . import cli
from .core import *
from .core import __all__ as _core__all__
__all__ = [
'cli',
*_core__all__
]
+37
View File
@@ -0,0 +1,37 @@
# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
'''
The command line interface
'''
__all__ = [
'main'
]
import argparse
import pathlib
from .utils import ExistingDirectory
def existing_directory(path: str, *args, **kwargs) -> pathlib.Path:
try:
return ExistingDirectory(path, *args, **kwargs)
except (FileNotFoundError, NotADirectoryError) as e:
raise argparse.ArgumentTypeError(f'{e.filename}{": " + e.args[1] if len(e.args) > 1 else ""}')
def main() -> int:
argument_parser = argparse.ArgumentParser()
argument_parser.add_argument('project_directory', type = existing_directory, default = pathlib.Path('.'), help = 'The directory of the project')
args = argument_parser.parse_args()
return 0
+19
View File
@@ -0,0 +1,19 @@
# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from . import python
__all__ = [
'python'
]
@@ -0,0 +1,22 @@
# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from ._core import *
from ._core import __all__ as _core__all__
from . import namespaces
__all__ = [
*_core__all__,
'namespaces'
]
+954
View File
@@ -0,0 +1,954 @@
# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from ...utils import ExistingDirectory, assert_that_is_instance, ExistingFile, non_empty_str, get_relative_path_segments
from ...exceptions import PythonArgumentStructureError, InvalidNamespaceError
from .namespaces import PythonModuleNamespace, PythonPackageNamespace
from .definitions import PythonDefinition, PythonFunctionDefinition, PythonAsyncFunctionDefinition, PythonClassDefinition
from .arguments import PythonArgumentKind, PythonFunctionArgument
from .docstrings import PythonDocstring
from collections.abc import Iterator
from typing import Union
from dataclasses import dataclass
from enum import Enum
import docstring_parser
import pathlib
import ast
import types
__all__ = [
'PythonDefinitionDocumentationIncludeSections',
'PythonDefinitionDocumentationGenerator',
'PythonModuleDocumentationGenerator',
'PythonDocumentationGenerator',
]
def _collect_definitions(tree_or_node) -> Iterator[PythonDefinition]:
'''
Collects all definitions of the tree or node.
:param tree_or_node: The tree or node.
:return: The definitions.
:rtype: Iterator[PythonDefinition]
'''
for node in tree_or_node.body:
if isinstance(node, ast.FunctionDef):
yield PythonFunctionDefinition.from_node(node)
if isinstance(node, ast.AsyncFunctionDef):
yield PythonAsyncFunctionDefinition.from_node(node)
if isinstance(node, ast.ClassDef):
yield PythonClassDefinition.from_node(node)
def _group_argument_list(argument_list: list[PythonFunctionArgument]) -> tuple[list[PythonFunctionArgument], list[PythonFunctionArgument], Union[PythonFunctionArgument, None], list[PythonFunctionArgument], Union[PythonFunctionArgument, None]]:
'''
Groups the argument list into a list of positional-only arguments, a
list of normal arguments, the * argument if there is one, a list of
keyword-only arguments and the var-keyword-argument if there is one.
:param argument_list: The argument list.
:type argument_list: list[PythonFunctionArgument]
:rtype: tuple[list[PythonFunctionArgument], list[PythonFunctionArgument], Union[PythonFunctionArgument, None], list[PythonFunctionArgument], Union[PythonFunctionArgument, None]]
'''
if argument_list:
if argument_list[-1].kind != PythonArgumentKind.KWARGS and any([
arg.kind == PythonArgumentKind.KWARGS
for arg in argument_list
]):
raise PythonArgumentStructureError('arguments cannot follow var-keyword argument')
positional_only_args = []
normal_args = []
vararg = None
keyword_only_args = []
kwargs = None
for arg in argument_list:
if arg.kind == PythonArgumentKind.POSITIONAL_ONLY:
positional_only_args.append(arg)
if arg.kind == PythonArgumentKind.NORMAL:
normal_args.append(arg)
if arg.kind == PythonArgumentKind.VARARG:
if vararg is not None:
raise PythonArgumentStructureError('* argument may only appear once', definition = arg)
vararg = arg
if arg.kind == PythonArgumentKind.KEYWORD_ONLY:
keyword_only_args.append(arg)
if arg.kind == PythonArgumentKind.KWARGS:
kwargs = arg
return positional_only_args, normal_args, vararg, keyword_only_args, kwargs
def _signature_argument_list(argument_list: list[PythonFunctionArgument]) -> list[str]:
'''
Returns a list of the arguments as strings for a function signature.
:param argument_list: The argument list.
:type argument_list: list[PythonFunctionArgument]
:return: The list of function signature argument strings.
:rtype: list[str]
'''
positional_only_args, normal_args, vararg, keyword_only_args, kwargs = _group_argument_list(argument_list)
args = []
for arg in positional_only_args:
args.append(arg.signature_repr())
if positional_only_args:
args.append('/')
for arg in normal_args:
args.append(arg.signature_repr())
if vararg is not None:
args.append(vararg.signature_repr())
if keyword_only_args and vararg is None:
args.append('*')
for arg in keyword_only_args:
args.append(arg.signature_repr())
if kwargs is not None:
args.append(kwargs.signature_repr())
return args
def _fit_row_length(expected: int, row: tuple) -> tuple:
'''
Fits the row length to the expected length.
If the row is too long, the tail, that makes it too long, is omitted.
If it is too short, it is filled with ``None`` (at the end).
:param expected: The expected length.
:type expected: int
:param row: The row.
:type row: tuple
'''
if expected < 0:
raise ValueError('expected must be larger than or equal 0.')
if len(row) > expected:
return row[:expected]
else:
return row + (None,) * (expected - len(row))
def _markdown_table(header: tuple, body: list[tuple], *, allow_markdown_table: bool = False, force_row_lengths: bool = False, one_line_values: bool = True, missing_value: str = '--') -> str:
'''
Generates a table.
If ``allow_markdown_table`` is ``False``, an alternative
representation is used.
:param headers: The table headers.
:type headers: tuple
:param body: The table body.
:type body: list[tuple]
:param allow_markdown_table: Controls whether a markdown table may be
used.
:type allow_markdown_table: bool
:param force_row_lengths: Controls whether an exception is raised if
a row does not have the correct length.
:type force_row_lengths: bool
:param one_line_values: Controls whether all newlines in all values
are replaced with spaces.
:type one_line_values: bool
:param missing_value: The string that is used if a value is missing
(only if ``force_row_lengths`` is ``False``).
:type missing_value: str
'''
if not header or not body:
return ''
header_len = len(header)
if allow_markdown_table:
table = '| ' + ' | '.join(header) + ' |\n|' + ' --- |' * len(header) + '\n'
for row in body:
if len(row) != header_len:
if force_row_lengths:
raise ValueError(f'expected {header_len} value{"s" if header_len > 1 else ""}, got {len(row)}')
else:
row = _fit_row_length(header_len, row)
table += '| ' + ' | '.join([
str(col if col is not None else missing_value).replace('\n', ' ')
if one_line_values
else str(col if col is not None else missing_value)
for col in row
]) + ' |\n'
else:
table = ''
for row in body:
if len(row) != header_len:
if force_row_lengths:
raise ValueError(f'expected {header_len} value{"s" if header_len > 1 else ""}, got {len(row)}')
else:
row = _fit_row_length(header_len, row)
sep = '- '
for i, col in enumerate(row):
table += sep + header[i] + ': ' + (str(col if col is not None else missing_value).replace('\n', ' ') \
if one_line_values \
else str(col if col is not None else missing_value)) + '\n\n'
sep = ' '
return table.strip()
def _is_docstring(docstring: Union[str, PythonDocstring, None]) -> bool:
'''
Checks whether the docstring is a docstring or an empty docstring.
:param docstring: The docstring.
:type docstring: Union[str, PythonDocstring, None]
:return: Whether there is a docstring.
:rtype: bool
'''
if docstring is None:
return False
if isinstance(docstring, PythonDocstring):
docstring = docstring.docstring
return docstring.strip() != ''
@dataclass
class _FunctionParamInfo:
annotation: Union[str, None]
description: Union[str, None]
@dataclass
class _NoDocstringTemplate:
description = None
params = []
raises = []
returns = None
many_returns = []
deprecation = None
examples = []
meta = []
short_description = None
long_description = None
blank_after_short_description = None
_NoDocstring = _NoDocstringTemplate()
def _argument_kind_human_readable(argument_kind: PythonArgumentKind) -> str:
'''
Returns the human-readable representation of a
``PythonArgumentKind``.
:param argument_kind: The argument kind.
:type argument_kind: PythonArgumentKind
:return: The human-readable representation.
:rtype: str
'''
return argument_kind.name.lower().replace('_', '-')
def _function_params_info(arg_list: list[PythonFunctionArgument], docstring_arg_list: list[docstring_parser.DocstringParam]) -> dict[str, _FunctionParamInfo]:
'''
Returns a dictionary with the parameter info.
The keys of the dictionary are the parameter names and the values are
their types. If there is no type annotation for a parameter, the type
in the docstring is used as a fallback. If the docstring also does
not contain the type for a parameter, its value in the dictionary is
set to ``None``.
:param arg_list: The list of parameters from the function parameter
list.
:type arg_list: list[PythonFunctionArgument]
:param docstring_arg_list: The list of parameters from the docstring
parameter list.
:return: The dictionary with the parameter types.
:rtype: dict[str, _FunctionParamInfo]
'''
arg_annotations = {arg.name: _FunctionParamInfo(arg.annotation, None) for arg in arg_list}
for arg in docstring_arg_list:
if arg.arg_name in arg_annotations.keys():
arg_annotations[arg.arg_name] = _FunctionParamInfo(
arg_annotations[arg.arg_name].annotation or arg.type_name,
arg.description
)
return arg_annotations
class PythonDefinitionDocumentationIncludeSections:
'''
A class representing the included sections in a Python definition
documentation.
:param signature: Whether include the signature section.
:type signature: bool
:param returns: Whether include the returns section.
:type returns: bool
:param params: Whether include the params section.
:type params: bool
:param exceptions: Whether include the exceptions section.
:type exceptions: bool
:param examples: Whether include the examples section.
:type examples: bool
:param docstring: Whether include the docstring section.
:type docstring: bool
:param bases: Whether include the bases section.
:type bases: bool
:param decorators: Whether include the decorators section.
:type decorators: bool
'''
def __init__(
self,
*,
signature: bool = True,
returns: bool = True,
params: bool = True,
exceptions: bool = True,
examples: bool = True,
docstring: bool = True,
bases: bool = True,
decorators: bool = True,
class_body: bool = True
):
self.signature = signature
self.returns = returns
self.params = params
self.exceptions = exceptions
self.examples = examples
self.docstring = docstring
self.bases = bases
self.decorators = decorators
self.class_body = class_body
self._args = {
'signature': self.signature,
'returns': self.returns,
'params': self.params,
'exceptions': self.exceptions,
'examples': self.examples,
'docstring': self.docstring,
'bases': self.bases,
'decorators': self.decorators,
'class_body': self.class_body,
}
def __repr__(self) -> str:
return type(self).__name__+ '(' + ', '.join(
[
key + ' = ' + repr(value)
for key, value in self._args.items()
]
) + ')'
def __eq__(self, value) -> str:
if not isinstance(value, PythonDefinitionDocumentationIncludeSections):
return False
return self._args == value._args
class PythonDefinitionDocumentationGenerator:
'''
A documentation generator for a Python definition.
:param definition: The Python definition.
:type definition: PythonDefinition
:param include_sections: The sections that will be included.
:type include_sections: PythonDefinitionDocumentationIncludeSections
:param level: The level of the definition in the markdown content.
:type level: int
:param allow_html: Controls whether the generated documentation may
contain HTML.
:type allow_html: bool
:param allow_tables: Controls whether the generated documentation may
contain tables.
:type allow_tables: bool
:param skip_empty_sections: Controls whether sections that does not
have any content (such as the parameters
section if there are no parameters taken)
are skipped. If ``False``, a short text
(such as 'This function takes no
parameters') is used.
:type skip_empty_sections: bool
:param is_method: Controls whether the definition is a class method.
:type is_method: bool
'''
def __init__(
self,
definition: PythonDefinition,
include_sections: PythonDefinitionDocumentationIncludeSections = PythonDefinitionDocumentationIncludeSections(),
level: int = 1,
*,
allow_html: bool = True,
allow_tables: bool = False,
skip_empty_sections: bool = True,
is_method: bool = False
) -> None:
self.definition = definition
self.include_sections = include_sections
self.level = level
self.allow_html = allow_html
self.allow_tables = allow_tables
self.skip_empty_sections = skip_empty_sections
self.is_method = is_method
self._docstring_cache = None
@property
def _docstring(self):
if self._docstring_cache is None:
self._docstring_cache = self.definition.doc.parse() if _is_docstring(self.definition.doc) else _NoDocstring
return self._docstring_cache
def generate_signature(self) -> str:
'''
Generates the signature of the definition. Works only if the
definition is a function or asynchronous function definition.
:raises TypeError: If the definition is a class definition.
:return: The signature.
:rtype: str
'''
if isinstance(self.definition, PythonClassDefinition):
raise TypeError('Cannot generate a signature of a class definition.')
signature = self.definition.name + '(' + ', '.join(_signature_argument_list(self.definition.args)) + ')'
if self.definition.returns is not None:
signature += ' -> ' + self.definition.returns
return signature.strip()
def _generate_signature_section(self) -> str:
'''
Generates the signature section.
:return: The signature section.
:rtype: str
'''
return self.level * '#' + '# ' + ('Function' if not self.is_method else 'Method') +' signature\n\n```python\n' + self.generate_signature() + '\n```'
def _generate_docstring_section(self) -> str:
'''
Generates the docstring section.
:return: The docstring section.
:rtype: str
'''
md = ''
if _is_docstring(self.definition.doc) or not self.skip_empty_sections:
if self.allow_html:
md += '<details><summary><h' + str(self.level + 1) + ' style="display:inline">Docstring' + '</h' + str(self.level + 1) + '></summary>'
else:
md += self.level * '#' + '# Docstring\n\n'
if _is_docstring(self.definition.doc):
md += self.definition.doc.docstring
elif not self.skip_empty_sections:
md += 'There is no docstring to show.'
if _is_docstring(self.definition.doc) or not self.skip_empty_sections:
if self.allow_html:
md += '</details>'
md += '\n\n'
return md.strip()
def _generate_params_section(self) -> str:
'''
Generates the parameters section.
:return: The parameters section.
:rtype: str
'''
md = ''
if self.definition.args or not self.skip_empty_sections:
md += self.level * '#' + '# Parameters\n\n'
params_types = _function_params_info(self.definition.args, self._docstring.params)
if params_types:
md += _markdown_table(['Parameter', 'Type', 'Kind', 'Default value', 'Description'], [
(
'`' + arg.name + '`',
('`' + params_types.get(arg.name).annotation + '`') if params_types.get(arg.name).annotation is not None else None,
_argument_kind_human_readable(arg.kind),
('`' + arg.default + '`') if arg.default is not None else None,
non_empty_str(params_types.get(arg.name).description)
)
for arg in self.definition.args
], allow_markdown_table = self.allow_tables)
md += '\n\n'
elif not self.skip_empty_sections:
md += 'This function takes no parameters.'
return md.strip()
def _generate_returns_section(self) -> str:
'''
Generates the returns section.
:return: The returns section.
:rtype: str
'''
md = ''
if self._docstring.returns or self.definition.returns or not self.skip_empty_sections:
md += self.level * '#' + '# Returns\n\n'
returns = False
if self._docstring.returns:
if self._docstring.returns.description is not None:
returns = True
md += self._docstring.returns.description + '\n\n'
rtype = self.definition.returns
if rtype is None and self._docstring.returns is not None:
rtype = self._docstring.returns.type_name
if rtype is not None:
returns = True
md += 'Return type: `' + rtype + '`'
if not self.skip_empty_sections and not returns:
md += 'This function does not provide any information about its return value.'
return md.strip()
def _generate_exceptions_section(self) -> str:
'''
Generates the exceptions section.
:return: The exceptions section.
:rtype: str
'''
md = ''
if self._docstring.raises or not self.skip_empty_sections:
md += self.level * '#' + '# Exceptions\n\n'
if self._docstring.raises:
md += _markdown_table(('Exception', 'Description'), [
(('`' + exception.type_name + '`') if exception.type_name is not None else None, non_empty_str(exception.description))
for exception in self._docstring.raises
], allow_markdown_table = self.allow_tables)
elif not self.skip_empty_sections:
md += 'This function raises no exceptions.'
return md.strip()
def _generate_examples_section(self) -> str:
'''
Generates the examples section.
:return: The examples section.
:rtype: str
'''
md = ''
if self._docstring.examples or not self.skip_empty_sections:
md += self.level * '#' + '# Examples\n\n'
examples = []
for example in self._docstring.examples:
examples.append('```python\n' +\
(example.snippet or example.description) + \
'\n```')
md += '\n\n---\n\n'.join(examples)
if not self._docstring.examples and not self.skip_empty_sections:
md += 'There are no examples.'
return md.strip()
def _generate_decorators_section(self) -> str:
'''
Generates the decorators section.
:return: The decorators section.
:rtype: str
'''
md = ''
if self.definition.decorators or not self.skip_empty_sections:
md += self.level * '#' + '# Decorators\n\n'
for decorator in self.definition.decorators:
md += '```python\n' + decorator + '\n```\n\n'
if not self.definition.decorators and not self.skip_empty_sections:
md += 'This function or class does not have any decorators.'
return md.strip()
def _generate_bases_section(self) -> str:
'''
Generates the bases section.
:return: The bases section.
:rtype: str
'''
md = ''
if self.definition.bases or not self.skip_empty_sections:
md += self.level * '#' + '# Base classes\n\n'
for base in self.definition.bases:
md += '- `' + base + '`\n'
if not self.definition.bases and not self.skip_empty_sections:
md += 'This class does not have any base classes.'
return md.strip()
def _generate_body_section(self) -> str:
'''
Generates the body section.
:return: The body section.
:rtype: str
'''
md = ''
if self.definition.body or not self.skip_empty_sections:
md += self.level * '#' + '# Body\n\n'
for method_or_class in self.definition.body:
md += PythonDefinitionDocumentationGenerator(
method_or_class,
self.include_sections,
level = self.level + 2,
allow_html = self.allow_html,
allow_tables = self.allow_tables,
skip_empty_sections = self.skip_empty_sections,
is_method = True
).generate_documentation() + '\n\n'
if not self.definition.body and not self.skip_empty_sections:
md += 'This class does not have a body.'
return md.strip()
def generate_documentation(self) -> str:
'''
Generates the documentation in the markdown format.
:return: The documentation.
:rtype: str
'''
md = self.level * '#' + ' '
if isinstance(self.definition, PythonAsyncFunctionDefinition):
md += 'Asynchronous ' + ('function' if not self.is_method else 'method')
elif isinstance(self.definition, PythonFunctionDefinition):
if self.is_method and self.definition.name == '__init__':
md += 'Constructor (method'
else:
md += 'Function' if not self.is_method else 'Method'
else:
md += 'Class'
md += ' `' + self.definition.name + '`'
if isinstance(self.definition, PythonFunctionDefinition) and not isinstance(self.definition, PythonAsyncFunctionDefinition) and self.is_method and self.definition.name == '__init__':
md += ')'
md += '\n\n'
if self._docstring.description is not None:
md += self._docstring.description + '\n\n'
if isinstance(self.definition, PythonFunctionDefinition):
if self.include_sections.signature:
md += self._generate_signature_section() + '\n\n'
if self.include_sections.returns:
md += self._generate_returns_section() + '\n\n'
if self.include_sections.params:
md += self._generate_params_section() + '\n\n'
if self.include_sections.exceptions:
md += self._generate_exceptions_section() + '\n\n'
if isinstance(self.definition, PythonClassDefinition):
if self.include_sections.bases:
md += self._generate_bases_section() + '\n\n'
if self.include_sections.class_body:
md += self._generate_body_section() + '\n\n'
if self.include_sections.decorators:
md += self._generate_decorators_section() + '\n\n'
if self.include_sections.examples:
md += self._generate_examples_section() + '\n\n'
if self.include_sections.docstring:
md += self._generate_docstring_section() + '\n\n'
return md.strip()
class PythonModuleDocumentationGenerator:
'''
A documentation generator for a Python module.
:param module_path: The path of the module file.
:type module_path: ExistingFile
:param src_directory: The directory of the source code. It is
optional and only used for making the module
identifiers in the documentation relative.
:type src_directory: Union[ExistingDirectory, None]
:param include_sections: The sections that will be included.
:type include_sections: PythonDefinitionDocumentationIncludeSections
:param kwargs: The options for ``PythonDefinitionDocumentationGenerator``.
'''
def __init__(
self,
module_path: ExistingFile,
src_directory: Union[ExistingDirectory, None] = None,
include_sections: PythonDefinitionDocumentationIncludeSections = PythonDefinitionDocumentationIncludeSections(),
**kwargs
) -> None:
assert_that_is_instance(module_path, ExistingFile)
assert_that_is_instance(src_directory, (ExistingDirectory, types.NoneType))
if module_path.name.count('.') > 1:
raise InvalidNamespaceError('namespace identifier cannot contain more than one dot', namespace_identifier = module_path.name)
self.module_path = pathlib.Path(str(module_path))
self.src_directory = pathlib.Path(str(src_directory)) if src_directory is not None else None
self.include_sections = include_sections
self.kwargs = kwargs
self._source_code_cache = None
self._ast_cache = None
@property
def _source_code(self) -> str:
if self._source_code_cache is None:
self._source_code_cache = self.module_path.read_text()
return self._source_code_cache
@property
def _ast(self) -> str:
if self._ast_cache is None:
self._ast_cache = ast.parse(self._source_code)
return self._ast_cache
def collect_definitions(self) -> Iterator[PythonDefinition]:
'''
Collects all function and class definitions of the module.
:return: All definitions of the module
:rtype: Iterator[PythonDefinition]
'''
tree = self._ast
return _collect_definitions(tree)
def generate_documentation(self) -> str:
'''
Generates the documentation in the markdown format.
:return: The documentation.
:rtype: str
'''
md = '# Module `'
if self.src_directory is not None:
module_identifier = '.'.join(get_relative_path_segments(self.module_path, self.src_directory))
if module_identifier.endswith('.py'):
md += module_identifier[:-3]
else:
md += module_identifier
else:
md += self.module_path.stem
md += '`\n\n'
docstring = ast.get_docstring(self._ast)
if docstring is not None:
md += docstring + '\n\n'
for definition in self.collect_definitions():
md += PythonDefinitionDocumentationGenerator(
definition,
self.include_sections,
2,
**self.kwargs
).generate_documentation() + '\n\n'
return md.strip()
def _generate_package_namespace_documentation(
namespace: PythonPackageNamespace,
documentation_directory: pathlib.Path,
root_directory: pathlib.Path,
dir: pathlib.Path,
include_sections: PythonDefinitionDocumentationIncludeSections = PythonDefinitionDocumentationIncludeSections(),
kwargs: dict = {}
) -> None:
'''
Generates the documentation for a package namespace and writes it to
the documentation directory.
:param namespace: The namespace.
:type namespace: PythonPackageNamespace
:param documentation_directory: The directory for the documentation.
:type documentation_directory: pathlib.Path
:param root_directory: The root directory.
:type root_directory: pathlib.Path
:param dir: The directory of the namespace.
:type dir: pathlib.Path
:param include_sections: The sections to be included in the documentation.
:type include_sections: PythonDefinitionDocumentationIncludeSections
:param kwargs: The options for the documentation generator.
'''
documentation_directory = pathlib.Path(str(documentation_directory))
for sub_namespace in namespace.sub_namespaces:
if isinstance(sub_namespace, PythonPackageNamespace):
(documentation_directory / sub_namespace.name).mkdir(mode = 0o700, exist_ok = True)
_generate_package_namespace_documentation(
sub_namespace,
documentation_directory / sub_namespace.name,
root_directory, dir / sub_namespace.name,
include_sections,
kwargs
)
elif isinstance(sub_namespace, PythonModuleNamespace):
(
documentation_directory / (sub_namespace.name + '.md')
).write_text(
PythonModuleDocumentationGenerator(
ExistingFile(str(dir / (sub_namespace.name + '.py'))),
ExistingDirectory(str(root_directory)),
include_sections,
**kwargs
).generate_documentation()
)
class PythonDocumentationGenerator:
'''
The class for the documentation generator.
:param project_directory: The directory of the project.
:type project_directory: ExistingDirectory
:param docs_directory: The directory for the generated documentation.
:type docs_directory: ExistingDirectory
:param include_sections: The sections to be included in the documentation.
:type include_sections: PythonDefinitionDocumentationIncludeSections
:param kwargs: The options for ``PythonDefinitionDocumentationGenerator``.
'''
def __init__(
self,
project_directory: ExistingDirectory,
docs_directory: ExistingDirectory,
include_sections: PythonDefinitionDocumentationIncludeSections = PythonDefinitionDocumentationIncludeSections(),
**kwargs
) -> None:
assert_that_is_instance(project_directory, ExistingDirectory)
assert_that_is_instance(docs_directory, ExistingDirectory)
self.project_directory = pathlib.Path(str(project_directory))
self.docs_directory = pathlib.Path(str(docs_directory))
self.include_sections = include_sections
self.kwargs = kwargs
def _namespace(self, directory: pathlib.Path) -> PythonPackageNamespace:
namespace = PythonPackageNamespace(directory.name, [])
for entry in directory.iterdir():
if entry.is_dir():
namespace.add(self._namespace(entry))
elif entry.suffix == '.py':
namespace.add(PythonModuleNamespace(entry.stem))
return namespace
def namespace(self) -> list[PythonPackageNamespace]:
'''
Returns the project as a namespace.
:return: The project as a namespace.
:rtype: list[PythonPackageNamespace]
'''
src_dir = self.project_directory / 'src'
return [
self._namespace(dir)
for dir in src_dir.iterdir() if dir.is_dir()
]
def generate_documentation(self) -> None:
'''
Generates the full project documentation and writes it to the
documentation directory.
'''
_generate_package_namespace_documentation(
PythonPackageNamespace('src', self.namespace()),
self.docs_directory,
self.project_directory / 'src',
self.project_directory / 'src',
self.include_sections,
self.kwargs
)
+173
View File
@@ -0,0 +1,173 @@
# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from enum import Enum
from typing import Union
import ast
from ...utils import assert_that_is_instance
from ...exceptions import InvalidPythonIdentifierError, InvalidPythonAnnotationError, PythonArgumentStructureError
import keyword
import types
__all__ = [
'PythonArgumentKind',
'PythonFunctionArgument',
'PythonASTArgumentsListParser'
]
class PythonArgumentKind(Enum):
NORMAL = 0
POSITIONAL_ONLY = 1
KEYWORD_ONLY = 2
VARARG = 3
KWARGS = 4
class PythonFunctionArgument:
'''
Represents an argument of a Python function.
:param name: The name of the argument.
:type name: str
:param kind: The kind of the argument.
:type kind: ArgumentKind
:param default: The default value of the argument.
:type default: Union[str, None]
:param annotation: The type annotation of the argument.
:type annotation: Union[str, None]
'''
def __init__(self, name: str, kind: PythonArgumentKind, default: Union[str, None], annotation: Union[str, None]) -> None:
assert_that_is_instance(name, str)
assert_that_is_instance(kind, PythonArgumentKind)
assert_that_is_instance(default, (str, types.NoneType))
assert_that_is_instance(annotation, (str, types.NoneType))
# check whether name is a valid identifier
if not name or not name.isidentifier() or keyword.iskeyword(name):
raise InvalidPythonIdentifierError('invalid identifier', identifier = name)
# check whether the annotation is a valid annotation
# Currently, it is only checked whether the parameter is not
# empty.
if annotation is not None and not annotation:
raise InvalidPythonAnnotationError('invalid annotation', annotation = annotation)
self.name = name
self.kind = kind
self.default = default
self.annotation = annotation
if self.kind in (PythonArgumentKind.VARARG, PythonArgumentKind.KWARGS) and self.default is not None:
raise PythonArgumentStructureError('* or ** arguments cannot have default values', definition = self.signature_repr())
def __eq__(self, value: PythonFunctionArgument) -> bool:
if not isinstance(value, PythonFunctionArgument):
return False
return self.name == value.name and self.kind == value.kind and self.default == value.default and self.annotation == value.annotation
def __repr__(self) -> str:
return type(self).__name__ + repr((
self.name,
self.kind,
self.default,
self.annotation
))
def signature_repr(self) -> str:
'''
Returns the function signature representation of the argument.
:return: The string representation of the argument.
:rtype: str
'''
if self.kind == PythonArgumentKind.KWARGS:
str_repr = '**'
elif self.kind == PythonArgumentKind.VARARG:
str_repr = '*'
else:
str_repr = ''
str_repr += self.name
if self.annotation is not None:
str_repr += ': ' + self.annotation
if self.default is not None:
str_repr += ' = ' + self.default
return str_repr
class PythonASTArgumentsListParser:
'''
A parser for making ``PythonFunctionArgument`` objects from an
``ast.arguments`` arguments list.
:param ast_arguments_list: The ``ast.arguments`` arguments list.
:type ast_arguments_list: ast.arguments
'''
def __init__(self, ast_arguments_list: ast.arguments) -> None:
self.ast_arguments_list = ast_arguments_list
def to_argument_list(self) -> list[PythonFunctionArgument]:
'''
Converts the AST arguments list to a list of Python function
argument objects.
:return: The list of Python function argument objects.
:rtype: list[PythonFunctionArgument]
'''
defaults = [None for _ in range(len(
self.ast_arguments_list.posonlyargs +
self.ast_arguments_list.args +
self.ast_arguments_list.kwonlyargs
) - len(self.ast_arguments_list.defaults) - len(self.ast_arguments_list.kw_defaults))] + \
[ast.unparse(d) 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]
i = 0
arguments = []
for arg in self.ast_arguments_list.posonlyargs:
if arg.annotation is None:
arguments.append(PythonFunctionArgument(arg.arg, PythonArgumentKind.POSITIONAL_ONLY, defaults[i], None))
else:
arguments.append(PythonFunctionArgument(arg.arg, PythonArgumentKind.POSITIONAL_ONLY, defaults[i], ast.unparse(arg.annotation)))
i += 1
for arg in self.ast_arguments_list.args:
if arg.annotation is None:
arguments.append(PythonFunctionArgument(arg.arg, PythonArgumentKind.NORMAL, defaults[i], None))
else:
arguments.append(PythonFunctionArgument(arg.arg, PythonArgumentKind.NORMAL, defaults[i], ast.unparse(arg.annotation)))
i += 1
if self.ast_arguments_list.vararg is not None:
if self.ast_arguments_list.vararg.annotation is None:
arguments.append(PythonFunctionArgument(self.ast_arguments_list.vararg.arg, PythonArgumentKind.VARARG, None, None))
else:
arguments.append(PythonFunctionArgument(self.ast_arguments_list.vararg.arg, PythonArgumentKind.VARARG, None, ast.unparse(self.ast_arguments_list.vararg.annotation)))
for arg in self.ast_arguments_list.kwonlyargs:
if arg.annotation is None:
arguments.append(PythonFunctionArgument(arg.arg, PythonArgumentKind.KEYWORD_ONLY, defaults[i], None))
else:
arguments.append(PythonFunctionArgument(arg.arg, PythonArgumentKind.KEYWORD_ONLY, defaults[i], ast.unparse(arg.annotation)))
i += 1
if self.ast_arguments_list.kwarg is not None:
if self.ast_arguments_list.kwarg.annotation is None:
arguments.append(PythonFunctionArgument(self.ast_arguments_list.kwarg.arg, PythonArgumentKind.KWARGS, None, None))
else:
arguments.append(PythonFunctionArgument(self.ast_arguments_list.kwarg.arg, PythonArgumentKind.KWARGS, None, ast.unparse(self.ast_arguments_list.kwarg.annotation)))
return arguments
@@ -0,0 +1,232 @@
# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from typing import Union
import ast
from .arguments import PythonFunctionArgument, PythonASTArgumentsListParser
from .docstrings import PythonDocstring
import keyword
from ...exceptions import InvalidPythonIdentifierError
from ...utils import assert_that_is_instance
import types
__all__ = [
'PythonDefinition',
'PythonFunctionDefinition',
'PythonAsyncFunctionDefinition',
'PythonClassDefinition'
]
def _python_docstring(doc: Union[str, None]) -> Union[PythonDocstring, None]:
'''
Returns the PythonDocstring object if the docstring is a string and
not ``None``.
:param doc: The docstring.
:type doc: Union[str, None]
:return: The PythonDocstring object or ``None``.
:rtype: Union[PythonDocstring, None]
'''
return PythonDocstring(doc) if doc is not None else None
class PythonDefinition:
'''
A base class for Python definition (a class, function or asynchronous
function definition) classes.
:param name: The name of the definition.
:type name: str
:param doc: The docstring.
:type doc: Union[PythonDocstring, None]
:param decorators: The definition decorators.
:type decorators: list[str]
:raises InvalidPythonIdentifierError: If the name is not a valid
Python identifier.
'''
def __init__(self, name: str, doc: Union[PythonDocstring, None], decorators: list[str]) -> None:
if keyword.iskeyword(name) or not name.isidentifier() or not name:
raise InvalidPythonIdentifierError('invalid identifier', identifier = name)
assert_that_is_instance(doc, (PythonDocstring, types.NoneType))
self.name = name
self.doc = doc
self.decorators = decorators
def _arg_list(self) -> tuple:
return (
self.name,
self.doc,
self.decorators
)
def __repr__(self) -> str:
return type(self).__name__ + repr(self._arg_list())
@classmethod
def from_node(cls, node: ast.stmt) -> PythonDefinition:
'''
From an AST node.
:param node: The AST node.
:type node: ast.stmt
'''
return cls(
node.name,
_python_docstring(ast.get_docstring(node)),
[ast.unparse(d) for d in node.decorator_list]
)
def __eq__(self, value: object) -> bool:
if not isinstance(value, PythonDefinition):
return False
return self._arg_list() == value._arg_list()
class PythonFunctionDefinition(PythonDefinition):
'''
A PythonDefinition subclass representing a Python function
definition.
:param name: The name of the definition.
:type name: str
:param args: The arguments.
:type args: list[PythonFunctionArgument]
:param returns: The return type of the function or class.
:type returns: Union[str, None]
:param doc: The docstring.
:type doc: Union[PythonDocstring, None]
:param decorators: The definition decorators.
:type decorators: list[str]
:raises InvalidPythonIdentifierError: If the name is not a valid
Python identifier.
'''
def __init__(self, name: str, args: list[PythonFunctionArgument], returns: Union[str, None], doc: Union[PythonDocstring, None], decorators: list[str]) -> None:
super().__init__(name, doc, decorators)
self.args = args
self.returns = returns
def _arg_list(self) -> tuple:
return (
self.name,
self.args,
self.returns,
self.doc,
self.decorators
)
@classmethod
def from_node(cls, node: ast.stmt) -> PythonFunctionDefinition:
'''
From an AST node.
:param node: The AST node.
:type node: ast.stmt
'''
return cls(
node.name,
PythonASTArgumentsListParser(node.args).to_argument_list(),
ast.unparse(node.returns) if node.returns is not None else None,
_python_docstring(ast.get_docstring(node)),
[ast.unparse(d) for d in node.decorator_list]
)
class PythonAsyncFunctionDefinition(PythonFunctionDefinition):
'''
A PythonDefinition subclass representing an asynchronous Python function
definition.
:param name: The name of the definition.
:type name: str
:param args: The arguments.
:type args: list[PythonFunctionArgument]
:param returns: The return type of the function or class.
:type returns: Union[str, None]
:param doc: The docstring.
:type doc: Union[PythonDocstring, None]
:param decorators: The definition decorators.
:type decorators: list[str]
:raises InvalidPythonIdentifierError: If the name is not a valid
Python identifier.
'''
class PythonClassDefinition(PythonDefinition):
'''
A PythonDefinition subclass representing a Python class definition.
:param name: The name of the definition.
:type name: str
:param bases: The base classes.
:type bases: list[str]
:param doc: The docstring.
:type doc: Union[PythonDocstring, None]
:param decorators: The definition decorators.
:type decorators: list[str]
:param body:
:type body: list[PythonDefinition]
:raises InvalidPythonIdentifierError: If the name is not a valid
Python identifier.
'''
def __init__(self, name: str, bases: list[str], doc: Union[PythonDocstring, None], decorators: list[str], body: list[PythonDefinition]) -> None:
super().__init__(name, doc, decorators)
self.bases = bases
self.body = body
def _arg_list(self) -> tuple:
return (
self.name,
self.bases,
self.doc,
self.decorators,
self.body
)
@classmethod
def from_node(cls, node: ast.stmt) -> PythonClassDefinition:
'''
From an AST node.
:param node: The AST node.
:type node: ast.stmt
'''
return cls(
node.name,
[ast.unparse(b) for b in node.bases],
_python_docstring(ast.get_docstring(node)),
[ast.unparse(d) for d in node.decorator_list],
[
PythonFunctionDefinition.from_node(expr)
if isinstance(expr, ast.FunctionDef)
else PythonAsyncFunctionDefinition.from_node(expr)
if isinstance(expr, ast.AsyncFunctionDef)
else PythonClassDefinition.from_node(expr)
for expr in node.body
if isinstance(expr, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef))
]
)
@@ -0,0 +1,49 @@
# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from enum import Enum
import docstring_parser
__all__ = [
'PythonDocstring'
]
class PythonDocstring:
'''
Represents a Python docstring.
:param docstring: The docstring as a string.
:type docstring: str
'''
def __init__(self, docstring: str) -> None:
self.docstring = docstring
def __repr__(self) -> str:
return type(self).__name__ + '(' + repr(self.docstring) + ')'
def __eq__(self, value) -> bool:
if not isinstance(value, type(self)):
return False
return self.docstring == value.docstring
def parse(self) -> docstring_parser.Docstring:
'''
Parses the docstring.
:return: The parsed docstring.
:rtype: docstring_parser.Docstring
'''
return docstring_parser.parse(self.docstring)
@@ -0,0 +1,189 @@
# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from ...exceptions import InvalidNamespaceError, NamespaceNotFoundError, NamespaceExistsError
from ...utils import assert_that_is_instance
from typing import Union, TypeAlias, Dict, List
__all__ = [
'PythonNamespace',
'PythonPackageNamespace',
'PythonModuleNamespace'
]
class PythonNamespace:
'''
The base class for all Python namespaces such as modules or packages.
:param name: The name of the namespace.
:type name: str
'''
def __init__(self, name: str) -> None:
assert_that_is_instance(name, str)
if not name.strip():
raise InvalidNamespaceError('invalid namespace', namespace_identifier = name)
self.name = name
def __repr__(self) -> str:
return f'{type(self).__name__}({self.name!r})'
PythonPackageNamespaceDictType: TypeAlias = Dict[str, Union[str, 'PythonPackageNamespaceDictType']]
class PythonPackageNamespace(PythonNamespace):
'''
A subclass of ``PythonNamespace`` for Python package namespaces.
:param name: The name of the namespace.
:type name: str
:param sub_namespaces: The sub-namespaces.
:type sub_namespaces: list[PythonNamespace]
'''
def __init__(self, name: str, sub_namespaces: List[PythonNamespace]) -> None:
assert_that_is_instance(sub_namespaces, list)
super().__init__(name)
self._sub_namespaces = sub_namespaces
def namespace(self, sub_namespaces: List[str]) -> PythonNamespace:
'''
Returns the namespace object with a specific identifier, such as
``['package', 'subpackage', 'module']``.
:param sub_namespaces: The identifier of the namespace.
:type sub_namespaces: list[str]
:raises InvalidNamespaceError: If the namespace identifier is
invalid.
:raises NamespaceNotFoundError: If the namespace does not exist.
:return: The namespace.
:rtype: PythonNamespace
'''
if not sub_namespaces:
raise InvalidNamespaceError('invalid namespace')
for sn in sub_namespaces:
if not sn:
raise InvalidNamespaceError('invalid namespace')
sub_namespace = self.__getitem__(sub_namespaces[0])
# package namespace
if isinstance(sub_namespace, PythonPackageNamespace):
if len(sub_namespaces) == 1:
return sub_namespace
return sub_namespace.namespace(sub_namespaces[1:])
# module namespace
else:
return sub_namespace
def _sub_namespaces_names(self, package: PythonPackageNamespace) -> PythonPackageNamespaceDictType:
'''
Returns the names of all sub-namespaces of the specified package
namespace and keeps the package structure.
:param package: The package.
:type package: PythonPackageNamespace
:return: The names if all sub-namespaces.
:rtype: PythonPackageNamespaceDictType
'''
return {
sn.name: self._sub_namespaces_names(sn) if isinstance(sn, PythonPackageNamespace) else sn.name
for sn in package._sub_namespaces
}
def sub_namespace_names(self) -> PythonPackageNamespaceDictType:
'''
Returns the names of all sub-namespaces and keeps the package
structure.
:return: The names if all sub-namespaces.
:rtype: PythonPackageNamespaceDictType
'''
return self._sub_namespaces_names(self)
def __eq__(self, value: PythonPackageNamespace) -> bool:
# For performance reasons, is not called self.sub_namespace_names
# if the name attribute does not match, which can be quickly
# verified
if self.name != value.name:
return False
return value.sub_namespace_names() == self.sub_namespace_names()
def __repr__(self) -> str:
return type(self).__name__ + repr((self.name, self._sub_namespaces))
def __getitem__(self, name: str) -> PythonNamespace:
'''
Returns a namespace with the specified name.
:param name: The name of the namespace
:type name: str
:raises NamespaceNotFoundError: If the namespace does not exist.
:return: The namespace
:rtype: PythonNamespace
'''
for sub in self._sub_namespaces:
if sub.name == name:
return sub
raise NamespaceNotFoundError('no such namespace', namespace_identifier = name)
def add(self, namespace: PythonNamespace) -> None:
'''
Adds a namespace.
:param namespace: The namespace to add.
:type namespace: PythonNamespace
:raises NamespaceExistsError: If a namespace with the name of the
namespace that is attempted to add
already exists.
'''
try:
self.__getitem__(namespace.name)
raise NamespaceExistsError('namespace already exists', namespace_identifier = namespace.name)
except NamespaceNotFoundError:
self._sub_namespaces.append(namespace)
@property
def sub_namespaces(self) -> List[PythonNamespace]:
return self._sub_namespaces
class PythonModuleNamespace(PythonNamespace):
'''
A subclass of ``PythonNamespace`` for Python module namespaces.
:param name: The name of the namespace.
:type name: str
'''
def __init__(self, name: str) -> None:
super().__init__(name)
def __eq__(self, value: PythonModuleNamespace) -> bool:
return self.name == value.name
+79
View File
@@ -0,0 +1,79 @@
# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
class NamespaceError(ValueError):
'''
Base class for namespace errors.
'''
def __init__(self, *args: object, namespace_identifier: str = '') -> None:
super().__init__(*args)
self.namespace_identifier = namespace_identifier
def __str__(self):
if not self.args:
return ''
else:
return f'{self.args[0]}{": " if self.namespace_identifier and self.args[0] else ""}{self.namespace_identifier if self.args[0] else ""}'
class InvalidNamespaceError(NamespaceError): ...
class NamespaceNotFoundError(NamespaceError): ...
class NamespaceExistsError(NamespaceError): ...
class PythonIdentifierError(ValueError):
'''
Base class for Python identifier errors.
'''
def __init__(self, *args: object, identifier: str = '') -> None:
super().__init__(*args)
self.identifier = identifier
def __str__(self):
if not self.args:
return ''
else:
return f'{self.args[0]}{": " if self.identifier and self.args[0] else ""}{self.identifier if self.args[0] else ""}'
class InvalidPythonIdentifierError(PythonIdentifierError): ...
class PythonAnnotationError(ValueError):
'''
Base class for Python annotation errors.
'''
def __init__(self, *args: object, annotation: str = '') -> None:
super().__init__(*args)
self.annotation = annotation
def __str__(self):
if not self.args:
return ''
else:
return f'{self.args[0]}{": " if self.annotation and self.args[0] else ""}{self.annotation if self.args[0] else ""}'
class InvalidPythonAnnotationError(PythonAnnotationError): ...
class PythonDefinitionError(ValueError):
'''
Base class for Python definition errors.
'''
def __init__(self, *args: object, definition: str = '') -> None:
super().__init__(*args)
self.definition = definition
def __str__(self):
if not self.args:
return ''
else:
return f'{self.args[0]}{": " if self.definition and self.args[0] else ""}{self.definition if self.args[0] else ""}'
class PythonArgumentStructureError(PythonDefinitionError): ...
+175
View File
@@ -0,0 +1,175 @@
# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
'''
The utilities for jcloud_docsgen
'''
import pathlib
import os
import errno
from typing import Union
import types
class ExistingDirectory(pathlib.Path):
'''
pathlib.Path subclass that is a directory.
ExistingDirectory represents a filesystem path but it also checks
whether the path exists and is not a file but a directory. It
inherits from pathlib.Path.
'''
def __init__(self, path, *args, **kwargs) -> None:
if not os.path.exists(path):
raise FileNotFoundError(errno.ENOENT, 'no such file or directory', path)
if not os.path.isdir(path):
raise NotADirectoryError(errno.ENOTDIR, 'not a directory', path)
super().__init__(*(path, *args), **kwargs)
class ExistingFile(pathlib.Path):
'''
pathlib.Path subclass that is a file.
ExistingFile represents a filesystem path but it also checks whether
the path exists and is not a directory but a file. It inherits from
pathlib.Path.
'''
def __init__(self, path, *args, **kwargs) -> None:
if not os.path.exists(path):
raise FileNotFoundError(errno.ENOENT, 'no such file or directory', path)
if not os.path.isfile(path):
raise IsADirectoryError(errno.EISDIR, 'is a directory', path)
super().__init__(*(path, *args), **kwargs)
def _quote(string: str, quotation_mark: str) -> str:
'''
Quotes a string.
:param string: The string to quote
:type string: str
:param quotation_mark: The quotation mark
:type quotation_mark: str
:return: The quoted string
:rtype: str
'''
return quotation_mark + string + quotation_mark
def human_readable_list(ls: list, final_separator: str = 'and', quotation_mark: str = '') -> str:
'''
Generates a human-readable list from a list.
:param ls: The list.
:type ls: list
:param final_separator: The seperator between the last and the
second-to-last element of the list. Default
is 'and'.
:type final_separator: str
:param quotation_mark: The characters that are inserted before and
after an element. Default is ''.
:return: The human-readable list.
:rtype: str
'''
# empty list
if not ls:
return ''
# list with one element
if len(ls) == 1:
return _quote(str(ls[0]), quotation_mark)
return ', '.join(
[
_quote(str(obj), quotation_mark) # quote the string representation of the object
for obj in ls[:-1] # do not include the last element of the list
]
) + ' ' + final_separator + ' ' + _quote(str(ls[-1]), quotation_mark)
def _list_type_names(types: list[type]) -> list[str]:
'''
Converts a list of types into a list of their names (``__name__``
attribute)
:param types: The list of types
:type types: list[type]
:return: The list of the names of the types
:rtype: list[str]
'''
return [tp.__name__ for tp in types]
def assert_that_is_instance(obj: object, class_or_tuple: Union[type, types.UnionType, tuple[type, ...]]) -> None:
if not isinstance(class_or_tuple, Union[type, types.UnionType, tuple]):
raise TypeError(f'class_or_tuple: expected \'Union[type, types.UnionType, tuple[type, ...]]\', \'got {type(class_or_tuple).__name__}\'')
if not isinstance(obj, class_or_tuple):
if isinstance(class_or_tuple, (tuple, types.UnionType)):
if isinstance(class_or_tuple, types.UnionType):
class_or_tuple = class_or_tuple.__args__
if len(class_or_tuple) > 1:
exception_message_expected = 'either '
else:
exception_message_expected = ''
exception_message_expected += human_readable_list(_list_type_names(class_or_tuple), 'or', '\'')
else:
exception_message_expected = '\'' + class_or_tuple.__name__ + '\''
raise TypeError(f'expected {exception_message_expected}, got \'{type(obj).__name__}\'')
def non_empty_str(value: Union[str, None]) -> Union[str, None]:
'''
Returns the value or ``None``.
Returns the value if it is not empty and not `Ǹone``; otherwise,
returns ``None``.
Please note: values are stripped, i. e. if the value consists only
of spaces, it is also considered empty.
:param value: The value.
:type value: Union[str, None]
:return: The value or ``None``.
:rtype: Union[str, None]
'''
if value is None:
return None
if value.strip() == '':
return None
return value
def get_relative_path_segments(path: pathlib.Path, root: pathlib.Path) -> list[str]:
'''
Returns the path segments relative to ``root``.
:param path: The path.
:type path: pathlib.Path
:param root: The root path.
:type root: pathlib.Path
:return: The path segments relative to ``root``.
:rtype: list[str]
'''
return list(
path.resolve()
.relative_to(
root.resolve()
)
.parts
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,62 @@
# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from src.jcloud_docsgen.core.python import PythonDocumentationGenerator
from src.jcloud_docsgen.core.python.namespaces import PythonModuleNamespace, PythonPackageNamespace
from src.jcloud_docsgen.utils import ExistingDirectory
import pytest
import pathlib
@pytest.mark.parametrize('project_directory,docs_directory,expected_exception,expected_exception_msg', [
(1, 1, TypeError, 'expected \'ExistingDirectory\', got \'int\''),
(1, None, TypeError, 'expected \'ExistingDirectory\', got \'int\''),
(42, None, TypeError, 'expected \'ExistingDirectory\', got \'int\''),
(ExistingDirectory('tests'), 1, TypeError, 'expected \'ExistingDirectory\', got \'int\''),
(ExistingDirectory('tests/'), 1, TypeError, 'expected \'ExistingDirectory\', got \'int\''),
(1, ExistingDirectory('tests/'), TypeError, 'expected \'ExistingDirectory\', got \'int\''),
(1, ExistingDirectory('tests'), TypeError, 'expected \'ExistingDirectory\', got \'int\''),
(None, ExistingDirectory('tests'), TypeError, 'expected \'ExistingDirectory\', got \'NoneType\''),
])
def test_PythonDocumentationGenerator_exceptions(project_directory, docs_directory, expected_exception, expected_exception_msg):
with pytest.raises(expected_exception) as exc_info:
PythonDocumentationGenerator(project_directory, docs_directory)
assert str(exc_info.value) == expected_exception_msg
@pytest.mark.parametrize('python_documentation_generator,expected', [
(PythonDocumentationGenerator(ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_1'), ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_1/docs')), []),
(PythonDocumentationGenerator(ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_2'), ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_2/docs')), [PythonPackageNamespace('pkg', [PythonModuleNamespace('module')])]),
(PythonDocumentationGenerator(ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_3'), ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_3/docs')), [PythonPackageNamespace('pkg', [PythonModuleNamespace('module'), PythonPackageNamespace('pkg', [PythonModuleNamespace('module2')])])]),
])
def test_PythonDocumentationGenerator_collect_modules(python_documentation_generator: PythonDocumentationGenerator, expected):
assert python_documentation_generator.namespace() == expected
@pytest.mark.parametrize('dir', [
d.name
for d in pathlib.Path('tests/unit/core/python/_core/test_PythonDocumentationGenerator/test_generate_documentation/').iterdir()
])
def test_generate_documentation(dir, expectdir):
with expectdir('test_PythonDocumentationGenerator/test_generate_documentation/' + dir) as d:
PythonDocumentationGenerator(
ExistingDirectory(
str(
pathlib.Path('tests/unit/core/python/_core/test_PythonDocumentationGenerator/test_generate_documentation/') /
dir /
'project'
)
),
ExistingDirectory(str(d)),
allow_tables = True,
allow_html = False
).generate_documentation(
) # simplify the tests
@@ -0,0 +1,9 @@
# Module `pkg.module`
## Function `func`
### Function signature
```python
func()
```
@@ -0,0 +1,37 @@
# Module `user_management.exceptions`
## Class `UserNotFoundError`
User does not exist.
### Base classes
- `Exception`
### Docstring
User does not exist.
## Class `InvalidEmailError`
Email address is invalid.
### Base classes
- `Exception`
### Docstring
Email address is invalid.
@@ -0,0 +1,21 @@
# Module `user_management.models`
## Class `User`
Represents a user.
### Decorators
```python
dataclass
```
### Docstring
Represents a user.
@@ -0,0 +1,130 @@
# Module `user_management.service`
## Class `UserService`
Central class for the user management.
### Body
#### Constructor (method `__init__`)
##### Method signature
```python
__init__(self, storage: InMemoryUserStorage)
```
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
| `storage` | `InMemoryUserStorage` | normal | -- | -- |
#### Method `create_user`
Creates and stores a new user.
##### Method signature
```python
create_user(self, user_id: str, name: str, email: str, age: int | None = None) -> User
```
##### Returns
Return type: `User`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
| `user_id` | `str` | normal | -- | -- |
| `name` | `str` | normal | -- | -- |
| `email` | `str` | normal | -- | -- |
| `age` | `int | None` | normal | `None` | -- |
##### Docstring
Creates and stores a new user.
#### Method `get_user`
Returns a user.
##### Method signature
```python
get_user(self, user_id: str) -> User
```
##### Returns
Return type: `User`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
| `user_id` | `str` | normal | -- | -- |
##### Docstring
Returns a user.
#### Method `delete_user`
Deletes a user.
##### Method signature
```python
delete_user(self, user_id: str) -> None
```
##### Returns
Return type: `None`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
| `user_id` | `str` | normal | -- | -- |
##### Docstring
Deletes a user.
### Docstring
Central class for the user management.
@@ -0,0 +1,158 @@
# Module `user_management.storage`
## Class `InMemoryUserStorage`
A simple in-memory storage for users.
Intended for testing purposes only.
### Body
#### Constructor (method `__init__`)
##### Method signature
```python
__init__(self)
```
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
#### Method `add`
Adds a user.
##### Method signature
```python
add(self, user: User) -> None
```
##### Returns
Return type: `None`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
| `user` | `User` | normal | -- | -- |
##### Docstring
Adds a user.
#### Method `get`
Returns a user based on their ID.
##### Method signature
```python
get(self, user_id: str) -> User
```
##### Returns
Return type: `User`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
| `user_id` | `str` | normal | -- | -- |
##### Docstring
Returns a user based on their ID.
#### Method `delete`
Deletes a user.
##### Method signature
```python
delete(self, user_id: str) -> None
```
##### Returns
Return type: `None`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
| `user_id` | `str` | normal | -- | -- |
##### Docstring
Deletes a user.
#### Method `list_all`
Returns all users.
##### Method signature
```python
list_all(self) -> List[User]
```
##### Returns
Return type: `List[User]`
##### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `self` | -- | normal | -- | -- |
##### Docstring
Returns all users.
### Docstring
A simple in-memory storage for users.
Intended for testing purposes only.
@@ -0,0 +1,61 @@
# Module `user_management.utils`
## Function `is_valid_email`
Checks whether an email address is valid.
### Function signature
```python
is_valid_email(email: str) -> bool
```
### Returns
Return type: `bool`
### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `email` | `str` | normal | -- | -- |
### Docstring
Checks whether an email address is valid.
## Function `normalize_name`
Removes extra spaces and formats the name.
### Function signature
```python
normalize_name(name: str) -> str
```
### Returns
Return type: `str`
### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `name` | `str` | normal | -- | -- |
### Docstring
Removes extra spaces and formats the name.
@@ -0,0 +1,8 @@
class UserNotFoundError(Exception):
"""User does not exist."""
pass
class InvalidEmailError(Exception):
"""Email address is invalid."""
pass
@@ -0,0 +1,13 @@
from dataclasses import dataclass
from typing import Optional
@dataclass
class User:
"""
Represents a user.
"""
id: str
name: str
email: str
age: Optional[int] = None
@@ -0,0 +1,48 @@
from .models import User
from .storage import InMemoryUserStorage
from .exceptions import UserNotFoundError, InvalidEmailError
from .utils import is_valid_email, normalize_name
class UserService:
"""
Central class for the user management.
"""
def __init__(self, storage: InMemoryUserStorage):
self.storage = storage
def create_user(self, user_id: str, name: str, email: str, age: int | None = None) -> User:
"""
Creates and stores a new user.
"""
if not is_valid_email(email):
raise InvalidEmailError(f"Invalid email: {email}")
user = User(
id=user_id,
name=normalize_name(name),
email=email,
age=age
)
self.storage.add(user)
return user
def get_user(self, user_id: str) -> User:
"""
Returns a user.
"""
try:
return self.storage.get(user_id)
except KeyError:
raise UserNotFoundError(user_id)
def delete_user(self, user_id: str) -> None:
"""
Deletes a user.
"""
try:
self.storage.delete(user_id)
except KeyError:
raise UserNotFoundError(user_id)
@@ -0,0 +1,28 @@
from typing import Dict, List
from .models import User
class InMemoryUserStorage:
"""
A simple in-memory storage for users.
Intended for testing purposes only.
"""
def __init__(self):
self._users: Dict[str, User] = {}
def add(self, user: User) -> None:
"""Adds a user."""
self._users[user.id] = user
def get(self, user_id: str) -> User:
"""Returns a user based on their ID."""
return self._users[user_id]
def delete(self, user_id: str) -> None:
"""Deletes a user."""
del self._users[user_id]
def list_all(self) -> List[User]:
"""Returns all users."""
return list(self._users.values())
@@ -0,0 +1,16 @@
import re
def is_valid_email(email: str) -> bool:
"""
Checks whether an email address is valid.
"""
pattern = r"^[\w\.-]+@[\w\.-]+\.\w+$"
return re.match(pattern, email) is not None
def normalize_name(name: str) -> str:
"""
Removes extra spaces and formats the name.
"""
return " ".join(name.strip().split()).title()
@@ -0,0 +1,964 @@
# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from src.jcloud_docsgen.core.python import PythonModuleDocumentationGenerator
from src.jcloud_docsgen.exceptions import InvalidNamespaceError
import pytest
from src.jcloud_docsgen.utils import ExistingFile, ExistingDirectory
@pytest.mark.parametrize('module_path,src_directory,expected_exception,expected_exception_msg', [
(1, 1, TypeError, 'expected \'ExistingFile\', got \'int\''),
(1, None, TypeError, 'expected \'ExistingFile\', got \'int\''),
(42, None, TypeError, 'expected \'ExistingFile\', got \'int\''),
(ExistingFile('tests/unit/utils/test_file'), 1, TypeError, 'expected either \'ExistingDirectory\' or \'NoneType\', got \'int\''),
(ExistingDirectory('tests/'), 1, TypeError, 'expected \'ExistingFile\', got \'ExistingDirectory\''),
(ExistingDirectory('tests'), 1, TypeError, 'expected \'ExistingFile\', got \'ExistingDirectory\''),
(1, ExistingDirectory('tests/'), TypeError, 'expected \'ExistingFile\', got \'int\''),
(1, ExistingDirectory('tests'), TypeError, 'expected \'ExistingFile\', got \'int\''),
(None, ExistingDirectory('tests'), TypeError, 'expected \'ExistingFile\', got \'NoneType\''),
(None, None, TypeError, 'expected \'ExistingFile\', got \'NoneType\''),
(1, None, TypeError, 'expected \'ExistingFile\', got \'int\''),
(ExistingFile('tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c'), None, InvalidNamespaceError, 'namespace identifier cannot contain more than one dot: a.b.c'),
(ExistingFile('tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d'), None, InvalidNamespaceError, 'namespace identifier cannot contain more than one dot: a.b.c.d'),
(ExistingFile('tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e'), None, InvalidNamespaceError, 'namespace identifier cannot contain more than one dot: a.b.c.d.e'),
(ExistingFile('tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.f'), None, InvalidNamespaceError, 'namespace identifier cannot contain more than one dot: a.b.c.d.e.f'),
(ExistingFile('tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.f.g'), None, InvalidNamespaceError, 'namespace identifier cannot contain more than one dot: a.b.c.d.e.f.g'),
(ExistingFile('tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.f.g.h'), None, InvalidNamespaceError, 'namespace identifier cannot contain more than one dot: a.b.c.d.e.f.g.h'),
(ExistingFile('tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.f.g.h.py'), None, InvalidNamespaceError, 'namespace identifier cannot contain more than one dot: a.b.c.d.e.f.g.h.py'),
(ExistingFile('tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.f.g.py'), None, InvalidNamespaceError, 'namespace identifier cannot contain more than one dot: a.b.c.d.e.f.g.py'),
(ExistingFile('tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.f.py'), None, InvalidNamespaceError, 'namespace identifier cannot contain more than one dot: a.b.c.d.e.f.py'),
(ExistingFile('tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.e.py'), None, InvalidNamespaceError, 'namespace identifier cannot contain more than one dot: a.b.c.d.e.py'),
(ExistingFile('tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.d.py'), None, InvalidNamespaceError, 'namespace identifier cannot contain more than one dot: a.b.c.d.py'),
(ExistingFile('tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.c.py'), None, InvalidNamespaceError, 'namespace identifier cannot contain more than one dot: a.b.c.py'),
(ExistingFile('tests/unit/core/python/_core/files_with_more_than_one_dot/a.b.py'), None, InvalidNamespaceError, 'namespace identifier cannot contain more than one dot: a.b.py'),
(ExistingFile('tests/unit/core/python/_core/directory.with.more.than.one.dot/a.b.py'), None, InvalidNamespaceError, 'namespace identifier cannot contain more than one dot: a.b.py'),
])
def test_PythonModuleDocumentationGenerator_exceptions(module_path, src_directory, expected_exception, expected_exception_msg):
with pytest.raises(expected_exception) as exc_info:
PythonModuleDocumentationGenerator(module_path, src_directory)
assert str(exc_info.value) == expected_exception_msg
@pytest.mark.parametrize('module_documentation_generator,expected', [
(PythonModuleDocumentationGenerator(ExistingFile('tests/unit/core/python/_core/test_project_dirs/pdir_2/src/pkg/module.py'), ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_2/src/'), allow_tables = True, allow_html = False), '''# Module `pkg.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(ExistingFile('tests/unit/core/python/_core/test_project_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(ExistingFile('tests/unit/core/python/_core/test_project_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.
:type a: int | float
:param b: The second number.
:type b: int | float
:return: The sum.
:rtype: int | float</details>
## 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 | -- | -- |
<details><summary><h5 style="display:inline">Docstring</h5></summary>Returns a greeting.
:return: The greeting.
:rtype: str</details>
<details><summary><h3 style="display:inline">Docstring</h3></summary>A class representing an animal.
:param name: Its name.
:type name: str</details>
## 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 | -- | -- |
<details><summary><h5 style="display:inline">Docstring</h5></summary>Barks.</details>
<details><summary><h3 style="display:inline">Docstring</h3></summary>A dog.</details>'''),
(PythonModuleDocumentationGenerator(ExistingFile('tests/unit/core/python/_core/test_project_dirs/pdir_2/src/pkg/module.py'), None, allow_tables = False, 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: `a`
Type: `int | float`
Kind: normal
Default value: --
Description: The first number.
- Parameter: `b`
Type: `int | float`
Kind: normal
Default value: --
Description: The second number.
<details><summary><h3 style="display:inline">Docstring</h3></summary>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</details>
## 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: `self`
Type: --
Kind: normal
Default value: --
Description: --
- Parameter: `name`
Type: `str`
Kind: normal
Default value: --
Description: --
#### Method `greet`
Returns a greeting.
##### Method signature
```python
greet(self)
```
##### Returns
The greeting.
Return type: `str`
##### Parameters
- Parameter: `self`
Type: --
Kind: normal
Default value: --
Description: --
<details><summary><h5 style="display:inline">Docstring</h5></summary>Returns a greeting.
:return: The greeting.
:rtype: str</details>
<details><summary><h3 style="display:inline">Docstring</h3></summary>A class representing an animal.
:param name: Its name.
:type name: str</details>
## Class `Dog`
A dog.
### Base classes
- `Animal`
### Body
#### Method `bark`
Barks.
##### Method signature
```python
bark(self) -> None
```
##### Returns
Return type: `None`
##### Parameters
- Parameter: `self`
Type: --
Kind: normal
Default value: --
Description: --
<details><summary><h5 style="display:inline">Docstring</h5></summary>Barks.</details>
<details><summary><h3 style="display:inline">Docstring</h3></summary>A dog.</details>'''),
(PythonModuleDocumentationGenerator(ExistingFile('tests/unit/core/python/_core/test_project_dirs/pdir_2/src/pkg/module.py'), None, allow_tables = False, 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: `a`
Type: `int | float`
Kind: normal
Default value: --
Description: The first number.
- Parameter: `b`
Type: `int | float`
Kind: normal
Default value: --
Description: 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: `self`
Type: --
Kind: normal
Default value: --
Description: --
- Parameter: `name`
Type: `str`
Kind: normal
Default value: --
Description: --
#### Method `greet`
Returns a greeting.
##### Method signature
```python
greet(self)
```
##### Returns
The greeting.
Return type: `str`
##### Parameters
- Parameter: `self`
Type: --
Kind: normal
Default value: --
Description: --
##### 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: `self`
Type: --
Kind: normal
Default value: --
Description: --
##### Docstring
Barks.
### Docstring
A dog.'''),
])
def test_PythonModuleDocumentationGenerator_exceptions(module_documentation_generator, expected):
assert module_documentation_generator.generate_documentation() == expected
@@ -0,0 +1,27 @@
# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from src.jcloud_docsgen.core.python._core import _argument_kind_human_readable
from src.jcloud_docsgen.core.python.arguments import PythonArgumentKind
import pytest
@pytest.mark.parametrize('argument_kind,expected', [
(PythonArgumentKind.NORMAL, 'normal'),
(PythonArgumentKind.POSITIONAL_ONLY, 'positional-only'),
(PythonArgumentKind.KEYWORD_ONLY, 'keyword-only'),
(PythonArgumentKind.VARARG, 'vararg'),
(PythonArgumentKind.KWARGS, 'kwargs'),
])
def test__argument_kind_human_readable(argument_kind, expected):
assert _argument_kind_human_readable(argument_kind) == expected
@@ -0,0 +1,139 @@
# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from src.jcloud_docsgen.core.python._core import _collect_definitions
from src.jcloud_docsgen.core.python.definitions import PythonClassDefinition, PythonFunctionDefinition
from src.jcloud_docsgen.core.python.arguments import PythonFunctionArgument, PythonArgumentKind
from src.jcloud_docsgen.core.python.docstrings import PythonDocstring
import pytest
import ast
def module(source_code: str) -> ast.Module:
'''
Returns an AST module of the source code.
:param source_code: The module source code.
:type source_code: str
:return: The AST module.
:rtype: ast.Module
'''
return ast.parse(source_code)
@pytest.mark.parametrize('module,expected', [
(module('def add(a, b):\n\treturn a + b'), [
PythonFunctionDefinition('add', [
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, None),
PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, None),
], None, None, [])
]),
(module('''def add(a: int | float, b: int | float) -> int | float:
"""Adds two numbers."""
return a + b
import typing
class Something:
"""A class."""
def __init__(self, value: typing.Any = 42) -> None:
self._value = value
@property
def value(self) -> typing.Any:
"""
The value.
"""
return self.get_value()
def get_value(self) -> typing.Any:
"""
Returns the value.
"""
return self._value
'''), [
PythonFunctionDefinition('add', [
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, 'int | float'),
PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, 'int | float'),
], 'int | float', PythonDocstring('Adds two numbers.'), []),
PythonClassDefinition('Something', [], PythonDocstring('A class.'), [], [
PythonFunctionDefinition('__init__', [
PythonFunctionArgument('self', PythonArgumentKind.NORMAL, None, None),
PythonFunctionArgument('value', PythonArgumentKind.NORMAL, '42', 'typing.Any'),
], 'None', None, []),
PythonFunctionDefinition('value', [
PythonFunctionArgument('self', PythonArgumentKind.NORMAL, None, None),
], 'typing.Any', PythonDocstring('The value.'), ['property']),
PythonFunctionDefinition('get_value', [
PythonFunctionArgument('self', PythonArgumentKind.NORMAL, None, None),
], 'typing.Any', PythonDocstring('Returns the value.'), []),
])
]),
(module('''from src.jcloud_docsgen.core.python import PythonDocumentationGenerator
from src.jcloud_docsgen.core.python.namespaces import PythonModuleNamespace, PythonPackageNamespace
import pytest
from src.jcloud_docsgen.utils import ExistingDirectory
@pytest.mark.parametrize('project_directory,docs_directory', [
(1, 1),
(1, None),
(42, None),
(ExistingDirectory('tests'), 1),
(ExistingDirectory('tests/'), 1),
(1, ExistingDirectory('tests/')),
(1, ExistingDirectory('tests')),
(None, ExistingDirectory('tests')),
])
def test_PythonDocumentationGenerator_type_exceptions(project_directory, docs_directory):
with pytest.raises(TypeError):
PythonDocumentationGenerator(project_directory, docs_directory)
@pytest.mark.parametrize('python_documentation_generator,expected', [
(PythonDocumentationGenerator(ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_1'), ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_1/docs')), []),
(PythonDocumentationGenerator(ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_2'), ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_2/docs')), [PythonPackageNamespace('pkg', [PythonModuleNamespace('module')])]),
(PythonDocumentationGenerator(ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_3'), ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_3/docs')), [PythonPackageNamespace('pkg', [PythonModuleNamespace('module'), PythonPackageNamespace('pkg', [PythonModuleNamespace('module2')])])]),
])
def test_PythonDocumentationGenerator_collect_modules(python_documentation_generator: PythonDocumentationGenerator, expected):
assert python_documentation_generator.namespace() == expected'''), [
PythonFunctionDefinition('test_PythonDocumentationGenerator_type_exceptions', [
PythonFunctionArgument('project_directory', PythonArgumentKind.NORMAL, None, None),
PythonFunctionArgument('docs_directory', PythonArgumentKind.NORMAL, None, None),
], None, None, ['''pytest.mark.parametrize('project_directory,docs_directory', [(1, 1), (1, None), (42, None), (ExistingDirectory('tests'), 1), (ExistingDirectory('tests/'), 1), (1, ExistingDirectory('tests/')), (1, ExistingDirectory('tests')), (None, ExistingDirectory('tests'))])''']),
PythonFunctionDefinition('test_PythonDocumentationGenerator_collect_modules', [
PythonFunctionArgument('python_documentation_generator', PythonArgumentKind.NORMAL, None, 'PythonDocumentationGenerator'),
PythonFunctionArgument('expected', PythonArgumentKind.NORMAL, None, None)
], None, None, ['''pytest.mark.parametrize('python_documentation_generator,expected', [(PythonDocumentationGenerator(ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_1'), ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_1/docs')), []), (PythonDocumentationGenerator(ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_2'), ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_2/docs')), [PythonPackageNamespace('pkg', [PythonModuleNamespace('module')])]), (PythonDocumentationGenerator(ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_3'), ExistingDirectory('tests/unit/core/python/_core/test_project_dirs/pdir_3/docs')), [PythonPackageNamespace('pkg', [PythonModuleNamespace('module'), PythonPackageNamespace('pkg', [PythonModuleNamespace('module2')])])])])'''])
]),
(module('''class C1:
class C2:
class C3:
def function(self, value: bool, /, arg) -> bool:
def should_be_not_in_the_result(arg: int) -> object:
pass
return not value'''), [
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):
assert list(_collect_definitions(module)) == expected
@@ -0,0 +1,56 @@
# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from src.jcloud_docsgen.core.python._core import _fit_row_length
import pytest
@pytest.mark.parametrize('expected_length,row,expected', [
(0, (), ()),
(1, (), (None,)),
(2, (), (None, None)),
(2, (1,), (1, None)),
(0, (1,), ()),
(1, (1, 2), (1,)),
(1, (1, 2), (1,)),
(0, (1, 2), ()),
(0, (1, 2, 3), ()),
(1, (1, 2, 3), (1,)),
(2, (1, 2, 3), (1, 2)),
(1, (1,), (1,)),
(2, (1, 1), (1, 1)),
(2, (1, 2), (1, 2)),
(3, (1, 2, 3), (1, 2, 3)),
])
def test__fit_row_length(expected_length, row, expected):
assert _fit_row_length(expected_length, row) == expected
@pytest.mark.parametrize('row', [
(),
(1,),
(1, 2),
(1, 2, 3),
(1, 2, 3, 456),
])
@pytest.mark.parametrize('expected_length,expected_exception', [
(-1, ValueError),
(-2, ValueError),
(-3, ValueError),
(-42, ValueError),
])
def test__fit_row_length_exceptions(expected_length, row, expected_exception):
with pytest.raises(expected_exception) as exc_info:
_fit_row_length(expected_length, row)
assert str(exc_info.value) == 'expected must be larger than or equal 0.'
@@ -0,0 +1,104 @@
# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from src.jcloud_docsgen.core.python._core import _function_params_info, _FunctionParamInfo
from src.jcloud_docsgen.core.python.arguments import PythonFunctionArgument, PythonArgumentKind
import pytest
import docstring_parser
def docstring_args_list(docstring: str) -> list[docstring_parser.DocstringParam]:
'''
Returns the parameter list of a docstring.
:param docstring: The docstring
:type docstring: str
:return: The parameter list.
:rtype: list[docstring_parser.DocstringParam]
'''
return docstring_parser.parse(docstring).params
@pytest.mark.parametrize('arg_list,docstring,expected', [
([
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', 'int'),
], ':param a: a\n:type a: str', {
'a': _FunctionParamInfo('int', 'a')
}),
([
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', None),
], ':param a: a\n:type a: str', {
'a': _FunctionParamInfo('str', 'a')
}),
([
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', None),
PythonFunctionArgument('b', PythonArgumentKind.NORMAL, '', None),
], ':param a: a\n:type a: str', {
'a': _FunctionParamInfo('str', 'a'),
'b': _FunctionParamInfo(None, None)
}),
([
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', None),
PythonFunctionArgument('b', PythonArgumentKind.NORMAL, '', 'int'),
], ':param a: a\n:type a: str', {
'a': _FunctionParamInfo('str', 'a'),
'b': _FunctionParamInfo('int', None)
}),
([
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', None),
PythonFunctionArgument('b', PythonArgumentKind.NORMAL, '', 'int'),
], ':param a: a\n:type a: str\n:param b: b\n:type b: str', {
'a': _FunctionParamInfo('str', 'a'),
'b': _FunctionParamInfo('int', 'b')
}),
([
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', None),
], ':param a: a\n:type a: str\n:param b: b\n:type b: int', {
'a': _FunctionParamInfo('str', 'a')
}),
([
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', None),
], '', {
'a': _FunctionParamInfo(None, None)
}),
([
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', 'str'),
], '', {
'a': _FunctionParamInfo('str', None)
}),
([
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', 'str'),
], ':param a:\n:type a: int', {
'a': _FunctionParamInfo('str', '')
}),
([
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', None),
], ':param a:\n:type a: int', {
'a': _FunctionParamInfo('int', '')
}),
([
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', None),
], ':param a: a', {
'a': _FunctionParamInfo(None, 'a')
}),
([
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '', 'str'),
], ':param a: a', {
'a': _FunctionParamInfo('str', 'a')
}),
([], ':param a: a\n:type a: str', {}),
([], '', {}),
])
def test__function_params_types(arg_list, docstring, expected):
assert _function_params_info(arg_list, docstring_args_list(docstring)) == expected
@@ -0,0 +1,93 @@
# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from src.jcloud_docsgen.core.python._core import _markdown_table
import pytest
@pytest.mark.parametrize('header,body,allow_markdown_table,force_row_lengths,one_line_values,missing_value,expected', [
([], [], False, False, True, '--', ''),
([], [('a',)], False, False, True, '--', ''),
(['a'], [], False, False, True, '--', ''),
(['a'], [(1,)], True, False, True, '--', '''| a |
| --- |
| 1 |'''),
(['a'], [(1, 2)], True, False, True, '--', '''| a |
| --- |
| 1 |'''),
(['a', 'b'], [(1, 2)], True, False, True, '--', '''| a | b |
| --- | --- |
| 1 | 2 |'''),
(['a', 'b'], [(1,)], True, False, True, '--', '''| a | b |
| --- | --- |
| 1 | -- |'''),
(['a', 'b'], [()], True, False, True, '--', '''| a | b |
| --- | --- |
| -- | -- |'''),
(['a', 'b'], [(1, 2), (3, 4)], True, False, True, '--', '''| a | b |
| --- | --- |
| 1 | 2 |
| 3 | 4 |'''),
(['a'], [('line1\nline2',)], True, False, True, '--', '''| a |
| --- |
| line1 line2 |'''),
(['a'], [('line1\nline2',)], True, False, False, '--', '''| a |
| --- |
| line1\nline2 |'''),
([], [], False, False, True, '--', ''),
([], [('a',)], False, False, True, '--', ''),
(['a'], [], False, False, True, '--', ''),
(['a'], [(1,)], False, False, True, '--', '''- a: 1'''),
(['a'], [(1, 2)], False, False, True, '--', '''- a: 1'''),
(['a', 'b'], [(1, 2)], False, False, True, '--', '''- a: 1
b: 2'''),
(['a', 'b'], [(1,)], False, False, True, '--', '''- a: 1
b: --'''),
(['a', 'b'], [()], False, False, True, '--', '''- a: --
b: --'''),
(['a', 'b'], [(1, 2), (3, 4)], False, False, True, '--', '''- a: 1
b: 2
- a: 3
b: 4'''),
(['a'], [('line1\nline2',)], False, False, True, '--', '- a: line1 line2'),
(['a'], [('line1\nline2',)], False, False, False, '--', '- a: line1\nline2'),
])
def test__markdown_table(header, body, allow_markdown_table, force_row_lengths, one_line_values, missing_value, expected):
assert _markdown_table(header, body, allow_markdown_table = allow_markdown_table, force_row_lengths = force_row_lengths, one_line_values = one_line_values, missing_value = missing_value) == expected
@pytest.mark.parametrize('header,body,allow_markdown_table,force_row_lengths,one_line_values,missing_value,expected_exception,expected_exception_msg', [
(['a'], [(1, 2)], True, True, True, '--', ValueError, 'expected 1 value, got 2'),
(['a'], [(1, 2, 3)], True, True, True, '--', ValueError, 'expected 1 value, got 3'),
(['a'], [()], True, True, True, '--', ValueError, 'expected 1 value, got 0'),
(['a', 'b'], [()], True, True, True, '--', ValueError, 'expected 2 values, got 0'),
(['a', 'b'], [(1,)], True, True, True, '--', ValueError, 'expected 2 values, got 1'),
(['a', 'b'], [(1, 2, 3)], True, True, True, '--', ValueError, 'expected 2 values, got 3'),
(['a'], [(1, 2)], False, True, True, '--', ValueError, 'expected 1 value, got 2'),
(['a'], [(1, 2, 3)], False, True, True, '--', ValueError, 'expected 1 value, got 3'),
(['a'], [()], False, True, True, '--', ValueError, 'expected 1 value, got 0'),
(['a', 'b'], [()], False, True, True, '--', ValueError, 'expected 2 values, got 0'),
(['a', 'b'], [(1,)], False, True, True, '--', ValueError, 'expected 2 values, got 1'),
(['a', 'b'], [(1, 2, 3)], False, True, True, '--', ValueError, 'expected 2 values, got 3'),
])
def test__markdown_table_exceptions(header, body, allow_markdown_table, force_row_lengths, one_line_values, missing_value, expected_exception, expected_exception_msg):
with pytest.raises(expected_exception) as exc_info:
_markdown_table(header, body, allow_markdown_table = allow_markdown_table, force_row_lengths = force_row_lengths, one_line_values = one_line_values, missing_value = missing_value)
assert str(exc_info.value) == expected_exception_msg
@@ -0,0 +1,97 @@
# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from src.jcloud_docsgen.core.python._core import _signature_argument_list
from src.jcloud_docsgen.core.python.arguments import PythonFunctionArgument, PythonArgumentKind
import pytest
@pytest.mark.parametrize('argument_list,expected', [
([
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, None)
], ['a']),
([
PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, None)
], ['a', '/']),
([
PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, None),
PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, None),
], ['a', '/', 'b']),
([
PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, None),
PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, None),
PythonFunctionArgument('c', PythonArgumentKind.KEYWORD_ONLY, None, None),
], ['a', '/', 'b', '*', 'c']),
([
PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, None),
PythonFunctionArgument('c', PythonArgumentKind.KEYWORD_ONLY, None, None),
], ['b', '*', 'c']),
([
PythonFunctionArgument('c', PythonArgumentKind.KEYWORD_ONLY, None, None),
], ['*', 'c']),
([
PythonFunctionArgument('b', PythonArgumentKind.POSITIONAL_ONLY, None, None),
PythonFunctionArgument('c', PythonArgumentKind.KEYWORD_ONLY, None, None),
], ['b', '/', '*', 'c']),
([
PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, None),
], ['*args']),
([
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, None),
PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, None),
], ['a', '*args']),
([
PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, None),
PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, None),
], ['a', '/', '*args']),
([
PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, None),
PythonFunctionArgument('a', PythonArgumentKind.KEYWORD_ONLY, None, None),
], ['*args', 'a']),
([
PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, None),
PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, None),
], ['*args', '**kwargs']),
([
PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, None),
PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, None),
PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, None),
], ['a', '/', '*args', '**kwargs']),
([
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, None),
PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, None),
PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, None),
], ['a', '*args', '**kwargs']),
([
PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, None),
PythonFunctionArgument('a', PythonArgumentKind.KEYWORD_ONLY, None, None),
PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, None),
], ['*args', 'a', '**kwargs']),
([
PythonFunctionArgument('a', PythonArgumentKind.KEYWORD_ONLY, None, None),
PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, None),
], ['*', 'a', '**kwargs']),
([
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, None),
PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, None),
], ['a', '**kwargs']),
([
PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, None),
PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, None),
], ['a', '/', '**kwargs']),
([
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '1', 'int'),
], ['a: int = 1']),
])
def test__signature_argument_list(argument_list, expected):
assert _signature_argument_list(argument_list) == expected
@@ -0,0 +1,50 @@
'''
A test module.
'''
def add(a: int | float, b: int | float) -> int | float:
'''
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
'''
return a + b
class Animal:
'''
A class representing an animal.
:param name: Its name.
:type name: str
'''
def __init__(self, name: str) -> None:
self.name = name
def greet(self):
'''
Returns a greeting.
:return: The greeting.
:rtype: str
'''
return f'Hi there! My name is {self.name}'
class Dog(Animal):
'''
A dog.
'''
def bark(self) -> None:
'''
Barks.
'''
print('Woof!')
@@ -0,0 +1,215 @@
# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from src.jcloud_docsgen.core.python.arguments import PythonASTArgumentsListParser, PythonFunctionArgument, PythonArgumentKind
import pytest
import ast
def argument_list(argument_list: str) -> ast.arguments:
'''
Returns an AST argument list from the argument list source code
:param argument_list: The argument list.
:type argument_list: str
:return: The AST argument list.
:rtype: ast.arguments
'''
return ast.parse('def func(' + argument_list + '): ...').body[0].args
@pytest.mark.parametrize('ast_arguments_list,expected', [
(argument_list('a'), [PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, None)]),
(argument_list('a: str'), [PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, 'str')]),
(argument_list('a: str | int'), [PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, 'str | int')]),
(argument_list('a: str, b'), [
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, 'str'),
PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, None)
]),
(argument_list('a: str, b: int'), [
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, 'str'),
PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, 'int')
]),
(argument_list('a, b: int'), [
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, None),
PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, 'int')
]),
(argument_list('a, b, *args'), [
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, None),
PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, None),
PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, None)
]),
(argument_list('a: int, b, *args'), [
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, 'int'),
PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, None),
PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, None)
]),
(argument_list('a, b: str, *args'), [
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, None),
PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, 'str'),
PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, None)
]),
(argument_list('a: int, b: str, *args'), [
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, 'int'),
PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, 'str'),
PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, None)
]),
(argument_list('a: int, b: str, *args: float'), [
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, 'int'),
PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, 'str'),
PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, 'float')
]),
(argument_list('a: int, b, *args: float'), [
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, 'int'),
PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, None),
PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, 'float')
]),
(argument_list('a: int, *args: float'), [
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, 'int'),
PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, 'float')
]),
(argument_list('a: int, /, **kwargs'), [
PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, 'int'),
PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, None)
]),
(argument_list('a: int, **kwargs: str'), [
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, 'int'),
PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, 'str')
]),
(argument_list('a: int, /, b, **kwargs'), [
PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, 'int'),
PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, None),
PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, None)
]),
(argument_list('a: int, /, b'), [
PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, 'int'),
PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, None),
]),
(argument_list('a: int, /, b, *, c'), [
PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, 'int'),
PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, None),
PythonFunctionArgument('c', PythonArgumentKind.KEYWORD_ONLY, None, None),
]),
(argument_list('a, b, *, c'), [
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, None),
PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, None),
PythonFunctionArgument('c', PythonArgumentKind.KEYWORD_ONLY, None, None),
]),
(argument_list('a, b, *args, c'), [
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, None),
PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, None),
PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, None),
PythonFunctionArgument('c', PythonArgumentKind.KEYWORD_ONLY, None, None),
]),
(argument_list('a, b, *args, c, **kwargs'), [
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, None),
PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, None),
PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, None),
PythonFunctionArgument('c', PythonArgumentKind.KEYWORD_ONLY, None, None),
PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, None),
]),
(argument_list('a, b, /, *, c, **kwargs'), [
PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, None),
PythonFunctionArgument('b', PythonArgumentKind.POSITIONAL_ONLY, None, None),
PythonFunctionArgument('c', PythonArgumentKind.KEYWORD_ONLY, None, None),
PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, None),
]),
(argument_list('a, b, /, c, *, d, **kwargs'), [
PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, None),
PythonFunctionArgument('b', PythonArgumentKind.POSITIONAL_ONLY, None, None),
PythonFunctionArgument('c', PythonArgumentKind.NORMAL, None, None),
PythonFunctionArgument('d', PythonArgumentKind.KEYWORD_ONLY, None, None),
PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, None),
]),
(argument_list('a: str, b: int, /, c: float, d: list, *, e: tuple, f: dict, **kwargs: set'), [
PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, 'str'),
PythonFunctionArgument('b', PythonArgumentKind.POSITIONAL_ONLY, None, 'int'),
PythonFunctionArgument('c', PythonArgumentKind.NORMAL, None, 'float'),
PythonFunctionArgument('d', PythonArgumentKind.NORMAL, None, 'list'),
PythonFunctionArgument('e', PythonArgumentKind.KEYWORD_ONLY, None, 'tuple'),
PythonFunctionArgument('f', PythonArgumentKind.KEYWORD_ONLY, None, 'dict'),
PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, 'set'),
]),
(argument_list('a: str, b: int, /, c: float, d: list, *args: bytes, e: tuple, f: dict, **kwargs: set'), [
PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, 'str'),
PythonFunctionArgument('b', PythonArgumentKind.POSITIONAL_ONLY, None, 'int'),
PythonFunctionArgument('c', PythonArgumentKind.NORMAL, None, 'float'),
PythonFunctionArgument('d', PythonArgumentKind.NORMAL, None, 'list'),
PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, 'bytes'),
PythonFunctionArgument('e', PythonArgumentKind.KEYWORD_ONLY, None, 'tuple'),
PythonFunctionArgument('f', PythonArgumentKind.KEYWORD_ONLY, None, 'dict'),
PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, 'set'),
]),
(argument_list('a: str, b: int, /, *args: bytes, c: tuple, d: dict, **kwargs: set'), [
PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, 'str'),
PythonFunctionArgument('b', PythonArgumentKind.POSITIONAL_ONLY, None, 'int'),
PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, 'bytes'),
PythonFunctionArgument('c', PythonArgumentKind.KEYWORD_ONLY, None, 'tuple'),
PythonFunctionArgument('d', PythonArgumentKind.KEYWORD_ONLY, None, 'dict'),
PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, 'set'),
]),
(argument_list('a = 1, b = \'a\', /, *args, c = None, d = True, **kwargs'), [
PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, '1', None),
PythonFunctionArgument('b', PythonArgumentKind.POSITIONAL_ONLY, '\'a\'', None),
PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, None),
PythonFunctionArgument('c', PythonArgumentKind.KEYWORD_ONLY, 'None', None),
PythonFunctionArgument('d', PythonArgumentKind.KEYWORD_ONLY, 'True', None),
PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, None),
]),
(argument_list('a, b = \'a\', /, *args, c = None, d = True, **kwargs'), [
PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, None),
PythonFunctionArgument('b', PythonArgumentKind.POSITIONAL_ONLY, '\'a\'', None),
PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, None),
PythonFunctionArgument('c', PythonArgumentKind.KEYWORD_ONLY, 'None', None),
PythonFunctionArgument('d', PythonArgumentKind.KEYWORD_ONLY, 'True', None),
PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, None),
]),
(argument_list('a, b , /, *args, c = None, d = True, **kwargs'), [
PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, None),
PythonFunctionArgument('b', PythonArgumentKind.POSITIONAL_ONLY, None, None),
PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, None),
PythonFunctionArgument('c', PythonArgumentKind.KEYWORD_ONLY, 'None', None),
PythonFunctionArgument('d', PythonArgumentKind.KEYWORD_ONLY, 'True', None),
PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, None),
]),
(argument_list('a, b , /, *args, c, d = True, **kwargs'), [
PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, None),
PythonFunctionArgument('b', PythonArgumentKind.POSITIONAL_ONLY, None, None),
PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, None),
PythonFunctionArgument('c', PythonArgumentKind.KEYWORD_ONLY, None, None),
PythonFunctionArgument('d', PythonArgumentKind.KEYWORD_ONLY, 'True', None),
PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, None),
]),
(argument_list('a, b , /, *args, c, d, **kwargs'), [
PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, None),
PythonFunctionArgument('b', PythonArgumentKind.POSITIONAL_ONLY, None, None),
PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, None),
PythonFunctionArgument('c', PythonArgumentKind.KEYWORD_ONLY, None, None),
PythonFunctionArgument('d', PythonArgumentKind.KEYWORD_ONLY, None, None),
PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, None),
]),
(argument_list('a, b , /, *args, c = None, d, **kwargs'), [
PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, None),
PythonFunctionArgument('b', PythonArgumentKind.POSITIONAL_ONLY, None, None),
PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, None),
PythonFunctionArgument('c', PythonArgumentKind.KEYWORD_ONLY, 'None', None),
PythonFunctionArgument('d', PythonArgumentKind.KEYWORD_ONLY, None, None),
PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, None),
]),
(argument_list('a = 1'), [
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, '1', None)
]),
])
def test_PythonASTArgumentsListParser_to_argument_list(ast_arguments_list, expected):
assert PythonASTArgumentsListParser(ast_arguments_list).to_argument_list() == expected
@@ -0,0 +1,157 @@
# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from src.jcloud_docsgen.core.python.arguments import PythonFunctionArgument, PythonArgumentKind
from src.jcloud_docsgen.exceptions import InvalidPythonIdentifierError, InvalidPythonAnnotationError, PythonArgumentStructureError
import pytest
@pytest.mark.parametrize('name,kind,default,annotation,expected_exception,expected_exception_msg', [
(None, PythonArgumentKind.NORMAL, None, None, TypeError, 'expected \'str\', got \'NoneType\''),
(1, PythonArgumentKind.NORMAL, None, None, TypeError, 'expected \'str\', got \'int\''),
(42, PythonArgumentKind.NORMAL, None, None, TypeError, 'expected \'str\', got \'int\''),
(4.2, PythonArgumentKind.NORMAL, None, None, TypeError, 'expected \'str\', got \'float\''),
(4.2, PythonArgumentKind.NORMAL, None, None, TypeError, 'expected \'str\', got \'float\''),
('a', PythonArgumentKind.NORMAL, 1, None, TypeError, 'expected either \'str\' or \'NoneType\', got \'int\''),
('a', PythonArgumentKind.NORMAL, None, 1, TypeError, 'expected either \'str\' or \'NoneType\', got \'int\''),
('a', 0, None, None, TypeError, 'expected \'PythonArgumentKind\', got \'int\''),
('a', PythonArgumentKind.NORMAL, None, '', InvalidPythonAnnotationError, 'invalid annotation'),
('', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier'),
('.', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: .'),
('#', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: #'),
('1a', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: 1a'),
('1_', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: 1_'),
('1,', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: 1,'),
(',', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: ,'),
(':', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: :'),
('-', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: -'),
(';', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: ;'),
('|', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: |'),
('<', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: <'),
('>', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: >'),
('a,', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: a,'),
(',a', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: ,a'),
('a:', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: a:'),
(':a', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: :a'),
('a-', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: a-'),
('-a', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: -a'),
('a;', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: a;'),
(';a', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: ;a'),
('a|', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: a|'),
('|a', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: |a'),
('a<', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: a<'),
('<a', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: <a'),
('a>', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: a>'),
('>a', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: >a'),
('class', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: class'),
('def', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: def'),
('lambda', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: lambda'),
('None', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: None'),
('True', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: True'),
('a', PythonArgumentKind.VARARG, '1', None, PythonArgumentStructureError, '* or ** arguments cannot have default values: *a = 1'),
('a', PythonArgumentKind.KWARGS, '1', None, PythonArgumentStructureError, '* or ** arguments cannot have default values: **a = 1'),
])
def test_PythonFunctionArgument_exceptions(name, kind, default, annotation, expected_exception, expected_exception_msg):
with pytest.raises(expected_exception) as exc_info:
PythonFunctionArgument(name, kind, default, annotation)
assert str(exc_info.value) == expected_exception_msg
@pytest.mark.parametrize('name', [
'a',
'_a',
'a1',
'a0',
'a_',
'_',
'_abc',
'abc',
'abc_',
'match',
'case',
'type',
'ä',
'none',
'true',
'false',
])
@pytest.mark.parametrize('kind', [
PythonArgumentKind.NORMAL,
PythonArgumentKind.POSITIONAL_ONLY,
PythonArgumentKind.KEYWORD_ONLY,
])
@pytest.mark.parametrize('default', [
None,
'x',
'1',
None,
])
@pytest.mark.parametrize('annotation', [
None,
'a',
'list[int]',
'_',
])
def test_PythonFunctionArgument(name, kind, default, annotation):
PythonFunctionArgument(name, kind, default, annotation)
@pytest.mark.parametrize('name', [
'a',
'_a',
'a1',
'a0',
'a_',
'_',
'_abc',
'abc',
'abc_',
'match',
'case',
'type',
'ä',
'none',
'true',
'false',
])
@pytest.mark.parametrize('kind', [
PythonArgumentKind.VARARG,
PythonArgumentKind.KWARGS,
])
@pytest.mark.parametrize('annotation', [
None,
'a',
'list[int]',
'_',
])
def test_PythonFunctionArgument_asterik(name, kind, annotation):
PythonFunctionArgument(name, kind, None, annotation)
@pytest.mark.parametrize('argument,expected', [
(PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, None), 'a'),
(PythonFunctionArgument('a', 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'),
(PythonFunctionArgument('a', 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'),
(PythonFunctionArgument('a', PythonArgumentKind.KEYWORD_ONLY, '1', None), '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):
assert argument.signature_repr() == expected
@@ -0,0 +1,97 @@
# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from src.jcloud_docsgen.core.python.definitions import PythonFunctionDefinition, PythonClassDefinition
from src.jcloud_docsgen.core.python.arguments import PythonFunctionArgument, PythonArgumentKind
from src.jcloud_docsgen.core.python.docstrings import PythonDocstring
from src.jcloud_docsgen.exceptions import InvalidPythonIdentifierError
from tests.utils.ast_node import ast_node
import pytest
@pytest.mark.parametrize('definition,expected', [
(PythonClassDefinition('A', [], None, None, []), 'PythonClassDefinition(\'A\', [], None, None, [])'),
(PythonClassDefinition('A', [], None, PythonDocstring('Hello, World!'), []), 'PythonClassDefinition(\'A\', [], None, PythonDocstring(\'Hello, World!\'), [])'),
])
def test_PythonDefinition_string_representation(definition, expected):
assert repr(definition) == expected
@pytest.mark.parametrize('name,args,returns,doc,decorators,expected_exception,expected_exception_message', [
('', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier'),
('.', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: .'),
('#', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: #'),
('1a', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: 1a'),
('1_', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: 1_'),
('1,', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: 1,'),
(',', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: ,'),
(':', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: :'),
('-', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: -'),
(';', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: ;'),
('|', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: |'),
('<', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: <'),
('>', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: >'),
('a,', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: a,'),
(',a', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: ,a'),
('a:', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: a:'),
(':a', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: :a'),
('a-', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: a-'),
('-a', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: -a'),
('a;', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: a;'),
(';a', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: ;a'),
('a|', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: a|'),
('|a', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: |a'),
('a<', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: a<'),
('<a', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: <a'),
('a>', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: a>'),
('>a', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: >a'),
('class', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: class'),
('def', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: def'),
('lambda', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: lambda'),
('None', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: None'),
('True', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: True'),
])
def test_PythonFunctionDefinition_exceptions(name, args, returns, doc, decorators, expected_exception, expected_exception_message):
with pytest.raises(expected_exception) as exc_info:
PythonClassDefinition(name, args, returns, doc, decorators)
assert str(exc_info.value) == expected_exception_message
@pytest.mark.parametrize('node,expected', [
(ast_node('@decorator\nclass Class(BaseClass1, BaseClass2):\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'])
])),
(ast_node('class Class: ...'), PythonClassDefinition('Class', [], None, [], [])),
(ast_node('class Class: \'\'\'docstring\'\'\''), PythonClassDefinition('Class', [], PythonDocstring('docstring'), [], [])),
(ast_node('class Class:\n\tdef __init__(self): ...'), PythonClassDefinition('Class', [], None, [], [
PythonFunctionDefinition('__init__', [
PythonFunctionArgument('self', PythonArgumentKind.NORMAL, 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):
assert PythonClassDefinition.from_node(node) == expected
@@ -0,0 +1,84 @@
# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from src.jcloud_docsgen.core.python.definitions import PythonDefinition
from src.jcloud_docsgen.core.python.docstrings import PythonDocstring
from src.jcloud_docsgen.exceptions import InvalidPythonIdentifierError
import pytest
@pytest.mark.parametrize('definition,expected', [
(PythonDefinition('a', None, []), 'PythonDefinition(\'a\', None, [])'),
(PythonDefinition('a', PythonDocstring('Hello, World!'), []), 'PythonDefinition(\'a\', PythonDocstring(\'Hello, World!\'), [])'),
])
def test_PythonDefinition_string_representation(definition, expected):
assert repr(definition) == expected
@pytest.mark.parametrize('name,doc,decorators,expected_exception,expected_exception_message', [
('', None, [], InvalidPythonIdentifierError, 'invalid identifier'),
('.', None, [], InvalidPythonIdentifierError, 'invalid identifier: .'),
('#', None, [], InvalidPythonIdentifierError, 'invalid identifier: #'),
('1a', None, [], InvalidPythonIdentifierError, 'invalid identifier: 1a'),
('1_', None, [], InvalidPythonIdentifierError, 'invalid identifier: 1_'),
('1,', None, [], InvalidPythonIdentifierError, 'invalid identifier: 1,'),
(',', None, [], InvalidPythonIdentifierError, 'invalid identifier: ,'),
(':', None, [], InvalidPythonIdentifierError, 'invalid identifier: :'),
('-', None, [], InvalidPythonIdentifierError, 'invalid identifier: -'),
(';', None, [], InvalidPythonIdentifierError, 'invalid identifier: ;'),
('|', None, [], InvalidPythonIdentifierError, 'invalid identifier: |'),
('<', None, [], InvalidPythonIdentifierError, 'invalid identifier: <'),
('>', None, [], InvalidPythonIdentifierError, 'invalid identifier: >'),
('a,', None, [], InvalidPythonIdentifierError, 'invalid identifier: a,'),
(',a', None, [], InvalidPythonIdentifierError, 'invalid identifier: ,a'),
('a:', None, [], InvalidPythonIdentifierError, 'invalid identifier: a:'),
(':a', None, [], InvalidPythonIdentifierError, 'invalid identifier: :a'),
('a-', None, [], InvalidPythonIdentifierError, 'invalid identifier: a-'),
('-a', None, [], InvalidPythonIdentifierError, 'invalid identifier: -a'),
('a;', None, [], InvalidPythonIdentifierError, 'invalid identifier: a;'),
(';a', None, [], InvalidPythonIdentifierError, 'invalid identifier: ;a'),
('a|', None, [], InvalidPythonIdentifierError, 'invalid identifier: a|'),
('|a', None, [], InvalidPythonIdentifierError, 'invalid identifier: |a'),
('a<', None, [], InvalidPythonIdentifierError, 'invalid identifier: a<'),
('<a', None, [], InvalidPythonIdentifierError, 'invalid identifier: <a'),
('a>', None, [], InvalidPythonIdentifierError, 'invalid identifier: a>'),
('>a', None, [], InvalidPythonIdentifierError, 'invalid identifier: >a'),
('class', None, [], InvalidPythonIdentifierError, 'invalid identifier: class'),
('def', None, [], InvalidPythonIdentifierError, 'invalid identifier: def'),
('lambda', None, [], InvalidPythonIdentifierError, 'invalid identifier: lambda'),
('None', None, [], InvalidPythonIdentifierError, 'invalid identifier: None'),
('True', None, [], InvalidPythonIdentifierError, 'invalid identifier: True'),
])
def test_PythonDefinition_exceptions(name, doc, decorators, expected_exception, expected_exception_message):
with pytest.raises(expected_exception) as exc_info:
PythonDefinition(name, doc, decorators)
assert str(exc_info.value) == expected_exception_message
@pytest.mark.parametrize('definition1,definition2,expected', [
(PythonDefinition('a', None, []), PythonDefinition('a', None, []), True),
(PythonDefinition('a', PythonDocstring('doc'), []), PythonDefinition('a', PythonDocstring('doc'), []), True),
(PythonDefinition('a', PythonDocstring('doc'), ['a']), PythonDefinition('a', PythonDocstring('doc'), ['a']), True),
(PythonDefinition('a', PythonDocstring('doc'), ['a', 'b']), PythonDefinition('a', PythonDocstring('doc'), ['a', 'b']), True),
(PythonDefinition('a', None, []), PythonDefinition('b', None, []), False),
(PythonDefinition('a', PythonDocstring('doc'), []), PythonDefinition('a', None, []), 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):
assert (definition1 == definition2) == expected
assert (definition2 == definition1) == expected
@@ -0,0 +1,87 @@
# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from src.jcloud_docsgen.core.python.definitions import PythonFunctionDefinition
from src.jcloud_docsgen.core.python.arguments import PythonFunctionArgument, PythonArgumentKind
from src.jcloud_docsgen.core.python.docstrings import PythonDocstring
from src.jcloud_docsgen.exceptions import InvalidPythonIdentifierError
from tests.utils.ast_node import ast_node
import pytest
@pytest.mark.parametrize('definition,expected', [
(PythonFunctionDefinition('a', [], None, None, []), 'PythonFunctionDefinition(\'a\', [], None, None, [])'),
(PythonFunctionDefinition('a', [], None, PythonDocstring('Hello, World!'), []), 'PythonFunctionDefinition(\'a\', [], None, PythonDocstring(\'Hello, World!\'), [])'),
])
def test_PythonDefinition_string_representation(definition, expected):
assert repr(definition) == expected
@pytest.mark.parametrize('name,args,returns,doc,decorators,expected_exception,expected_exception_message', [
('', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier'),
('.', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: .'),
('#', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: #'),
('1a', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: 1a'),
('1_', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: 1_'),
('1,', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: 1,'),
(',', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: ,'),
(':', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: :'),
('-', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: -'),
(';', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: ;'),
('|', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: |'),
('<', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: <'),
('>', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: >'),
('a,', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: a,'),
(',a', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: ,a'),
('a:', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: a:'),
(':a', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: :a'),
('a-', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: a-'),
('-a', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: -a'),
('a;', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: a;'),
(';a', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: ;a'),
('a|', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: a|'),
('|a', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: |a'),
('a<', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: a<'),
('<a', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: <a'),
('a>', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: a>'),
('>a', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: >a'),
('class', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: class'),
('def', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: def'),
('lambda', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: lambda'),
('None', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: None'),
('True', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: True'),
])
def test_PythonFunctionDefinition_exceptions(name, args, returns, doc, decorators, expected_exception, expected_exception_message):
with pytest.raises(expected_exception) as exc_info:
PythonFunctionDefinition(name, args, returns, doc, decorators)
assert str(exc_info.value) == expected_exception_message
@pytest.mark.parametrize('node,expected', [
(ast_node('def 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, [])),
(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', [
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, 'int'),
PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, 'int'),
], 'int', None, ['decorator1', 'decorator2'])),
(ast_node('def func(): ...'), PythonFunctionDefinition('func', [], None, None, [])),
(ast_node('@decorator\ndef func(): ...'), PythonFunctionDefinition('func', [], None, None, ['decorator'])),
])
def test_PythonDefinition_from_node(node, expected):
assert PythonFunctionDefinition.from_node(node) == expected
@@ -0,0 +1,47 @@
# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from src.jcloud_docsgen.core.python.namespaces import PythonModuleNamespace
import pytest
class StrSubclass(str): ...
@pytest.mark.parametrize('namespace,expected_name', [
(PythonModuleNamespace('a'), 'a'),
(PythonModuleNamespace('1'), '1'),
(PythonModuleNamespace(StrSubclass('a')), StrSubclass('a')),
])
def test_PythonModuleNamespace_name_attribute(namespace, expected_name):
assert namespace.name == expected_name
@pytest.mark.parametrize('namespace1,namespace2,expected', [
(PythonModuleNamespace('a'), PythonModuleNamespace('a'), True),
(PythonModuleNamespace('b'), PythonModuleNamespace('b'), True),
(PythonModuleNamespace('1'), PythonModuleNamespace('1'), True),
(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):
assert (namespace1 == namespace2) == expected
assert (namespace2 == namespace1) == expected
@pytest.mark.parametrize('namespace,expected', [
(PythonModuleNamespace('a'), 'PythonModuleNamespace(\'a\')'),
(PythonModuleNamespace('1'), 'PythonModuleNamespace(\'1\')'),
(PythonModuleNamespace(StrSubclass('a')), 'PythonModuleNamespace(\'a\')'),
])
def test_PythonModuleNamespace_string_representation(namespace, expected):
assert repr(namespace) == expected
@@ -0,0 +1,34 @@
# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from src.jcloud_docsgen.core.python.namespaces import PythonNamespace
import pytest
class StrSubclass(str): ...
@pytest.mark.parametrize('namespace,expected_name', [
(PythonNamespace('a'), 'a'),
(PythonNamespace('1'), '1'),
(PythonNamespace(StrSubclass('a')), StrSubclass('a')),
])
def test_PythonNamespace_name_attribute(namespace, expected_name):
assert namespace.name == expected_name
@pytest.mark.parametrize('namespace,expected', [
(PythonNamespace('a'), 'PythonNamespace(\'a\')'),
(PythonNamespace('1'), 'PythonNamespace(\'1\')'),
(PythonNamespace(StrSubclass('a')), 'PythonNamespace(\'a\')'),
])
def test_PythonNamespace_string_representation(namespace, expected):
assert repr(namespace) == expected
@@ -0,0 +1,108 @@
# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from src.jcloud_docsgen.core.python.namespaces import PythonPackageNamespace, PythonModuleNamespace
import pytest
from src.jcloud_docsgen.exceptions import NamespaceNotFoundError, InvalidNamespaceError
class StrSubclass(str): ...
@pytest.mark.parametrize('namespace,expected_name', [
(PythonPackageNamespace('a', []), 'a'),
(PythonPackageNamespace('1', []), '1'),
(PythonPackageNamespace(StrSubclass('a'), []), StrSubclass('a')),
])
def test_PythonPackageNamespace_name_attribute(namespace, expected_name):
assert namespace.name == expected_name
namespace_b = PythonModuleNamespace('b')
namespace_a = PythonPackageNamespace('a', [
namespace_b
])
namespace_c = PythonModuleNamespace('c')
namespace_k = PythonModuleNamespace('k')
namespace_j = PythonPackageNamespace('j', [namespace_k])
namespace_i = PythonPackageNamespace('i', [namespace_j])
namespace_h = PythonPackageNamespace('h', [namespace_i])
namespace_g = PythonPackageNamespace('g', [namespace_h])
namespace_f = PythonPackageNamespace('f', [namespace_g])
namespace_e = PythonPackageNamespace('e', [namespace_f])
namespace_d = PythonPackageNamespace('d', [namespace_e])
@pytest.mark.parametrize('namespace,sub_namespaces,expected', [
(PythonPackageNamespace('name', [
namespace_a,
namespace_c
]), ['c'], namespace_c),
(PythonPackageNamespace('name', [
namespace_a,
namespace_c
]), ['a'], namespace_a),
(PythonPackageNamespace('name', [
namespace_a,
namespace_c
]), ['a', 'b'], namespace_b),
(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', 'k'], namespace_k),
])
def test_PythonPackageNamespace_namespace(namespace: PythonPackageNamespace, sub_namespaces, expected):
assert namespace.namespace(sub_namespaces) == expected
@pytest.mark.parametrize('namespace,sub_namespaces,expected_exception,expected_exception_msg', [
(namespace_a, ['c'], NamespaceNotFoundError, 'no such namespace: c'),
(namespace_h, ['i', 'k'], NamespaceNotFoundError, 'no such namespace: k'),
(namespace_a, ['b', ''], InvalidNamespaceError, 'invalid namespace'),
(namespace_a, ['', 'b'], InvalidNamespaceError, 'invalid namespace'),
(namespace_a, ['b', '', 'b'], InvalidNamespaceError, 'invalid namespace'),
])
def test_PythonPackageNamespace_namespace_exceptions(namespace, sub_namespaces, expected_exception, expected_exception_msg):
with pytest.raises(expected_exception) as exc_info:
namespace.namespace(sub_namespaces)
assert str(exc_info.value) == expected_exception_msg
@pytest.mark.parametrize('namespace,expected', [
(PythonPackageNamespace('a', [PythonModuleNamespace('b')]), {'b': 'b'}),
(PythonPackageNamespace('a', [PythonModuleNamespace('b'), PythonPackageNamespace('c', [PythonModuleNamespace('d')])]), {'b': 'b', 'c': {'d': 'd'}}),
(PythonPackageNamespace('a', []), {})
])
def test_PythonPackageNamespace_sub_namespace_names(namespace, expected):
assert namespace.sub_namespace_names() == expected
@pytest.mark.parametrize('namespace1,namespace2,expected', [
(PythonPackageNamespace('a', [PythonModuleNamespace('b')]), PythonPackageNamespace('a', [PythonModuleNamespace('b')]), True),
(PythonPackageNamespace('a', [PythonModuleNamespace('b'), PythonPackageNamespace('c', [PythonModuleNamespace('d')])]), PythonPackageNamespace('a', [PythonModuleNamespace('b'), PythonPackageNamespace('c', [PythonModuleNamespace('d')])]), True),
(PythonPackageNamespace('a', []), PythonPackageNamespace('a', []), True),
(PythonPackageNamespace('a', []), PythonPackageNamespace('b', []), False),
])
def test_PythonPackageNamespace___eq__(namespace1, namespace2, expected):
assert (namespace1 == namespace2) == expected
assert (namespace2 == namespace1) == expected
@pytest.mark.parametrize('namespace,expected', [
(PythonPackageNamespace('a', []), 'PythonPackageNamespace(\'a\', [])'),
(PythonPackageNamespace('1', []), 'PythonPackageNamespace(\'1\', [])'),
(PythonPackageNamespace(StrSubclass('a'), []), 'PythonPackageNamespace(\'a\', [])'),
(namespace_a, 'PythonPackageNamespace(\'a\', [PythonModuleNamespace(\'b\')])'),
(namespace_d, 'PythonPackageNamespace(\'d\', [PythonPackageNamespace(\'e\', [PythonPackageNamespace(\'f\', [PythonPackageNamespace(\'g\', [PythonPackageNamespace(\'h\', [PythonPackageNamespace(\'i\', [PythonPackageNamespace(\'j\', [PythonModuleNamespace(\'k\')])])])])])])])')
])
def test_PythonPackageNamespace_string_representation(namespace, expected):
assert repr(namespace) == expected
@pytest.mark.parametrize('namespace,new_namespace,expected', [
(PythonPackageNamespace('a', []), PythonModuleNamespace('b'), PythonPackageNamespace('a', [PythonModuleNamespace('b')]))
])
def test_PythonPackageNamespace___getitem__(namespace, new_namespace, expected):
namespace.add(new_namespace)
assert namespace == expected
@@ -0,0 +1,42 @@
# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from src.jcloud_docsgen.utils import ExistingDirectory
import pytest
import pathlib
@pytest.mark.parametrize('path,expected', [
('tests/unit/utils/test_directory/', 'tests/unit/utils/test_directory'),
('tests/unit/utils/test_directory', 'tests/unit/utils/test_directory'),
('tests/unit/utils/', 'tests/unit/utils'),
('tests/unit/utils', 'tests/unit/utils'),
('/', '/'),
('./', '.'),
('.', '.'),
('../', '..'),
('..', '..'),
])
def test_ExistingDirectory(path, expected):
assert ExistingDirectory(path).as_posix() == expected
@pytest.mark.parametrize('path,expected_exception,expected_exception_message', [
('tests/unit/utils/does_not_exist/', FileNotFoundError, '[Errno 2] no such file or directory: \'tests/unit/utils/does_not_exist/\''),
('tests/unit/utils/does_not_exist', FileNotFoundError, '[Errno 2] no such file or directory: \'tests/unit/utils/does_not_exist\''),
('', FileNotFoundError, '[Errno 2] no such file or directory: \'\''),
('README.md', NotADirectoryError, '[Errno 20] not a directory: \'README.md\''),
])
def test_ExistingDirectory_exceptions(path, expected_exception, expected_exception_message):
with pytest.raises(expected_exception) as exc_info:
ExistingDirectory(path)
assert str(exc_info.value) == expected_exception_message
+35
View File
@@ -0,0 +1,35 @@
# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from src.jcloud_docsgen.utils import ExistingFile
import pytest
import pathlib
@pytest.mark.parametrize('path,expected', [
('tests/unit/utils/test_file', 'tests/unit/utils/test_file'),
])
def test_ExistingDirectory(path, expected):
assert ExistingFile(path).as_posix() == expected
@pytest.mark.parametrize('path,expected_exception,expected_exception_message', [
('tests/unit/utils/does_not_exist', FileNotFoundError, '[Errno 2] no such file or directory: \'tests/unit/utils/does_not_exist\''),
('', FileNotFoundError, '[Errno 2] no such file or directory: \'\''),
('tests/unit/utils/test_directory', IsADirectoryError, '[Errno 21] is a directory: \'tests/unit/utils/test_directory\''),
('.', IsADirectoryError, '[Errno 21] is a directory: \'.\''),
('./', IsADirectoryError, '[Errno 21] is a directory: \'./\''),
])
def test_ExistingDirectory_exceptions(path, expected_exception, expected_exception_message):
with pytest.raises(expected_exception) as exc_info:
ExistingFile(path)
assert str(exc_info.value) == expected_exception_message
@@ -0,0 +1,43 @@
# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from src.jcloud_docsgen.utils import assert_that_is_instance
import pytest
import types
class TestType: ...
TestType.__name__ = 'NOT TestType'
@pytest.mark.parametrize('obj,class_or_tuple', [
(1, int),
(1, (int,)),
(1, str | int),
(1, int | str | list),
(TestType(), TestType),
(None, types.NoneType),
])
def test_assert_that_is_instance(obj, class_or_tuple):
assert_that_is_instance(obj, class_or_tuple)
@pytest.mark.parametrize('obj,class_or_tuple,expected_exception_msg', [
(1, str, 'expected \'str\', got \'int\''),
(1, (str,), 'expected \'str\', got \'int\''),
(1, str | float, 'expected either \'str\' or \'float\', got \'int\''),
(1, str | float | list, 'expected either \'str\', \'float\' or \'list\', got \'int\''),
(1, TestType, 'expected \'NOT TestType\', got \'int\''),
])
def test_assert_that_is_instance_exceptions(obj, class_or_tuple, expected_exception_msg):
with pytest.raises(TypeError) as exc_info:
assert_that_is_instance(obj, class_or_tuple)
assert str(exc_info.value) == expected_exception_msg
View File
@@ -0,0 +1,76 @@
# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from src.jcloud_docsgen.utils import get_relative_path_segments
import pytest
import pathlib
@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):
assert get_relative_path_segments(path, root) == expected
@@ -0,0 +1,54 @@
# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from src.jcloud_docsgen.utils import human_readable_list
import pytest
@pytest.mark.parametrize('ls,final_separator,quotation_mark,expected', [
([], '', '', ''),
([], 'and', '', ''),
([], 'and', '\'', ''),
([1], '', '', '1'),
([-1], '', '', '-1'),
([0], '', '', '0'),
([0.0], '', '', '0.0'),
([-0.0], '', '', '-0.0'),
([4.2], '', '', '4.2'),
([-4.2], '', '', '-4.2'),
(['a'], '', '', 'a'),
([1], 'and', '', '1'),
(['a'], 'and', '', 'a'),
([1], 'and', '\'', '\'1\''),
(['a'], 'and', '\'', '\'a\''),
([1, 2], 'and', '', '1 and 2'),
([1, 2], 'and', '\'', '\'1\' and \'2\''),
([1, 'a'], 'and', '', '1 and a'),
([1, 'a'], 'and', '\'', '\'1\' and \'a\''),
([1, 'a'], 'and', '', '1 and a'),
([1, 'a'], 'and', '\'', '\'1\' and \'a\''),
([1, 'a'], 'and', '\'', '\'1\' and \'a\''),
([1, 'a'], '', '', '1 a'),
([1, 'a'], '', '\'', '\'1\' \'a\''),
([1, 'a'], '', '', '1 a'),
([1, 'a'], '', '\'', '\'1\' \'a\''),
([1, 'a'], '', '\'', '\'1\' \'a\''),
([1, 2, 3], 'and', '', '1, 2 and 3'),
([1, 2, 3], '', '', '1, 2 3'),
([1, 2, 3], 'and', '\'', '\'1\', \'2\' and \'3\''),
([1, 2, 3], '', '\'', '\'1\', \'2\' \'3\''),
([1, 2, 3, 42, 'a', 23], 'and', '', '1, 2, 3, 42, a and 23'),
([1, 2, 3, 42, 'a', 23], 'and', '\'', '\'1\', \'2\', \'3\', \'42\', \'a\' and \'23\''),
])
def test_human_readable_list(ls, final_separator, quotation_mark, expected):
assert human_readable_list(ls, final_separator, quotation_mark) == expected
+30
View File
@@ -0,0 +1,30 @@
# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from src.jcloud_docsgen.utils import non_empty_str
import pytest
@pytest.mark.parametrize('value,expected', [
(None, None),
('', None),
(' ', None),
(' ', None),
(' ', None),
(' a ', ' a '),
(' a', ' a'),
('a ', 'a '),
('a', 'a'),
])
def test_non_empty_str(value, expected):
assert non_empty_str(value) == expected
+36
View File
@@ -0,0 +1,36 @@
# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Union
import ast
__all__ = [
'ast_node'
]
def ast_node(statement: str) -> Union[ast.stmt, None]:
'''
Returns an AST node from the statement.
:param statement: The statement.
:type statement: str
:return: The node.
:rtype: Union[ast.stmt, None]
'''
body = ast.parse(statement).body
if not body:
return None
return body[0]