From 436dacf80604472614f64ca0c0a2e7cfb0d8b059 Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Sat, 5 Sep 2026 19:48:59 +0200 Subject: [PATCH 01/21] feat(cli): make project_directory argument optional --- src/jcloud_docsgen/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jcloud_docsgen/cli.py b/src/jcloud_docsgen/cli.py index b153c0a..0c0596e 100644 --- a/src/jcloud_docsgen/cli.py +++ b/src/jcloud_docsgen/cli.py @@ -33,6 +33,6 @@ def existing_directory(path: str) -> pathlib.Path: 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') + argument_parser.add_argument('project_directory', type = existing_directory, nargs = '?', default = pathlib.Path('.'), help = 'The directory of the project') args = argument_parser.parse_args() return 0 -- 2.47.3 From 2d6de2028fbeb4b44978ca45de68bd43e0a082a9 Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Sat, 5 Sep 2026 19:53:22 +0200 Subject: [PATCH 02/21] feat(cli): add output directory option --- src/jcloud_docsgen/cli.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/jcloud_docsgen/cli.py b/src/jcloud_docsgen/cli.py index 0c0596e..0189116 100644 --- a/src/jcloud_docsgen/cli.py +++ b/src/jcloud_docsgen/cli.py @@ -30,9 +30,15 @@ def existing_directory(path: str) -> pathlib.Path: raise argparse.ArgumentTypeError(f'{_path}: no such file or directory') if not path.is_dir(): raise argparse.ArgumentTypeError(f'{_path}: not a directory') + return path def main() -> int: argument_parser = argparse.ArgumentParser() argument_parser.add_argument('project_directory', type = existing_directory, nargs = '?', default = pathlib.Path('.'), help = 'The directory of the project') + argument_parser.add_argument('-o', '--output-dir', type = pathlib.Path, help = 'the output directory the documentation will be written to') args = argument_parser.parse_args() + + if args.output_dir is None: + args.output_dir = args.project_directory / 'docs/reference' + return 0 -- 2.47.3 From 350f8f9941ac53208bb8229a9f26ed3e051dc8ba Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Sat, 5 Sep 2026 20:35:05 +0200 Subject: [PATCH 03/21] refactor(cli)!: make CLI a package --- src/jcloud_docsgen/cli/__init__.py | 19 +++++++++++++++++++ src/jcloud_docsgen/{ => cli}/cli.py | 0 2 files changed, 19 insertions(+) create mode 100644 src/jcloud_docsgen/cli/__init__.py rename src/jcloud_docsgen/{ => cli}/cli.py (100%) diff --git a/src/jcloud_docsgen/cli/__init__.py b/src/jcloud_docsgen/cli/__init__.py new file mode 100644 index 0000000..863e79b --- /dev/null +++ b/src/jcloud_docsgen/cli/__init__.py @@ -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 .cli import main + +__all__ = [ + 'main' +] diff --git a/src/jcloud_docsgen/cli.py b/src/jcloud_docsgen/cli/cli.py similarity index 100% rename from src/jcloud_docsgen/cli.py rename to src/jcloud_docsgen/cli/cli.py -- 2.47.3 From bce073746e1556184a6a2f362f421ed5b7b91b91 Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Wed, 9 Sep 2026 20:27:25 +0200 Subject: [PATCH 04/21] feat(cli): add Python CLI handling Added a subparser for Python documentation and a function to generate Python documentation. --- src/jcloud_docsgen/cli/cli.py | 67 +++++++++++- src/jcloud_docsgen/cli/parsers/__init__.py | 19 ++++ src/jcloud_docsgen/cli/parsers/python.py | 114 +++++++++++++++++++++ 3 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 src/jcloud_docsgen/cli/parsers/__init__.py create mode 100644 src/jcloud_docsgen/cli/parsers/python.py diff --git a/src/jcloud_docsgen/cli/cli.py b/src/jcloud_docsgen/cli/cli.py index 0189116..70864b6 100644 --- a/src/jcloud_docsgen/cli/cli.py +++ b/src/jcloud_docsgen/cli/cli.py @@ -22,6 +22,9 @@ __all__ = [ import argparse import pathlib +from .parsers.python import VALID_SECTIONS as VALID_PYTHON_SECTIONS + +_LANG_PYTHON = 'python' def existing_directory(path: str) -> pathlib.Path: _path = path @@ -33,12 +36,72 @@ def existing_directory(path: str) -> pathlib.Path: return path def main() -> int: + common_parser = argparse.ArgumentParser(add_help = False) + common_parser.add_argument( + 'project_directory', + type = existing_directory, + nargs = '?', + default = pathlib.Path('.'), + help = 'The directory of the project' + ) + common_parser.add_argument( + '-o', + '--output-dir', + type = pathlib.Path, + help = 'the output directory the documentation will be written to' + ) + argument_parser = argparse.ArgumentParser() - argument_parser.add_argument('project_directory', type = existing_directory, nargs = '?', default = pathlib.Path('.'), help = 'The directory of the project') - argument_parser.add_argument('-o', '--output-dir', type = pathlib.Path, help = 'the output directory the documentation will be written to') + + subparsers = argument_parser.add_subparsers(dest = 'language', required = True) + + python_parser = subparsers.add_parser( + _LANG_PYTHON, + parents = [common_parser], + help = 'generate documentation for Python project' + ) + python_parser.add_argument( + '-s', + '--sections', + nargs = '+', + choices = VALID_PYTHON_SECTIONS, + help = 'the sections to be included in the documentation' + ) + python_parser.add_argument( + '-S', + '--sections-exclude', + nargs = '+', + default = [], + choices = VALID_PYTHON_SECTIONS, + help = 'the sections to be excluded in the documentation. If a section shall be included AND excluded, it is excluded.' + ) + python_parser.add_argument( + '-H', + '--allow-html', + action = 'store_true', + help = 'indicates whether HTML is allowed in the documentation. By default, HTML is disabled.' + ) + python_parser.add_argument( + '-T', + '--allow-tables', + action = 'store_true', + help = 'indicates whether Markdown tables are allowed in the documentation. By default, Markdown tables are disabled. Warning: Markdown tables are not CommonMark.' + ) + args = argument_parser.parse_args() if args.output_dir is None: args.output_dir = args.project_directory / 'docs/reference' + if args.language == _LANG_PYTHON: + from .parsers.python import generate + generate( + args.project_directory, + args.output_dir, + include_sections = args.sections, + exclude_sections = args.sections_exclude, + allow_html = args.allow_html, + allow_tables = args.allow_tables + ) + return 0 diff --git a/src/jcloud_docsgen/cli/parsers/__init__.py b/src/jcloud_docsgen/cli/parsers/__init__.py new file mode 100644 index 0000000..f774970 --- /dev/null +++ b/src/jcloud_docsgen/cli/parsers/__init__.py @@ -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' +] diff --git a/src/jcloud_docsgen/cli/parsers/python.py b/src/jcloud_docsgen/cli/parsers/python.py new file mode 100644 index 0000000..d67600a --- /dev/null +++ b/src/jcloud_docsgen/cli/parsers/python.py @@ -0,0 +1,114 @@ +# 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 pathlib +from ...core.python import PythonDefinitionDocumentationIncludeSections, PythonDocumentationGenerator +from typing import Optional + +__all__ = [ + 'VALID_SECTIONS', + 'generate' +] + +VALID_SECTIONS = list(PythonDefinitionDocumentationIncludeSections()._args.keys()) + +def _generate( + project_directory: pathlib.Path, + output_directory: pathlib.Path, + *, + include_sections: PythonDefinitionDocumentationIncludeSections = PythonDefinitionDocumentationIncludeSections(), + allow_html: bool = False, + allow_tables: bool = False +) -> None: + ''' + Generates the documentation for a Python project. + + :param project_directory: The project directory. + :type project_directory: pathlib.Path + :param output_directory: The output directory. + :type output_directory: pathlib.Path + :param include_sections: The sections to be included in the + documentation. + :type include_sections: PythonDefinitionDocumentationIncludeSections + :param allow_html: Indicates whether HTML is allowed to appear in + the generated documentation. + :type allow_html: bool + :param allow_tables: Indicates whether Markdown tables are allowed. + Warning: Markdown tables are not CommonMark. + ''' + + docs_generator = PythonDocumentationGenerator( + project_directory, + output_directory, + include_sections = include_sections, + allow_html = allow_html, + allow_tables = allow_tables + ) + docs_generator.generate_documentation() + +def generate( + project_directory: pathlib.Path, + output_directory: pathlib.Path, + *, + include_sections: Optional[list[str]] = None, + exclude_sections: list[str] = [], + allow_html: bool = False, + allow_tables: bool = False +) -> None: + ''' + Generates the documentation for a Python project. + + :param project_directory: The project directory. + :type project_directory: pathlib.Path + :param output_directory: The output directory. + :type output_directory: pathlib.Path + :param include_sections: The sections to be included in the + documentation. + :type include_sections: Optional[list[str]] + :param exclude_sections: The sections to be excluded in the + documentation. + :type exclude_sections: list[str] + :param allow_html: Indicates whether HTML is allowed to appear in + the generated documentation. + :type allow_html: bool + :param allow_tables: Indicates whether Markdown tables are allowed. + Warning: Markdown tables are not CommonMark. + ''' + + if include_sections is None: + include_sections = VALID_SECTIONS + + for section in exclude_sections: + while section in include_sections: + del include_sections[include_sections.index(section)] + + sections = PythonDefinitionDocumentationIncludeSections( + signature = 'signature' in include_sections, + returns = 'returns' in include_sections, + params = 'params' in include_sections, + exceptions = 'exceptions' in include_sections, + examples = 'examples' in include_sections, + docstring = 'docstring' in include_sections, + bases = 'bases' in include_sections, + decorators = 'decorators' in include_sections, + class_body = 'class_body' in include_sections + ) if include_sections else PythonDefinitionDocumentationIncludeSections() + + _generate( + project_directory, + output_directory, + include_sections = sections, + allow_html = allow_html, + allow_tables = allow_tables + ) -- 2.47.3 From 34457f0b9f13f9d2ec13b878503eddccc565cf98 Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Thu, 10 Sep 2026 09:50:19 +0200 Subject: [PATCH 05/21] chore: add project script Added the script 'jcloud-docsgen' to pyproject.toml so that jCloud Docsgen can be run directly without using 'python3 -m ...'. --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 8452ac3..a86e6bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,9 @@ dependencies = ["docstring-parser"] requires-python = ">=3.10" license = "Apache-2.0" +[project.scripts] +jcloud-docsgen = "jcloud_docsgen.cli.cli:main" + [tool.poetry] packages = [ { include = "jcloud_docsgen", from = "src"} -- 2.47.3 From a8513a78e2f4ff47addcefa42db14e4926e83b2c Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Thu, 10 Sep 2026 09:51:55 +0200 Subject: [PATCH 06/21] docs(changelog): add CLI entry to changelog --- docs/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 7e3b690..5a473c4 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- CLI for `jcloud-docsgen` + ### Deprecated - utility class for existing directories -- 2.47.3 From 60a097f0cce33ebb8dc822841e5dafd0cd0e7c23 Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Thu, 10 Sep 2026 09:58:18 +0200 Subject: [PATCH 07/21] feat(cli): allow to pass command line arguments to function --- src/jcloud_docsgen/cli/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/jcloud_docsgen/cli/cli.py b/src/jcloud_docsgen/cli/cli.py index 70864b6..d54fde5 100644 --- a/src/jcloud_docsgen/cli/cli.py +++ b/src/jcloud_docsgen/cli/cli.py @@ -35,7 +35,7 @@ def existing_directory(path: str) -> pathlib.Path: raise argparse.ArgumentTypeError(f'{_path}: not a directory') return path -def main() -> int: +def main(argv = None) -> int: common_parser = argparse.ArgumentParser(add_help = False) common_parser.add_argument( 'project_directory', @@ -88,7 +88,7 @@ def main() -> int: help = 'indicates whether Markdown tables are allowed in the documentation. By default, Markdown tables are disabled. Warning: Markdown tables are not CommonMark.' ) - args = argument_parser.parse_args() + args = argument_parser.parse_args(argv) if args.output_dir is None: args.output_dir = args.project_directory / 'docs/reference' -- 2.47.3 From 3ab9884be07080f42629d7da1f28b1f8beb460fb Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Thu, 10 Sep 2026 13:26:53 +0200 Subject: [PATCH 08/21] feat(cli): add error handling The CLI now handles error that may result from user input and prints an error message. Following error types are handled: - file not found - not a directory - directory instead of file - file exists - permission error - general operating system errors - Unicode errors - errors parsing project source code Additionally, KeyboardInterrupt and BrokenPipeError are catched and an appropriate exit code is returned. --- src/jcloud_docsgen/cli/cli.py | 58 +++++++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/src/jcloud_docsgen/cli/cli.py b/src/jcloud_docsgen/cli/cli.py index d54fde5..a9a6db2 100644 --- a/src/jcloud_docsgen/cli/cli.py +++ b/src/jcloud_docsgen/cli/cli.py @@ -22,9 +22,11 @@ __all__ = [ import argparse import pathlib +import sys from .parsers.python import VALID_SECTIONS as VALID_PYTHON_SECTIONS _LANG_PYTHON = 'python' +_PROGRAM_NAME = 'jcloud-docsgen' def existing_directory(path: str) -> pathlib.Path: _path = path @@ -35,6 +37,19 @@ def existing_directory(path: str) -> pathlib.Path: raise argparse.ArgumentTypeError(f'{_path}: not a directory') return path +def error(message: str, exit_code: int = 1): + ''' + Shows an error message and returns the exit code. + + :param message: The error message to show. + :type message: str + :param exit_code: The exit code. + :type exit_code: int + ''' + + print(f'{_PROGRAM_NAME}: error: {message}', file = sys.stderr) + return exit_code + def main(argv = None) -> int: common_parser = argparse.ArgumentParser(add_help = False) common_parser.add_argument( @@ -93,15 +108,38 @@ def main(argv = None) -> int: if args.output_dir is None: args.output_dir = args.project_directory / 'docs/reference' - if args.language == _LANG_PYTHON: - from .parsers.python import generate - generate( - args.project_directory, - args.output_dir, - include_sections = args.sections, - exclude_sections = args.sections_exclude, - allow_html = args.allow_html, - allow_tables = args.allow_tables - ) + try: + if args.language == _LANG_PYTHON: + from .parsers.python import generate + generate( + args.project_directory, + args.output_dir, + include_sections = args.sections, + exclude_sections = args.sections_exclude, + allow_html = args.allow_html, + allow_tables = args.allow_tables + ) + except FileNotFoundError as exc: + return error(f'{exc.filename}: no such file or directory') + except NotADirectoryError as exc: + return error(f'{exc.filename}: not a directory') + except IsADirectoryError as exc: + return error(f'{exc.filename}: is a directory') + except FileExistsError as exc: + return error(f'{exc.filename}: file already exists') + except PermissionError as exc: + return error(f'{exc.filename}: permission denied') + except OSError as exc: + return error(f'{exc.filename}: {exc.args[1]}') + except UnicodeError: + return error('invalid text encoding') + except SyntaxError: + # TODO: add a custom exception class to clearify that a project + # parsing syntax error is meant. + return error(f'syntax error in project source code') + except KeyboardInterrupt: + return 130 + except BrokenPipeError: + return 141 return 0 -- 2.47.3 From 29d1c5bced09fb813efe39783cde01231cf0e8ff Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Thu, 10 Sep 2026 13:34:59 +0200 Subject: [PATCH 09/21] refactor(cli): move parser creation to a function --- src/jcloud_docsgen/cli/cli.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/jcloud_docsgen/cli/cli.py b/src/jcloud_docsgen/cli/cli.py index a9a6db2..3c25d3e 100644 --- a/src/jcloud_docsgen/cli/cli.py +++ b/src/jcloud_docsgen/cli/cli.py @@ -50,7 +50,15 @@ def error(message: str, exit_code: int = 1): print(f'{_PROGRAM_NAME}: error: {message}', file = sys.stderr) return exit_code -def main(argv = None) -> int: +def create_argument_parser() -> argparse.ArgumentParser: + ''' + Prepares a parser to parse the command-line arguments using + argparse. + + :return: The parser. + :rtype: argparse.ArgumentParser + ''' + common_parser = argparse.ArgumentParser(add_help = False) common_parser.add_argument( 'project_directory', @@ -103,6 +111,10 @@ def main(argv = None) -> int: help = 'indicates whether Markdown tables are allowed in the documentation. By default, Markdown tables are disabled. Warning: Markdown tables are not CommonMark.' ) + return argument_parser + +def main(argv = None) -> int: + argument_parser = create_argument_parser() args = argument_parser.parse_args(argv) if args.output_dir is None: -- 2.47.3 From db0d36f956505b7147388766b3ece12d1807e6b6 Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Thu, 10 Sep 2026 13:37:45 +0200 Subject: [PATCH 10/21] feat(cli): add __all__ to cli.cli module --- src/jcloud_docsgen/cli/cli.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/jcloud_docsgen/cli/cli.py b/src/jcloud_docsgen/cli/cli.py index 3c25d3e..eede797 100644 --- a/src/jcloud_docsgen/cli/cli.py +++ b/src/jcloud_docsgen/cli/cli.py @@ -25,6 +25,13 @@ import pathlib import sys from .parsers.python import VALID_SECTIONS as VALID_PYTHON_SECTIONS +__all__ = [ + 'existing_directory', + 'error', + 'create_argument_parser', + 'main' +] + _LANG_PYTHON = 'python' _PROGRAM_NAME = 'jcloud-docsgen' -- 2.47.3 From 348be4f07f4698a698a7458afd1dcde55f83f255 Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Thu, 10 Sep 2026 15:52:05 +0200 Subject: [PATCH 11/21] refactor(cli): rename 'parsers' package to 'languages' --- src/jcloud_docsgen/cli/cli.py | 4 ++-- src/jcloud_docsgen/cli/{parsers => languages}/__init__.py | 0 src/jcloud_docsgen/cli/{parsers => languages}/python.py | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename src/jcloud_docsgen/cli/{parsers => languages}/__init__.py (100%) rename src/jcloud_docsgen/cli/{parsers => languages}/python.py (100%) diff --git a/src/jcloud_docsgen/cli/cli.py b/src/jcloud_docsgen/cli/cli.py index eede797..c44e93c 100644 --- a/src/jcloud_docsgen/cli/cli.py +++ b/src/jcloud_docsgen/cli/cli.py @@ -23,7 +23,7 @@ __all__ = [ import argparse import pathlib import sys -from .parsers.python import VALID_SECTIONS as VALID_PYTHON_SECTIONS +from .languages.python import VALID_SECTIONS as VALID_PYTHON_SECTIONS __all__ = [ 'existing_directory', @@ -129,7 +129,7 @@ def main(argv = None) -> int: try: if args.language == _LANG_PYTHON: - from .parsers.python import generate + from .languages.python import generate generate( args.project_directory, args.output_dir, diff --git a/src/jcloud_docsgen/cli/parsers/__init__.py b/src/jcloud_docsgen/cli/languages/__init__.py similarity index 100% rename from src/jcloud_docsgen/cli/parsers/__init__.py rename to src/jcloud_docsgen/cli/languages/__init__.py diff --git a/src/jcloud_docsgen/cli/parsers/python.py b/src/jcloud_docsgen/cli/languages/python.py similarity index 100% rename from src/jcloud_docsgen/cli/parsers/python.py rename to src/jcloud_docsgen/cli/languages/python.py -- 2.47.3 From 84fe67230f003d5d8b904c1586e2de770bc03d83 Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Fri, 11 Sep 2026 19:13:25 +0200 Subject: [PATCH 12/21] test(cli): add tests for the CLI --- tests/integration/cli/test_cli.py | 34 ++++++++++ .../cli/python_project/expected/pkg/module.md | 20 ++++++ .../python_project/project/src/pkg/module.py | 11 ++++ tests/unit/cli/test_parser.py | 34 ++++++++++ tests/unit/cli/test_python.py | 64 +++++++++++++++++++ 5 files changed, 163 insertions(+) create mode 100644 tests/integration/cli/test_cli.py create mode 100644 tests/unit/cli/python_project/expected/pkg/module.md create mode 100644 tests/unit/cli/python_project/project/src/pkg/module.py create mode 100644 tests/unit/cli/test_parser.py create mode 100644 tests/unit/cli/test_python.py diff --git a/tests/integration/cli/test_cli.py b/tests/integration/cli/test_cli.py new file mode 100644 index 0000000..1e2d1f1 --- /dev/null +++ b/tests/integration/cli/test_cli.py @@ -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. + +import subprocess +import sys + +def test_cli(): + result = subprocess.run( + [ + sys.executable, + '-m', + 'jcloud_docsgen', + '--help' + ], + capture_output = True, + text = True + ) + stdout = result.stdout.lower() + assert \ + 'usage:' in stdout and \ + 'positional arguments' in stdout and \ + 'options' in stdout and \ + '--help' in stdout diff --git a/tests/unit/cli/python_project/expected/pkg/module.md b/tests/unit/cli/python_project/expected/pkg/module.md new file mode 100644 index 0000000..7b92bdc --- /dev/null +++ b/tests/unit/cli/python_project/expected/pkg/module.md @@ -0,0 +1,20 @@ +# Module `pkg.module` + +## Function `f` + +Does something. + + +### Parameters + +| Parameter | Type | Kind | Default value | Description | +| --- | --- | --- | --- | --- | +| `a` | `int` | normal | `42` | A parameter. | + +

