31 Commits
Author SHA1 Message Date
jakob.scheid 347bff5dac Merge pull request 'Chore: prepare 0.2.3' (#75) from chore/prepare-0-2-3 into main
Generate API reference / generate (push) Successful in 21s
Release package / Build package (release) Successful in 23s
Release package / Prepare release asset upload (release) Successful in 14s
Release package / Publish package to package registry (release) Successful in 15s
Release package / Upload release asset (dist/jcloud_docsgen-0.2.3.tar.gz) (release) Successful in 17s
Release package / Upload release asset (dist/jcloud_docsgen-0.2.3-py3-none-any.whl) (release) Successful in 18s
Reviewed-on: #75
2026-10-21 13:28:05 +02:00
jakob.scheid 7ff849387a docs(changelog): Move Unreleased section to 0.2.3 2026-10-21 13:26:24 +02:00
jakob.scheid 2b31b82a06 chore: update project version to 0.2.3 2026-10-21 13:21:54 +02:00
jcloud-bot Bot 92dc2fcd47 chore: update API reference 2026-10-21 11:14:39 +00:00
jakob.scheid b1727c100f Merge pull request 'Refactor(utils): move general utility functions to utility module' (#73) from refactor/restructure-general-utility-functions into main
Generate API reference / generate (push) Successful in 22s
Reviewed-on: #73
2026-10-21 13:14:27 +02:00
jakob.scheid 6ddbd17497 refactor(utils)!: move markdown_table to utility module
Moved the markdown_table utility function from the Python documentation
generator core module to the utility module. The public exports
definition of the utility module and all imports were updated.

The test module was also moved to the appropriate new path and the
testing functions were renamed.
2026-10-21 13:02:36 +02:00
jakob.scheid 36bc14185a refactor(utils)!: move fit_row_length to utility module
Moved the _fit_row_length function from the Python generator core module
to fit_row_length in the utility module. The public exports definition
of the utility module and all imports were updated.

The test module was also moved to the appropriate new path and the
testing functions were renamed.
2026-10-21 13:01:28 +02:00
jakob.scheid 39d3b0ace4 Merge pull request 'Fix(markdown): escape sequences for pipe characters in table cells' (#71) from fix/markdown-table-cells-pipe into main
Generate API reference / generate (push) Successful in 22s
Reviewed-on: #71
2026-10-21 12:33:17 +02:00
jakob.scheid 3f74e6457e docs(changelog): add Markdown escape sequences entry
Added a changelog entry for the escape sequences in Markdown table
cells.
2026-10-21 12:22:52 +02:00
jakob.scheid a6b5e8f1c9 fix(markdown): escape pipe characters in table cells
Pipe characters ('|') in Markdown table cells are now replaced with an
escape sequence ('\|') to ensure that cells are not split by Markdown
renderers.

Tests which expected pipe characters to be unescaped, which is
incorrect, were updatedto expect an escape sequence and new test cases
to ensure the correct escaping behavior were added.
2026-10-21 12:15:53 +02:00
jakob.scheid a85cd1b433 Merge pull request 'Chore: prepare 0.2.2' (#69) from chore/prepare-0-2-2 into main
Generate API reference / generate (push) Successful in 21s
Release package / Build package (release) Successful in 24s
Release package / Prepare release asset upload (release) Successful in 13s
Release package / Publish package to package registry (release) Successful in 15s
Release package / Upload release asset (dist/jcloud_docsgen-0.2.2-py3-none-any.whl) (release) Successful in 25s
Release package / Upload release asset (dist/jcloud_docsgen-0.2.2.tar.gz) (release) Successful in 16s
Reviewed-on: #69
2026-10-14 23:59:40 +02:00
jakob.scheid 06f91a18e9 docs(changelog): move Unreleased section to 0.2.2 2026-10-14 23:58:11 +02:00
jakob.scheid bc542ed781 chore: update project version in pyproject.toml 2026-10-14 23:57:37 +02:00
jakob.scheid aa44f9c3a1 Merge pull request 'Fix(deps): add typing-extensions dependency' (#67) from fix/typing-extensions-import-error into main
Generate API reference / generate (push) Successful in 22s
Reviewed-on: #67
2026-10-14 23:54:31 +02:00
jakob.scheid 0f4c344676 docs(changelog): add import error fix entry
Added a changelog entry for the fix of the typing-extensions import
error in Python <3.13.
2026-10-14 23:50:47 +02:00
jakob.scheid ae01f223e3 fix(deps): add typing-extensions
Added typing-extensions to fix import error in Python <3.13.
2026-10-14 23:49:36 +02:00
jakob.scheid 4781e21f5d Merge pull request 'Docs(changelog): update 0.2.1 release date to today' (#65) from chore/update-0-2-1-release-date into main
Generate API reference / generate (push) Successful in 21s
Release package / Build package (release) Successful in 25s
Release package / Prepare release asset upload (release) Successful in 13s
Release package / Publish package to package registry (release) Successful in 15s
Release package / Upload release asset (dist/jcloud_docsgen-0.2.1-py3-none-any.whl) (release) Successful in 17s
Release package / Upload release asset (dist/jcloud_docsgen-0.2.1.tar.gz) (release) Successful in 18s
Reviewed-on: #65
2026-10-06 17:52:01 +02:00
jakob.scheid 0d86785dba docs(changelog): update 0.2.1 release date
Updated the release date of version 0.2.1 to 2026-10-06.
2026-10-06 17:49:59 +02:00
jakob.scheid 5402dd960e Merge pull request 'Chore: preapre release 0.2.1' (#63) from chore/prepare-0-2-1 into main
Generate API reference / generate (push) Successful in 20s
Reviewed-on: #63
2026-10-04 17:26:58 +02:00
jakob.scheid d8249d3eb1 docs(changelog): move Unreleased section to 0.2.1 2026-10-04 17:25:18 +02:00
jakob.scheid 6c593a286c chore: update version information to 0.2.1 2026-10-04 17:24:48 +02:00
jcloud-bot Bot 4b659d7d2d chore: update API reference 2026-10-04 15:19:27 +00:00
jakob.scheid 1ccb610310 Merge pull request 'Fix(python-generator): ignore invalid namespaces' (#61) from fix/ignore-invalid-python-namespaces into main
Generate API reference / generate (push) Successful in 22s
Reviewed-on: #61
2026-10-04 17:19:13 +02:00
jakob.scheid a25c66d77b docs(changelog): add entry for stricter Python namespace name validation 2026-10-04 17:06:44 +02:00
jakob.scheid 82e10c5493 docs(changelog): add Python invalid namespaces ignoring entry 2026-10-04 17:04:04 +02:00
jakob.scheid 03fa81f0df fix(python-generator): ignore invalid namespaces
Python modules or packages are now ignored if their name is not a valid
Python namespace name.
2026-10-04 16:54:48 +02:00
jakob.scheid efa2dbf724 test(python-namespaces): add test for namespace name validation 2026-10-04 16:47:33 +02:00
jakob.scheid 0f8f9d3cfa feat(python-namespaces)!: add validation for namespace names
Added more strict validation for namespaces names to PythonNamespace
than before and updated error messages.
All tests depending on this were updated to the new behavior.
2026-10-04 16:39:31 +02:00
jakob.scheid 3e77ebb7d0 Merge pull request 'Refactor: define public module exports' (#60) from refactor/modules-all-variables into main
Generate API reference / generate (push) Successful in 30s
Reviewed-on: #60
2026-10-03 18:50:32 +02:00
jakob.scheid 310861dacc refactor(exceptions): define public module exports
Added an __all__ variable to the exceptions module.
2026-10-03 18:50:04 +02:00
jakob.scheid 4365de27a8 refactor(utils): define public module exports
Added an __all__ variable to the utility module.
2026-10-03 18:49:47 +02:00
45 changed files with 543 additions and 366 deletions

No files matched your search

+18 -1
View File
@@ -1,11 +1,28 @@
# Changelog
## [Unreleased]
## [0.2.3] - 2026-10-21
### Fixed
- Escape sequences for pipe characters in values of Markdown table cells
## [0.2.2] - 2026-10-14
### Fixed
- Import error in Python <3.13 which prevented the tool from running and made it unusuable in Python <3.13
## [0.2.1] - 2026-10-06
### Fixed
- Python namespace name conflict when package and module have the same name
- Internal namespace name error which occured when generating Python documentation with `.` as the project directory
- Ignore Python namespaces with invalid names during documentation generation
### Changed
- Stricter Python namespace name validation
## [0.2.0] - 2026-09-28
@@ -116,106 +116,6 @@ Returns a list of the arguments as strings for a function signature.
:return: The list of function signature argument strings.
:rtype: list[str]
## Function `_fit_row_length`
Fits the row length to the expected length.
If the row is too long, the tail, that makes it too long, is omitted.
If it is too short, it is filled with ``None`` (at the end).
### Function signature
```python
_fit_row_length(expected: int, row: tuple) -> tuple
```
### Returns
Return type: `tuple`
### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `expected` | `int` | normal | -- | The expected length. |
| `row` | `tuple` | normal | -- | The row. |
### Docstring
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
## Function `_markdown_table`
Generates a table.
If ``allow_markdown_table`` is ``False``, an alternative
representation is used.
### Function signature
```python
_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
```
### Returns
Return type: `str`
### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `header` | `tuple` | normal | -- | -- |
| `body` | `list[tuple]` | normal | -- | The table body. |
| `allow_markdown_table` | `bool` | keyword-only | `False` | Controls whether a markdown table may be used. |
| `force_row_lengths` | `bool` | keyword-only | `False` | Controls whether an exception is raised if a row does not have the correct length. |
| `one_line_values` | `bool` | keyword-only | `True` | Controls whether all newlines in all values are replaced with spaces. |
| `missing_value` | `str` | keyword-only | `'--'` | The string that is used if a value is missing (only if ``force_row_lengths`` is ``False``). |
### Docstring
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
## Function `_is_docstring`
Checks whether the docstring is a docstring or an empty docstring.
@@ -1,5 +1,45 @@
# Module `jcloud_docsgen.core.python.namespaces`
## Function `validate_namespace_name`
Checks whether a namespace name is valid. Namespace names are valid
if they contain no dots and are valid Python identifiers.
### Function signature
```python
validate_namespace_name(name: str) -> bool
```
### Returns
Whether the namespace name is valid.
Return type: `bool`
### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `name` | `str` | normal | -- | The namespace name to validate. |
### Docstring
Checks whether a namespace name is valid. Namespace names are valid
if they contain no dots and are valid Python identifiers.
:param name: The namespace name to validate.
:type name: str
:returns: Whether the namespace name is valid.
:rtype: bool
## Class `PythonNamespace`
The base class for all Python namespaces such as modules or packages.
@@ -57,6 +97,8 @@ The base class for all Python namespaces such as modules or packages.
:param name: The name of the namespace.
:type name: str
:raises InvalidNamespaceError: If the namespace name is invalid.
## Class `PythonPackageNamespace`
A subclass of ``PythonNamespace`` for Python package namespaces.
@@ -381,6 +423,8 @@ A subclass of ``PythonNamespace`` for Python package namespaces.
:param sub_namespaces: The sub-namespaces.
:type sub_namespaces: list[PythonNamespace]
:raises InvalidNamespaceError: If the namespace name is invalid.
## Class `PythonModuleNamespace`
A subclass of ``PythonNamespace`` for Python module namespaces.
+101 -1
View File
@@ -340,4 +340,104 @@ Returns the path segments relative to ``root``.
:type root: pathlib.Path
:return: The path segments relative to ``root``.
:rtype: list[str]
:rtype: list[str]
## Function `fit_row_length`
Fits the row length to the expected length.
If the row is too long, the tail, that makes it too long, is omitted.
If it is too short, it is filled with ``None`` (at the end).
### Function signature
```python
fit_row_length(expected: int, row: tuple) -> tuple
```
### Returns
Return type: `tuple`
### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `expected` | `int` | normal | -- | The expected length. |
| `row` | `tuple` | normal | -- | The row. |
### Docstring
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
## Function `markdown_table`
Generates a table.
If ``allow_markdown_table`` is ``False``, an alternative
representation is used.
### Function signature
```python
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
```
### Returns
Return type: `str`
### Parameters
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `header` | `tuple` | normal | -- | -- |
| `body` | `list[tuple]` | normal | -- | The table body. |
| `allow_markdown_table` | `bool` | keyword-only | `False` | Controls whether a markdown table may be used. |
| `force_row_lengths` | `bool` | keyword-only | `False` | Controls whether an exception is raised if a row does not have the correct length. |
| `one_line_values` | `bool` | keyword-only | `True` | Controls whether all newlines in all values are replaced with spaces. |
| `missing_value` | `str` | keyword-only | `'--'` | The string that is used if a value is missing (only if ``force_row_lengths`` is ``False``). |
### Docstring
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
Generated
+4 -4
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.4.3 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.5.1 and should not be changed by hand.
[[package]]
name = "colorama"
@@ -235,14 +235,14 @@ version = "4.16.0"
description = "Backported and Experimental Type Hints for Python 3.9+"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
markers = "python_version == \"3.10\""
groups = ["main", "dev"]
files = [
{file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"},
{file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"},
]
markers = {dev = "python_version == \"3.10\""}
[metadata]
lock-version = "2.1"
python-versions = ">=3.10"
content-hash = "24c9a2d464f6c737eeae0a66f792fa23824c3960b7cc72a22fff1408f21ec812"
content-hash = "0fb3b53540e7a7fe173bf29a2031bd59f863a8dea94a29d51933de2375c3bfa2"
+2 -2
View File
@@ -4,9 +4,9 @@ build-backend = "poetry.core.masonry.api"
[project]
name = "jcloud-docsgen"
version = "0.2.0"
version = "0.2.3"
description = "A tool that generates Markdown documentation for software project source code"
dependencies = ["docstring-parser"]
dependencies = ["docstring-parser", "typing-extensions (>=4.16.0,<5.0.0)"]
requires-python = ">=3.10"
license = "Apache-2.0"
+25 -134
View File
@@ -28,7 +28,7 @@ from ...exceptions import (
InvalidNamespaceError,
PythonArgumentStructureError,
)
from ...utils import get_relative_path_segments, non_empty_str
from ...utils import get_relative_path_segments, markdown_table, non_empty_str
from .arguments import PythonArgumentKind, PythonFunctionArgument
from .definitions import (
PythonAsyncFunctionDefinition,
@@ -37,7 +37,11 @@ from .definitions import (
PythonFunctionDefinition,
)
from .docstrings import PythonDocstring
from .namespaces import PythonModuleNamespace, PythonPackageNamespace
from .namespaces import (
PythonModuleNamespace,
PythonPackageNamespace,
validate_namespace_name,
)
__all__ = [
'PythonDefinitionDocumentationIncludeSections',
@@ -180,128 +184,6 @@ def _signature_argument_list(
return args
def _fit_row_length(expected: int, row: tuple) -> tuple:
"""
Fits the row length to the expected length.
If the row is too long, the tail, that makes it too long, is omitted.
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'
f'{"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'
f'{"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: str | PythonDocstring | None) -> bool:
"""
Checks whether the docstring is a docstring or an empty docstring.
@@ -637,7 +519,7 @@ class PythonDefinitionDocumentationGenerator:
)
if params_types:
md += _markdown_table(
md += markdown_table(
['Parameter', 'Type', 'Kind', 'Default value', 'Description'],
[
(
@@ -716,7 +598,7 @@ class PythonDefinitionDocumentationGenerator:
md += self.level * '#' + '# Exceptions\n\n'
if self._docstring.raises:
md += _markdown_table(
md += markdown_table(
('Exception', 'Description'),
[
(
@@ -926,9 +808,12 @@ class PythonModuleDocumentationGenerator:
),
**kwargs,
) -> None:
if module_path.name.count('.') > 1:
if not (
module_path.suffix == '.py'
and validate_namespace_name(module_path.stem)
):
raise InvalidNamespaceError(
'namespace identifier cannot contain more than one dot',
'invalid module file name',
namespace_identifier=module_path.name,
)
@@ -1107,12 +992,18 @@ class PythonDocumentationGenerator:
)
for entry in directory.iterdir():
if entry.is_dir() and (
entry.parent / (f'{entry.stem}.py')
).resolve() not in [e.resolve() for e in entry.parent.iterdir()]:
namespace.add(self._namespace(entry))
elif entry.suffix == '.py':
namespace.add(PythonModuleNamespace(entry.stem))
try:
if entry.is_dir() and (
entry.parent / (f'{entry.stem}.py')
).resolve() not in [
e.resolve() for e in entry.parent.iterdir()
]:
namespace.add(self._namespace(entry))
elif entry.suffix == '.py':
namespace.add(PythonModuleNamespace(entry.stem))
except InvalidNamespaceError:
# the name of the directory or module is invalid; ignore.
pass
return namespace
+26 -3
View File
@@ -14,6 +14,7 @@
from __future__ import annotations
import keyword
from typing import TypeAlias, Union
from ...exceptions import (
@@ -24,27 +25,47 @@ from ...exceptions import (
from ...utils import assert_that_is_instance
__all__ = [
'validate_namespace_name',
'PythonNamespace',
'PythonPackageNamespace',
'PythonModuleNamespace',
]
def validate_namespace_name(name: str) -> bool:
"""
Checks whether a namespace name is valid. Namespace names are valid
if they contain no dots and are valid Python identifiers.
:param name: The namespace name to validate.
:type name: str
:returns: Whether the namespace name is valid.
:rtype: bool
"""
if not isinstance(name, str):
return False
return name.isidentifier() and not keyword.iskeyword(name)
class PythonNamespace:
"""
The base class for all Python namespaces such as modules or packages.
:param name: The name of the namespace.
:type name: str
:raises InvalidNamespaceError: If the namespace name is invalid.
"""
def __init__(self, name: str) -> None:
assert_that_is_instance(name, str)
if not name.strip():
if not validate_namespace_name(name):
raise InvalidNamespaceError(
'invalid namespace',
namespace_identifier=name,
'invalid namespace', namespace_identifier=name
)
self.name = name
@@ -70,6 +91,8 @@ class PythonPackageNamespace(PythonNamespace):
:type name: str
:param sub_namespaces: The sub-namespaces.
:type sub_namespaces: list[PythonNamespace]
:raises InvalidNamespaceError: If the namespace name is invalid.
"""
def __init__(
-1
View File
@@ -14,7 +14,6 @@
import errno
__all__ = [
'NamespaceError',
'InvalidNamespaceError',
+126
View File
@@ -34,6 +34,8 @@ __all__ = [
'assert_that_is_instance',
'non_empty_str',
'get_relative_path_segments',
'fit_row_length',
'markdown_table',
]
@@ -237,3 +239,127 @@ def get_relative_path_segments(
)
.parts,
)
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'
f'{"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', ' ')
.replace('|', '\\|')
if one_line_values
else str(
col if col is not None else missing_value
).replace('|', '\\|')
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'
f'{"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()
@@ -0,0 +1,9 @@
# Module `pkg.a.b`
## Function `fn`
### Function signature
```python
fn()
```
@@ -47,7 +47,7 @@ Return type: `User`
| `user_id` | `str` | normal | -- | -- |
| `name` | `str` | normal | -- | -- |
| `email` | `str` | normal | -- | -- |
| `age` | `int | None` | normal | `None` | -- |
| `age` | `int \| None` | normal | `None` | -- |
@@ -25,176 +25,134 @@ from src.jcloud_docsgen.exceptions import InvalidNamespaceError
[
(
pathlib.Path(
(
'tests/unit/core/python/_core/fil'
'es_with_more_than_one_dot/a.b.c'
),
('tests/unit/core/python/_core/invalid_module_names/a.b.c'),
),
None,
InvalidNamespaceError,
'namespace identifier cannot contain more than one dot: a.b.c',
'invalid module file name: a.b.c',
),
(
pathlib.Path(
('tests/unit/core/python/_core/invalid_module_names/a.b.c.d'),
),
None,
InvalidNamespaceError,
'invalid module file name: a.b.c.d',
),
(
pathlib.Path(
(
'tests/unit/core/python/_core/file'
's_with_more_than_one_dot/a.b.c.d'
'tests/unit/core/python/_core/i'
'nvalid_module_names/a.b.c.d.e'
),
),
None,
InvalidNamespaceError,
'namespace identifier cannot contain more than one dot: a.b.c.d',
'invalid module file name: a.b.c.d.e',
),
(
pathlib.Path(
(
'tests/unit/core/python/_core/files'
'_with_more_than_one_dot/a.b.c.d.e'
'tests/unit/core/python/_core/in'
'valid_module_names/a.b.c.d.e.f'
),
),
None,
InvalidNamespaceError,
(
'namespace identifier cannot cont'
'ain more than one dot: a.b.c.d.e'
),
'invalid module file name: a.b.c.d.e.f',
),
(
pathlib.Path(
(
'tests/unit/core/python/_core/files_'
'with_more_than_one_dot/a.b.c.d.e.f'
'tests/unit/core/python/_core/inv'
'alid_module_names/a.b.c.d.e.f.g'
),
),
None,
InvalidNamespaceError,
(
'namespace identifier cannot conta'
'in more than one dot: a.b.c.d.e.f'
),
'invalid module file name: a.b.c.d.e.f.g',
),
(
pathlib.Path(
(
'tests/unit/core/python/_core/files_w'
'ith_more_than_one_dot/a.b.c.d.e.f.g'
'tests/unit/core/python/_core/inva'
'lid_module_names/a.b.c.d.e.f.g.h'
),
),
None,
InvalidNamespaceError,
(
'namespace identifier cannot contai'
'n more than one dot: a.b.c.d.e.f.g'
),
'invalid module file name: a.b.c.d.e.f.g.h',
),
(
pathlib.Path(
(
'tests/unit/core/python/_core/files_wi'
'th_more_than_one_dot/a.b.c.d.e.f.g.h'
'tests/unit/core/python/_core/inval'
'id_module_names/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'
),
'invalid module file name: a.b.c.d.e.f.g.h.py',
),
(
pathlib.Path(
(
'tests/unit/core/python/_core/files_wit'
'h_more_than_one_dot/a.b.c.d.e.f.g.h.py'
'tests/unit/core/python/_core/inva'
'lid_module_names/a.b.c.d.e.f.g.py'
),
),
None,
InvalidNamespaceError,
(
'namespace identifier cannot contain m'
'ore than one dot: a.b.c.d.e.f.g.h.py'
),
'invalid module file name: a.b.c.d.e.f.g.py',
),
(
pathlib.Path(
(
'tests/unit/core/python/_core/files_wi'
'th_more_than_one_dot/a.b.c.d.e.f.g.py'
'tests/unit/core/python/_core/in'
'lid_module_names/a.b.c.d.e.f.py'
),
),
None,
InvalidNamespaceError,
(
'namespace identifier cannot contain '
'more than one dot: a.b.c.d.e.f.g.py'
),
'invalid module file name: a.b.c.d.e.f.py',
),
(
pathlib.Path(
(
'tests/unit/core/python/_core/files_w'
'ith_more_than_one_dot/a.b.c.d.e.f.py'
'tests/unit/core/python/_core/in'
'valid_module_names/a.b.c.d.e.py'
),
),
None,
InvalidNamespaceError,
(
'namespace identifier cannot contain'
' more than one dot: a.b.c.d.e.f.py'
),
'invalid module file name: a.b.c.d.e.py',
),
(
pathlib.Path(
(
'tests/unit/core/python/_core/files_'
'with_more_than_one_dot/a.b.c.d.e.py'
'tests/unit/core/python/_core/i'
'nvalid_module_names/a.b.c.d.py'
),
),
None,
InvalidNamespaceError,
(
'namespace identifier cannot contai'
'n more than one dot: a.b.c.d.e.py'
),
'invalid module file name: a.b.c.d.py',
),
(
pathlib.Path(
(
'tests/unit/core/python/_core/files'
'_with_more_than_one_dot/a.b.c.d.py'
),
('tests/unit/core/python/_core/invalid_module_names/a.b.c.py'),
),
None,
InvalidNamespaceError,
(
'namespace identifier cannot conta'
'in more than one dot: a.b.c.d.py'
),
'invalid module file name: a.b.c.py',
),
(
pathlib.Path(
(
'tests/unit/core/python/_core/file'
's_with_more_than_one_dot/a.b.c.py'
),
('tests/unit/core/python/_core/invalid_module_names/a.b.py'),
),
None,
InvalidNamespaceError,
(
'namespace identifier cannot cont'
'ain more than one dot: a.b.c.py'
),
),
(
pathlib.Path(
(
'tests/unit/core/python/_core/fil'
'es_with_more_than_one_dot/a.b.py'
),
),
None,
InvalidNamespaceError,
'namespace identifier cannot contain more than one dot: a.b.py',
'invalid module file name: a.b.py',
),
(
pathlib.Path(
@@ -205,7 +163,7 @@ from src.jcloud_docsgen.exceptions import InvalidNamespaceError
),
None,
InvalidNamespaceError,
'namespace identifier cannot contain more than one dot: a.b.py',
'invalid module file name: a.b.py',
),
],
)
@@ -292,8 +250,8 @@ Return type: `int | float`
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `a` | `int | float` | normal | -- | The first number. |
| `b` | `int | float` | normal | -- | The second number. |
| `a` | `int \\| float` | normal | -- | The first number. |
| `b` | `int \\| float` | normal | -- | The second number. |
@@ -503,8 +461,8 @@ Return type: `int | float`
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `a` | `int | float` | normal | -- | The first number. |
| `b` | `int | float` | normal | -- | The second number. |
| `a` | `int \\| float` | normal | -- | The first number. |
| `b` | `int \\| float` | normal | -- | The second number. |
@@ -714,8 +672,8 @@ Return type: `int | float`
| Parameter | Type | Kind | Default value | Description |
| --- | --- | --- | --- | --- |
| `a` | `int | float` | normal | -- | The first number. |
| `b` | `int | float` | normal | -- | The second number. |
| `a` | `int \\| float` | normal | -- | The first number. |
| `b` | `int \\| float` | normal | -- | The second number. |
@@ -28,7 +28,6 @@ class StrSubclass(str): ...
'namespace,expected_name',
[
(PythonModuleNamespace('a'), 'a'),
(PythonModuleNamespace('1'), '1'),
(PythonModuleNamespace(StrSubclass('a')), StrSubclass('a')),
],
)
@@ -41,11 +40,7 @@ def test_PythonModuleNamespace_name_attribute(namespace, expected_name):
[
(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),
(PythonModuleNamespace('a'), 'a', False),
(PythonModuleNamespace('a'), 'b', False),
(PythonModuleNamespace('a'), PythonPackageNamespace('a', []), False),
@@ -61,7 +56,6 @@ def test_PythonModuleNamespace___eq__(namespace1, namespace2, expected):
'namespace,expected',
[
(PythonModuleNamespace('a'), "PythonModuleNamespace('a')"),
(PythonModuleNamespace('1'), "PythonModuleNamespace('1')"),
(
PythonModuleNamespace(StrSubclass('a')),
"PythonModuleNamespace('a')",
@@ -24,7 +24,6 @@ class StrSubclass(str): ...
'namespace,expected_name',
[
(PythonNamespace('a'), 'a'),
(PythonNamespace('1'), '1'),
(PythonNamespace(StrSubclass('a')), StrSubclass('a')),
],
)
@@ -36,7 +35,6 @@ def test_PythonNamespace_name_attribute(namespace, expected_name):
'namespace,expected',
[
(PythonNamespace('a'), "PythonNamespace('a')"),
(PythonNamespace('1'), "PythonNamespace('1')"),
(PythonNamespace(StrSubclass('a')), "PythonNamespace('a')"),
],
)
@@ -38,13 +38,6 @@ class StrSubclass(str): ...
),
'a',
),
(
PythonPackageNamespace(
'1',
[],
),
'1',
),
(
PythonPackageNamespace(
StrSubclass('a'),
@@ -291,10 +284,6 @@ def test_PythonPackageNamespace___eq__(namespace1, namespace2, expected):
PythonPackageNamespace('a', []),
"PythonPackageNamespace('a', [])",
),
(
PythonPackageNamespace('1', []),
"PythonPackageNamespace('1', [])",
),
(
PythonPackageNamespace(StrSubclass('a'), []),
"PythonPackageNamespace('a', [])",
@@ -0,0 +1,52 @@
# 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.
import pytest
from src.jcloud_docsgen.core.python.namespaces import validate_namespace_name
@pytest.mark.parametrize(
'name,expected',
[
('a', True),
('namespace', True),
('match', True),
('case', True),
('type', True),
('_', True),
('__', True),
('___', True),
('_a', True),
('_a_', True),
('_a.', False),
('a.b', False),
('a-b', False),
('a.', False),
('a-', False),
('.a', False),
('-a', False),
('.a.', False),
('-a-', False),
('.', False),
('a-.b', False),
('"', False),
('""', False),
('def', False),
('class', False),
('import', False),
],
)
def test_validate_namespace_name(name, expected):
assert validate_namespace_name(name) == expected
@@ -14,7 +14,7 @@
import pytest
from src.jcloud_docsgen.core.python._core import _fit_row_length
from src.jcloud_docsgen.utils import fit_row_length
@pytest.mark.parametrize(
@@ -37,8 +37,8 @@ from src.jcloud_docsgen.core.python._core import _fit_row_length
(3, (1, 2, 3), (1, 2, 3)),
],
)
def test__fit_row_length(expected_length, row, expected):
assert _fit_row_length(expected_length, row) == expected
def test_fit_row_length(expected_length, row, expected):
assert fit_row_length(expected_length, row) == expected
@pytest.mark.parametrize(
@@ -60,7 +60,7 @@ def test__fit_row_length(expected_length, row, expected):
(-42, ValueError),
],
)
def test__fit_row_length_exceptions(expected_length, row, expected_exception):
def test_fit_row_length_exceptions(expected_length, row, expected_exception):
with pytest.raises(expected_exception) as exc_info:
_fit_row_length(expected_length, row)
fit_row_length(expected_length, row)
assert str(exc_info.value) == 'expected must be larger than or equal 0.'
@@ -14,7 +14,7 @@
import pytest
from src.jcloud_docsgen.core.python._core import _markdown_table
from src.jcloud_docsgen.utils import markdown_table
@pytest.mark.parametrize(
@@ -138,6 +138,72 @@ from src.jcloud_docsgen.core.python._core import _markdown_table
"""| a |
| --- |
| line1\nline2 |""",
),
(
['a'],
[('line1\nline2 | 2',)],
True,
False,
False,
'--',
"""| a |
| --- |
| line1\nline2 \\| 2 |""",
),
(
['a'],
[('line1\nline2 | 2',)],
True,
False,
True,
'--',
"""| a |
| --- |
| line1 line2 \\| 2 |""",
),
(
['a'],
[('1 | 2',)],
True,
False,
True,
'--',
"""| a |
| --- |
| 1 \\| 2 |""",
),
(
['a'],
[('1 |',)],
True,
False,
True,
'--',
"""| a |
| --- |
| 1 \\| |""",
),
(
['a'],
[('|',)],
True,
False,
True,
'--',
"""| a |
| --- |
| \\| |""",
),
(
['a'],
[('| 2',)],
True,
False,
True,
'--',
"""| a |
| --- |
| \\| 2 |""",
),
(
[],
@@ -252,7 +318,7 @@ from src.jcloud_docsgen.core.python._core import _markdown_table
),
],
)
def test__markdown_table(
def test_markdown_table(
header,
body,
allow_markdown_table,
@@ -262,7 +328,7 @@ def test__markdown_table(
expected,
):
assert (
_markdown_table(
markdown_table(
header,
body,
allow_markdown_table=allow_markdown_table,
@@ -402,7 +468,7 @@ def test__markdown_table(
),
],
)
def test__markdown_table_exceptions(
def test_markdown_table_exceptions(
header,
body,
allow_markdown_table,
@@ -413,7 +479,7 @@ def test__markdown_table_exceptions(
expected_exception_msg,
):
with pytest.raises(expected_exception) as exc_info:
_markdown_table(
markdown_table(
header,
body,
allow_markdown_table=allow_markdown_table,