From 0261a7b3a6fc861ac8608fb3aeb8ff9217cb5940 Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Thu, 24 Sep 2026 22:38:04 +0200 Subject: [PATCH 1/6] feat(exceptions): add DirectoryNotEmptyError Added an OSError subclass for indicating that the directory is not empty. This exception is explicitly handled by the CLI now. --- src/jcloud_docsgen/cli/cli.py | 3 +++ src/jcloud_docsgen/exceptions.py | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/jcloud_docsgen/cli/cli.py b/src/jcloud_docsgen/cli/cli.py index 1fae4e0..7c7f43e 100644 --- a/src/jcloud_docsgen/cli/cli.py +++ b/src/jcloud_docsgen/cli/cli.py @@ -20,6 +20,7 @@ import argparse import pathlib import sys +from ..exceptions import DirectoryNotEmptyError from .languages.python import VALID_SECTIONS as VALID_PYTHON_SECTIONS __all__ = [ @@ -157,6 +158,8 @@ def main(argv=None) -> int: return error(f'{exc.filename}: not a directory') except IsADirectoryError as exc: return error(f'{exc.filename}: is a directory') + except DirectoryNotEmptyError as exc: + return error(f'{exc.filename}: directory not empty') except FileExistsError as exc: return error(f'{exc.filename}: file already exists') except PermissionError as exc: diff --git a/src/jcloud_docsgen/exceptions.py b/src/jcloud_docsgen/exceptions.py index dc81f2e..d687917 100644 --- a/src/jcloud_docsgen/exceptions.py +++ b/src/jcloud_docsgen/exceptions.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import errno + class NamespaceError(ValueError): """ @@ -101,3 +103,12 @@ class PythonDefinitionError(ValueError): class PythonArgumentStructureError(PythonDefinitionError): ... + + +class DirectoryNotEmptyError(OSError): + """ + Directory is not empty. + """ + + def __init__(self, filename: str | None = None) -> None: + super().__init__(errno.ENOTEMPTY, 'Directory not empty', filename) From 5791358b4143d4e7bd3bb040fdaf4d9b46890830 Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Sun, 27 Sep 2026 14:26:26 +0200 Subject: [PATCH 2/6] feat(python-generator): add force argument to generator Added a force argument to PythonDocumentationGenerator.generate_documentation to control whether the content of the output directory should be overwritten or not, if it is not empty. If the force argument is True, the output directory content will first be deleted recursively. If it is False and the output directory is not empty, a DirectoryNotEmptyError will be raised. --- src/jcloud_docsgen/core/python/_core.py | 27 +++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/jcloud_docsgen/core/python/_core.py b/src/jcloud_docsgen/core/python/_core.py index 73b6b26..af783b3 100644 --- a/src/jcloud_docsgen/core/python/_core.py +++ b/src/jcloud_docsgen/core/python/_core.py @@ -22,7 +22,13 @@ from typing import TypeAlias import docstring_parser -from ...exceptions import InvalidNamespaceError, PythonArgumentStructureError +import shutil + +from ...exceptions import ( + DirectoryNotEmptyError, + InvalidNamespaceError, + PythonArgumentStructureError, +) from ...utils import get_relative_path_segments, non_empty_str from .arguments import PythonArgumentKind, PythonFunctionArgument from .definitions import ( @@ -1114,12 +1120,29 @@ class PythonDocumentationGenerator: return self._namespace(self.project_directory) - def generate_documentation(self) -> None: + def generate_documentation(self, force: bool = False) -> None: """ Generates the full project documentation and writes it to the documentation directory. + + :param force: Indicates whether existing contents of the output + directory shall be overwritten. If ``True``, all + contents of the output directory will be deleted + first. + :type force: bool + + :raises DirectoryNotEmptyError: If ``force`` is ``False`` and + the output directory is not + empty. """ + if self.docs_directory.exists() and list( + self.docs_directory.iterdir() + ): + if force: + shutil.rmtree(self.docs_directory) + else: + raise DirectoryNotEmptyError(str(self.docs_directory)) self.docs_directory.mkdir(parents=True, exist_ok=True) _generate_package_namespace_documentation( From d8508e0fa307b4421996ad4712b16f630b016636 Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Sun, 27 Sep 2026 15:01:09 +0200 Subject: [PATCH 3/6] test(python-generator): add tests for force overwriting Added tests to test new output directory deletion behavior: - output directory overwriting (not empty; force) - error (not empty; not force) - nothing (empty; force) - nothing (empty; not force) One test was adapted to output directory emptiness check because the .gitkeep file which is required for Git to track the directory makes it non-empty. --- .../test_PythonDocumentationGenerator.py | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/tests/unit/core/python/_core/test_PythonDocumentationGenerator.py b/tests/unit/core/python/_core/test_PythonDocumentationGenerator.py index 36f095f..fb90798 100644 --- a/tests/unit/core/python/_core/test_PythonDocumentationGenerator.py +++ b/tests/unit/core/python/_core/test_PythonDocumentationGenerator.py @@ -18,6 +18,7 @@ import pytest import tempfile +from src.jcloud_docsgen.exceptions import DirectoryNotEmptyError from src.jcloud_docsgen.core.python import PythonDocumentationGenerator from src.jcloud_docsgen.core.python.namespaces import ( PythonModuleNamespace, @@ -120,7 +121,7 @@ def test_generate_documentation(dir, expectdir): pathlib.Path(str(d)), allow_tables=True, allow_html=False, - ).generate_documentation() # simplify the tests + ).generate_documentation(True) # simplify the tests def test_creates_directory(): @@ -144,3 +145,51 @@ def test_creates_directory(): ) docs_generator.generate_documentation() assert docs_dir.exists() + + +class TestForce: + def _prepare(self, directory): + directory = pathlib.Path(directory) + src_directory = directory / 'src' + src_directory.mkdir() + docs_directory = directory / 'docs' + docs_directory.mkdir() + return directory, src_directory, docs_directory + + def test_overwrites_output_directory(self): + with tempfile.TemporaryDirectory() as directory: + directory, src_directory, docs_directory = self._prepare(directory) + test_dir = docs_directory / 'test_dir' + test_dir.mkdir() + documentation_generator = PythonDocumentationGenerator( + src_directory, docs_directory + ) + documentation_generator.generate_documentation(True) + assert not test_dir.exists() + + def test_error(self): + with tempfile.TemporaryDirectory() as directory: + directory, src_directory, docs_directory = self._prepare(directory) + (docs_directory / 'test_dir').mkdir() + documentation_generator = PythonDocumentationGenerator( + src_directory, docs_directory + ) + with pytest.raises(DirectoryNotEmptyError) as exc_info: + documentation_generator.generate_documentation() + assert exc_info.value.filename == str(docs_directory) + + def test_nothing_force(self): + with tempfile.TemporaryDirectory() as directory: + directory, src_directory, docs_directory = self._prepare(directory) + documentation_generator = PythonDocumentationGenerator( + src_directory, docs_directory + ) + documentation_generator.generate_documentation(True) + + def test_nothing_not_force(self): + with tempfile.TemporaryDirectory() as directory: + directory, src_directory, docs_directory = self._prepare(directory) + documentation_generator = PythonDocumentationGenerator( + src_directory, docs_directory + ) + documentation_generator.generate_documentation(True) From f0c271f8fd231df57088b7b8bccf61a3f8619994 Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Sun, 27 Sep 2026 15:04:18 +0200 Subject: [PATCH 4/6] feat(cli): add Python generator force argument --- src/jcloud_docsgen/cli/cli.py | 10 ++++++++++ src/jcloud_docsgen/cli/languages/python.py | 11 ++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/jcloud_docsgen/cli/cli.py b/src/jcloud_docsgen/cli/cli.py index 7c7f43e..14c7e10 100644 --- a/src/jcloud_docsgen/cli/cli.py +++ b/src/jcloud_docsgen/cli/cli.py @@ -82,6 +82,15 @@ def create_argument_parser() -> argparse.ArgumentParser: default='docs/reference', help='the output directory the documentation will be written to', ) + common_parser.add_argument( + '-f', + '--force', + action='store_true', + help=( + 'generate documentation even if the output directory is not ' + 'empty. Warning: data can be lost when setting this flag.' + ), + ) argument_parser = argparse.ArgumentParser() @@ -151,6 +160,7 @@ def main(argv=None) -> int: exclude_sections=args.sections_exclude, allow_html=args.allow_html, allow_tables=args.allow_tables, + force=args.force, ) except FileNotFoundError as exc: return error(f'{exc.filename}: no such file or directory') diff --git a/src/jcloud_docsgen/cli/languages/python.py b/src/jcloud_docsgen/cli/languages/python.py index ae74fe9..c012e40 100644 --- a/src/jcloud_docsgen/cli/languages/python.py +++ b/src/jcloud_docsgen/cli/languages/python.py @@ -78,6 +78,7 @@ def _generate( ), allow_html: bool = False, allow_tables: bool = False, + force: bool = False, ) -> None: """ Generates the documentation for a Python project. @@ -94,6 +95,9 @@ def _generate( :type allow_html: bool :param allow_tables: Indicates whether Markdown tables are allowed. Warning: Markdown tables are not CommonMark. + :param force: Indicates whether the output directory shall be + overwritten if it is not empty. + :type force: bool """ docs_generator = PythonDocumentationGenerator( @@ -103,7 +107,7 @@ def _generate( allow_html=allow_html, allow_tables=allow_tables, ) - docs_generator.generate_documentation() + docs_generator.generate_documentation(force) def generate( @@ -114,6 +118,7 @@ def generate( exclude_sections: list[str] = [], allow_html: bool = False, allow_tables: bool = False, + force: bool = False, ) -> None: """ Generates the documentation for a Python project. @@ -133,6 +138,9 @@ def generate( :type allow_html: bool :param allow_tables: Indicates whether Markdown tables are allowed. Warning: Markdown tables are not CommonMark. + :param force: Indicates whether the output directory shall be + overwritten if it is not empty. + :type force: bool """ sections = parse_sections_list(include_sections, exclude_sections) @@ -143,4 +151,5 @@ def generate( include_sections=sections, allow_html=allow_html, allow_tables=allow_tables, + force=force, ) From f2b9f47e0964b111c03695fda147b850141259ce Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Sun, 27 Sep 2026 15:16:41 +0200 Subject: [PATCH 5/6] test(cli): add tests for Python force overwriting Added a test to the CLI test file for the documentation generation error. --- tests/unit/cli/test_python.py | 47 +++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/tests/unit/cli/test_python.py b/tests/unit/cli/test_python.py index 70891de..d15e037 100644 --- a/tests/unit/cli/test_python.py +++ b/tests/unit/cli/test_python.py @@ -16,6 +16,8 @@ import pathlib import pytest +import tempfile + from jcloud_docsgen.cli.cli import create_argument_parser from jcloud_docsgen.cli.cli import main as cli_main from jcloud_docsgen.cli.languages.python import parse_sections_list @@ -174,15 +176,40 @@ def test_sections_success(): def test_docs_generation(expectdir): with expectdir('python_project') as output_dir: output_dir: pathlib.Path + (output_dir / 'test_dir').mkdir() input_dir = pathlib.Path(__file__).parent / 'python_project/project' - cli_main( - [ - 'python', - '-s', - 'params', - 'docstring', - '-HTo', - str(output_dir), - str(input_dir), - ] + assert ( + cli_main( + [ + 'python', + '-s', + 'params', + 'docstring', + '-HTfo', + str(output_dir), + str(input_dir), + ] + ) + == 0 + ) + + +def test_docs_generation_error(): + with tempfile.TemporaryDirectory() as output_dir: + output_dir = pathlib.Path(output_dir) + (output_dir / 'test_dir').mkdir() + input_dir = pathlib.Path(__file__).parent / 'python_project/project' + assert ( + cli_main( + [ + 'python', + '-s', + 'params', + 'docstring', + '-HTo', + str(output_dir), + str(input_dir), + ] + ) + == 1 ) From b043a4ea1792a19ab8474cc12f79a2102b4c4837 Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Sun, 27 Sep 2026 17:37:02 +0200 Subject: [PATCH 6/6] docs(changelog): add accidental overwriting protection entry Added a changelog entry for the protection against accidental overwriting of the output directory. --- docs/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 731d028..7384717 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -7,6 +7,7 @@ - CLI for `jcloud-docsgen` that can be called without `python3 -m ...` - CI pipeline to generate API reference on every push or merge on `main` - Python generator output directory creation +- Protection against accidental overwriting of the output directory ### Changed