Docstring

Does something. + +:param a: A parameter. +:type a: int + +:return: A string. +:rtype: str
\ No newline at end of file diff --git a/tests/unit/cli/python_project/project/src/pkg/module.py b/tests/unit/cli/python_project/project/src/pkg/module.py new file mode 100644 index 0000000..fd01ebd --- /dev/null +++ b/tests/unit/cli/python_project/project/src/pkg/module.py @@ -0,0 +1,11 @@ +def f(a: int = 42) -> str: + ''' + Does something. + + :param a: A parameter. + :type a: int + + :return: A string. + :rtype: str + ''' + ... diff --git a/tests/unit/cli/test_parser.py b/tests/unit/cli/test_parser.py new file mode 100644 index 0000000..7eb5a19 --- /dev/null +++ b/tests/unit/cli/test_parser.py @@ -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. + +import pytest +from jcloud_docsgen.cli.cli import create_argument_parser + +@pytest.mark.parametrize('lang', [ + 'javascript', + 'Python', + 'PYTHON' +]) +def test_language_error(lang): + parser = create_argument_parser() + + with pytest.raises(SystemExit): + parser.parse_args([lang]) + +@pytest.mark.parametrize('lang', [ + 'python' +]) +def test_language_success(lang): + parser = create_argument_parser() + parser.parse_args([lang]) diff --git a/tests/unit/cli/test_python.py b/tests/unit/cli/test_python.py new file mode 100644 index 0000000..947f566 --- /dev/null +++ b/tests/unit/cli/test_python.py @@ -0,0 +1,64 @@ +# 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 pathlib +import pytest +from jcloud_docsgen.cli.cli import create_argument_parser, main as cli_main + +def test_sections_error(): + parser = create_argument_parser() + + with pytest.raises(SystemExit): + parser.parse_args([ + 'python', + '-s', + 'param' # correct: `params` + ]) + + with pytest.raises(SystemExit): + parser.parse_args([ + 'python', + '-s', + 'params', + 'docstrng' # correct: `docstring` + ]) + +def test_sections_success(): + parser = create_argument_parser() + + parser.parse_args([ + 'python', + '-s', + 'params' + ]) + parser.parse_args([ + 'python', + '-s', + 'params', + 'docstring' + ]) + +def test_docs_generation(expectdir): + with expectdir('python_project') as output_dir: + output_dir: pathlib.Path + input_dir = pathlib.Path(__file__).parent / 'python_project/project' + cli_main([ + 'python', + '-s', + 'params', + 'docstring', + '-HTo', + str(output_dir), + str(input_dir) + ]) -- 2.47.3 From e2e6a59d137372b0c7b704a3ef722bd9a8a4a432 Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Sat, 12 Sep 2026 19:53:45 +0200 Subject: [PATCH 13/21] docs(changelog): update CLI entry --- docs/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 5a473c4..7623886 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,7 +4,7 @@ ### Added -- CLI for `jcloud-docsgen` +- CLI for `jcloud-docsgen` that can be called without `python3 -m ...` ### Deprecated -- 2.47.3 From 8f5dc33369c1884906f313ab1d715f1fe30015f7 Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Sat, 12 Sep 2026 19:56:15 +0200 Subject: [PATCH 14/21] feat(cli): import languages package --- src/jcloud_docsgen/cli/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/jcloud_docsgen/cli/__init__.py b/src/jcloud_docsgen/cli/__init__.py index 863e79b..9acb5fc 100644 --- a/src/jcloud_docsgen/cli/__init__.py +++ b/src/jcloud_docsgen/cli/__init__.py @@ -12,8 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +from . import languages from .cli import main __all__ = [ + 'languages', 'main' ] -- 2.47.3 From 2cb99d4ca931b7a8dc5ee0c965438dde35082d92 Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Sun, 13 Sep 2026 14:50:34 +0200 Subject: [PATCH 15/21] feat(cli): add cli module to cli package __init__ module --- src/jcloud_docsgen/cli/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/jcloud_docsgen/cli/__init__.py b/src/jcloud_docsgen/cli/__init__.py index 9acb5fc..f9d1488 100644 --- a/src/jcloud_docsgen/cli/__init__.py +++ b/src/jcloud_docsgen/cli/__init__.py @@ -12,10 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +from . import cli from . import languages from .cli import main __all__ = [ + 'cli', 'languages', 'main' ] -- 2.47.3 From 8644232a00c446e53c5ac7adc966a28182b3690b Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Sun, 13 Sep 2026 14:54:32 +0200 Subject: [PATCH 16/21] feat: add __main__ module Added a __main__ module that calls the CLI main function. --- src/jcloud_docsgen/__main__.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/jcloud_docsgen/__main__.py diff --git a/src/jcloud_docsgen/__main__.py b/src/jcloud_docsgen/__main__.py new file mode 100644 index 0000000..57403ce --- /dev/null +++ b/src/jcloud_docsgen/__main__.py @@ -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. + +import sys +from .cli.cli import main + +if __name__ == '__main__': + sys.exit(main()) -- 2.47.3 From 089cf5f2ff28121a81ecdf9b7caeeaafa655212c Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Sun, 13 Sep 2026 16:34:26 +0200 Subject: [PATCH 17/21] refactor(cli): move sections list parsing to function --- src/jcloud_docsgen/cli/languages/python.py | 55 +++++++++++++++------- 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/src/jcloud_docsgen/cli/languages/python.py b/src/jcloud_docsgen/cli/languages/python.py index d67600a..017065f 100644 --- a/src/jcloud_docsgen/cli/languages/python.py +++ b/src/jcloud_docsgen/cli/languages/python.py @@ -18,11 +18,47 @@ from typing import Optional __all__ = [ 'VALID_SECTIONS', + 'parse_sections_list', 'generate' ] VALID_SECTIONS = list(PythonDefinitionDocumentationIncludeSections()._args.keys()) +def parse_sections_list(include_sections: Optional[list[str]] = None, exclude_sections: list[str] = []) -> PythonDefinitionDocumentationIncludeSections: + ''' + Returns a Python definition documentation sections list from the + sections list to be included and the sections list to be excluded. + These lists both consist of the sections as strings which contain + the section name. + + :param include_sections: The list of sections to be included. + :type include_sections: Optional[list[str]] + :param exclude_sections: The list of sections to be excluded. + :type exclude_sections: list[str] + + :return: The Python definition documentation sections object. + :rtype: PythonDefinitionDocumentationIncludeSections + ''' + + if include_sections is None: + include_sections = VALID_SECTIONS + + for section in exclude_sections: + while section in include_sections: + del include_sections[include_sections.index(section)] + + return PythonDefinitionDocumentationIncludeSections( + signature = 'signature' in include_sections, + returns = 'returns' in include_sections, + params = 'params' in include_sections, + exceptions = 'exceptions' in include_sections, + examples = 'examples' in include_sections, + docstring = 'docstring' in include_sections, + bases = 'bases' in include_sections, + decorators = 'decorators' in include_sections, + class_body = 'class_body' in include_sections + ) + def _generate( project_directory: pathlib.Path, output_directory: pathlib.Path, @@ -86,24 +122,7 @@ def generate( Warning: Markdown tables are not CommonMark. ''' - if include_sections is None: - include_sections = VALID_SECTIONS - - for section in exclude_sections: - while section in include_sections: - del include_sections[include_sections.index(section)] - - sections = PythonDefinitionDocumentationIncludeSections( - signature = 'signature' in include_sections, - returns = 'returns' in include_sections, - params = 'params' in include_sections, - exceptions = 'exceptions' in include_sections, - examples = 'examples' in include_sections, - docstring = 'docstring' in include_sections, - bases = 'bases' in include_sections, - decorators = 'decorators' in include_sections, - class_body = 'class_body' in include_sections - ) if include_sections else PythonDefinitionDocumentationIncludeSections() + sections = parse_sections_list(include_sections, exclude_sections) _generate( project_directory, -- 2.47.3 From 5e84156e01263f94a49602d2e268218901ea77d0 Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Sun, 13 Sep 2026 19:00:42 +0200 Subject: [PATCH 18/21] fix(cli): copy VALID_SECTIONS when parsing sections list --- src/jcloud_docsgen/cli/languages/python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jcloud_docsgen/cli/languages/python.py b/src/jcloud_docsgen/cli/languages/python.py index 017065f..b905416 100644 --- a/src/jcloud_docsgen/cli/languages/python.py +++ b/src/jcloud_docsgen/cli/languages/python.py @@ -41,7 +41,7 @@ def parse_sections_list(include_sections: Optional[list[str]] = None, exclude_se ''' if include_sections is None: - include_sections = VALID_SECTIONS + include_sections = VALID_SECTIONS[:] for section in exclude_sections: while section in include_sections: -- 2.47.3 From bd7780f112a2c539ff339459eed1e1f71f753a34 Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Sun, 13 Sep 2026 19:01:46 +0200 Subject: [PATCH 19/21] test(cli): add tests for Python sections list parser --- tests/unit/cli/test_python.py | 102 ++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/tests/unit/cli/test_python.py b/tests/unit/cli/test_python.py index 947f566..a797249 100644 --- a/tests/unit/cli/test_python.py +++ b/tests/unit/cli/test_python.py @@ -15,6 +15,108 @@ import pathlib import pytest from jcloud_docsgen.cli.cli import create_argument_parser, main as cli_main +from jcloud_docsgen.cli.languages.python import parse_sections_list +from jcloud_docsgen.core.python import PythonDefinitionDocumentationIncludeSections + +@pytest.mark.parametrize('include,exclude,expected', [ + ( + [], + [], + PythonDefinitionDocumentationIncludeSections( + signature = False, + returns = False, + params = False, + exceptions = False, + examples = False, + docstring = False, + bases = False, + decorators = False, + class_body = False + ) + ), + ( + [], + ['signature'], + PythonDefinitionDocumentationIncludeSections( + signature = False, + returns = False, + params = False, + exceptions = False, + examples = False, + docstring = False, + bases = False, + decorators = False, + class_body = False + ) + ), + ( + ['signature'], + ['signature'], + PythonDefinitionDocumentationIncludeSections( + signature = False, + returns = False, + params = False, + exceptions = False, + examples = False, + docstring = False, + bases = False, + decorators = False, + class_body = False + ) + ), + ( + ['signature'], + [], + PythonDefinitionDocumentationIncludeSections( + signature = True, + returns = False, + params = False, + exceptions = False, + examples = False, + docstring = False, + bases = False, + decorators = False, + class_body = False + ) + ), + ( + None, + [], + PythonDefinitionDocumentationIncludeSections() + ), + ( + ['signature', 'exceptions', 'bases', 'class_body'], + ['signature', 'bases'], + PythonDefinitionDocumentationIncludeSections( + signature = False, + returns = False, + params = False, + exceptions = True, + examples = False, + docstring = False, + bases = False, + decorators = False, + class_body = True + ) + ), + ( + None, + ['params', 'docstring'], + PythonDefinitionDocumentationIncludeSections( + signature = True, + returns = True, + params = False, + exceptions = True, + examples = True, + docstring = False, + bases = True, + decorators = True, + class_body = True + ) + ) +]) +def test_parse_sections_list(include, exclude, expected): + assert parse_sections_list(include, exclude) == expected def test_sections_error(): parser = create_argument_parser() -- 2.47.3 From 56588e6c785ee3977be0372960b2633eb7b4850a Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Sun, 13 Sep 2026 20:09:02 +0200 Subject: [PATCH 20/21] fix: jcloud_docsgen.cli instead of jcloud_docsgen.cli.cli in pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a86e6bf..1dbff9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ requires-python = ">=3.10" license = "Apache-2.0" [project.scripts] -jcloud-docsgen = "jcloud_docsgen.cli.cli:main" +jcloud-docsgen = "jcloud_docsgen.cli:main" [tool.poetry] packages = [ -- 2.47.3 From 1ce3d8f9d19248e25547a0444311e8429b98f6e1 Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Sun, 13 Sep 2026 20:10:22 +0200 Subject: [PATCH 21/21] refactor(cli): remove unnecessary, redundant __all__ --- src/jcloud_docsgen/cli/cli.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/jcloud_docsgen/cli/cli.py b/src/jcloud_docsgen/cli/cli.py index c44e93c..f079be0 100644 --- a/src/jcloud_docsgen/cli/cli.py +++ b/src/jcloud_docsgen/cli/cli.py @@ -16,10 +16,6 @@ The command line interface ''' -__all__ = [ - 'main' -] - import argparse import pathlib import sys -- 2.47.3