From b2389cb271e0b49e5dd248d42d3a59bff055a219 Mon Sep 17 00:00:00 2001 From: Gregor Sturm <gregor.sturm@boehringer-ingelheim.com> Date: Tue, 14 Jan 2025 16:26:31 +0100 Subject: [PATCH] docs command reference (#82) * Sphinx command reference with click_extra * Docs: add command reference --- docs/command_reference.md | 97 ++++++++++++++++++++++++++++++++++++ docs/conf.py | 9 +++- docs/index.md | 1 + pyproject.toml | 4 +- src/dso/cli/__init__.py | 34 ++++++------- src/dso/cli/_create.py | 10 ++-- src/dso/cli/_exec.py | 6 +-- tests/conftest.py | 6 +-- tests/test_cli.py | 4 +- tests/test_compile_config.py | 8 +-- tests/test_create.py | 8 +-- tests/test_exec.py | 10 ++-- tests/test_get_config.py | 4 +- tests/test_init.py | 6 +-- tests/test_lint.py | 4 +- tests/test_pandocfilter.py | 4 +- tests/test_watermark.py | 4 +- 17 files changed, 162 insertions(+), 57 deletions(-) create mode 100644 docs/command_reference.md diff --git a/docs/command_reference.md b/docs/command_reference.md new file mode 100644 index 0000000..31d8b14 --- /dev/null +++ b/docs/command_reference.md @@ -0,0 +1,97 @@ +# Command reference + +## dso + +```{eval-rst} +.. click:run:: + from dso.cli import dso + invoke(dso, args=["--help"]) +``` + +## dso compile-config + +```{eval-rst} +.. click:run:: + from dso.cli import dso + invoke(dso, args=["compile-config", "--help"]) +``` + +## dso create + +```{eval-rst} +.. click:run:: + from dso.cli import dso + invoke(dso, args=["create", "--help"]) +``` + +### dso create folder + +```{eval-rst} +.. click:run:: + from dso.cli import dso + invoke(dso, args=["create", "folder", "--help"]) +``` + +### dso create stage + +```{eval-rst} +.. click:run:: + from dso.cli import dso + invoke(dso, args=["create", "stage", "--help"]) +``` + +## dso exec + +```{eval-rst} +.. click:run:: + from dso.cli import dso + invoke(dso, args=["exec", "--help"]) +``` + +### dso exec quarto + +```{eval-rst} +.. click:run:: + from dso.cli import dso + invoke(dso, args=["exec", "quarto" ,"--help"]) +``` + +## dso get-config + +```{eval-rst} +.. click:run:: + from dso.cli import dso + invoke(dso, args=["get-config", "--help"]) +``` + +## dso init + +```{eval-rst} +.. click:run:: + from dso.cli import dso + invoke(dso, args=["init", "--help"]) +``` + +## dso lint + +```{eval-rst} +.. click:run:: + from dso.cli import dso + invoke(dso, args=["lint", "--help"]) +``` + +## dso repro + +```{eval-rst} +.. click:run:: + from dso.cli import dso + invoke(dso, args=["repro", "--help"]) +``` + +## dso watermark + +```{eval-rst} +.. click:run:: + from dso.cli import dso + invoke(dso, args=["watermark", "--help"]) +``` diff --git a/docs/conf.py b/docs/conf.py index bb7f51e..541355b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,6 +9,13 @@ from datetime import datetime from importlib.metadata import metadata from pathlib import Path +import os +import rich_click as click + +# set maximum width for help pages +click.rich_click.MAX_WIDTH = 96 +# Set force color for rich outputs shown in the docs +os.environ["FORCE_COLOR"] = "1" HERE = Path(__file__).parent sys.path.insert(0, str(HERE / "extensions")) @@ -58,7 +65,7 @@ "sphinx.ext.mathjax", "IPython.sphinxext.ipython_console_highlighting", "sphinxext.opengraph", - "sphinxcontrib.programoutput", + "click_extra.sphinx", *[p.stem for p in (HERE / "extensions").glob("*.py")], ] diff --git a/docs/index.md b/docs/index.md index 01e1eec..29ae1fc 100644 --- a/docs/index.md +++ b/docs/index.md @@ -15,6 +15,7 @@ getting_started.md :maxdepth: 1 :caption: DSO CLI +command_reference.md CHANGELOG.md contributing.md ``` diff --git a/pyproject.toml b/pyproject.toml index dbb7042..61976ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ dependencies = [ optional-dependencies.dev = [ "hatch", "pre-commit" ] optional-dependencies.doc = [ + "click-extra[sphinx]", "docutils>=0.8,!=0.18.*,!=0.19.*", # For notebooks "ipykernel", @@ -52,7 +53,6 @@ optional-dependencies.doc = [ "sphinx-book-theme>=1", "sphinx-copybutton", "sphinxcontrib-bibtex>=1", - "sphinxcontrib-programoutput>=0.18", "sphinxext-opengraph", ] optional-dependencies.test = [ @@ -67,7 +67,7 @@ optional-dependencies.test = [ urls.Documentation = "https://github.com/Boehringer-Ingelheim/dso" urls.Home-page = "https://github.com/Boehringer-Ingelheim/dso" urls.Source = "https://github.com/Boehringer-Ingelheim/dso" -scripts.dso = "dso.cli:cli" +scripts.dso = "dso.cli:dso" [tool.hatch.version] source = "vcs" diff --git a/src/dso/cli/__init__.py b/src/dso/cli/__init__.py index ddf8897..bd60b7e 100644 --- a/src/dso/cli/__init__.py +++ b/src/dso/cli/__init__.py @@ -15,15 +15,15 @@ from dso._metadata import __version__ from dso._util import get_project_root, get_template_path, instantiate_with_repo -from ._create import create_cli -from ._exec import exec_cli +from ._create import dso_create +from ._exec import dso_exec click.rich_click.USE_MARKDOWN = True @click.command(name="compile-config") @click.argument("args", nargs=-1) -def compile_config_cli(args): +def dso_compile_config(args): """Compile params.in.yaml into params.yaml using Jinja2 templating and resolving recursive templates. If passing no arguments, configs will be resolved for the current working directory (i.e. all parent configs, @@ -58,7 +58,7 @@ def compile_config_cli(args): @click.argument( "stage", ) -def get_config_cli(stage, all, skip_compile): +def dso_get_config(stage, all, skip_compile): """Get the configuration for a given stage and print it to STDOUT in yaml format. The path to the stage must be relative to the root dir of the project. @@ -85,7 +85,7 @@ def get_config_cli(stage, all, skip_compile): @click.command( "init", ) -def init_cli(name: str | None = None, description: str | None = None): +def dso_init(name: str | None = None, description: str | None = None): """ Initialize a new project. A project can contain several stages organized in arbitrary subdirectories. @@ -122,7 +122,7 @@ def init_cli(name: str | None = None, description: str | None = None): is_flag=True, ) @click.argument("args", nargs=-1) -def lint_cli(args, skip_compile: bool = False): +def dso_lint(args, skip_compile: bool = False): """Lint a dso project Performs consistency checks according to a set of rules. @@ -149,7 +149,7 @@ def lint_cli(args, skip_compile: bool = False): context_settings={"ignore_unknown_options": True}, ) @click.argument("args", nargs=-1, type=click.UNPROCESSED) -def repro_cli(args): +def dso_repro(args): """Wrapper around dvc repro, compiling configuration before running.""" from dso._compile_config import compile_all_configs from dso._util import check_ask_pre_commit @@ -176,7 +176,7 @@ def repro_cli(args): @click.option("--font_outline", type=int) @click.option("--font_color", help="Use RGBA (e.g. `#AAAAAA88`) to specify transparency") @click.option("--font_outline_color", help="Use RGBA (e.g. `#AAAAAA88`) to specify transparency") -def watermark_cli(input_image, output_image, text, **kwargs): +def dso_watermark(input_image, output_image, text, **kwargs): """Add a watermark to an image To be called from the dso-r package for implementing a custom graphics device. @@ -211,7 +211,7 @@ def watermark_cli(input_image, output_image, text, **kwargs): is_flag=True, ) @click.version_option(version=__version__, prog_name="dso") -def cli(quiet: int, verbose: bool): +def dso(quiet: int, verbose: bool): """Root command""" if quiet >= 2: log.setLevel(logging.ERROR) @@ -224,11 +224,11 @@ def cli(quiet: int, verbose: bool): os.environ["DSO_VERBOSE"] = "1" -cli.add_command(create_cli) -cli.add_command(init_cli) -cli.add_command(compile_config_cli) -cli.add_command(repro_cli) -cli.add_command(exec_cli) -cli.add_command(lint_cli) -cli.add_command(get_config_cli) -cli.add_command(watermark_cli) +dso.add_command(dso_create) +dso.add_command(dso_init) +dso.add_command(dso_compile_config) +dso.add_command(dso_repro) +dso.add_command(dso_exec) +dso.add_command(dso_lint) +dso.add_command(dso_get_config) +dso.add_command(dso_watermark) diff --git a/src/dso/cli/_create.py b/src/dso/cli/_create.py index 6eda77b..a3d2368 100644 --- a/src/dso/cli/_create.py +++ b/src/dso/cli/_create.py @@ -33,7 +33,7 @@ @click.option("--template", type=click.Choice(list(STAGE_TEMPLATES))) @click.argument("name", required=False) @click.command("stage", help=CREATE_STAGE_HELP_TEXT) -def create_stage_cli(name: str | None = None, template: str | None = None, description: str | None = None): +def dso_create_stage(name: str | None = None, template: str | None = None, description: str | None = None): """Create a new stage.""" import questionary @@ -71,7 +71,7 @@ def create_stage_cli(name: str | None = None, template: str | None = None, descr @click.argument("name", required=False) @click.command("folder") -def create_folder_cli(name: str | None = None): +def dso_create_folder(name: str | None = None): """Create a new folder. A folder can contain subfolders or stages. Technically, nothing prevents you from just using `mkdir`. This command additionally adds some default @@ -102,10 +102,10 @@ def create_folder_cli(name: str | None = None): @click.group(name="create") -def create_cli(): +def dso_create(): """Create stage folder structure subcommand.""" pass -create_cli.add_command(create_stage_cli) -create_cli.add_command(create_folder_cli) +dso_create.add_command(dso_create_stage) +dso_create.add_command(dso_create_folder) diff --git a/src/dso/cli/_exec.py b/src/dso/cli/_exec.py index ca65090..36ae163 100644 --- a/src/dso/cli/_exec.py +++ b/src/dso/cli/_exec.py @@ -16,7 +16,7 @@ default=bool(int(os.environ.get("DSO_SKIP_COMPILE", 0))), is_flag=True, ) -def exec_quarto_cli(stage: str, skip_compile: bool = True): +def dso_exec_quarto(stage: str, skip_compile: bool = True): """ Render a quarto stage. Quarto parameters are inherited from params.yaml @@ -84,9 +84,9 @@ def exec_quarto_cli(stage: str, skip_compile: bool = True): @click.group(name="exec") -def exec_cli(): +def dso_exec(): """Dso wrappers around various tools""" pass -exec_cli.add_command(exec_quarto_cli) +dso_exec.add_command(dso_exec_quarto) diff --git a/tests/conftest.py b/tests/conftest.py index c37adc0..7a629f9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,7 +6,7 @@ from pytest import fixture from dso._compile_config import compile_all_configs -from dso.cli import create_cli, init_cli +from dso.cli import dso_create, dso_init TESTDATA = Path(__file__).parent / "data" @@ -17,7 +17,7 @@ def dso_project(tmp_path) -> Path: runner = CliRunner() proj_name = "dso_project" chdir(tmp_path) - runner.invoke(init_cli, [proj_name, "--description", "a test project"]) + runner.invoke(dso_init, [proj_name, "--description", "a test project"]) return tmp_path / proj_name @@ -31,7 +31,7 @@ def quarto_stage(dso_project) -> Path: runner = CliRunner() stage_name = "quarto_stage" chdir(dso_project) - runner.invoke(create_cli, ["stage", stage_name, "--template", "quarto", "--description", "a quarto stage"]) + runner.invoke(dso_create, ["stage", stage_name, "--template", "quarto", "--description", "a quarto stage"]) with (Path(stage_name) / "src" / f"{stage_name}.qmd").open("w") as f: f.write( dedent( diff --git a/tests/test_cli.py b/tests/test_cli.py index 3268691..1506687 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,9 +1,9 @@ from click.testing import CliRunner -from dso.cli import cli +from dso.cli import dso def test_root_command(): runner = CliRunner() - result = runner.invoke(cli) + result = runner.invoke(dso) assert result.exit_code == 0 diff --git a/tests/test_compile_config.py b/tests/test_compile_config.py index d1bf3de..0387cc5 100644 --- a/tests/test_compile_config.py +++ b/tests/test_compile_config.py @@ -14,7 +14,7 @@ _get_parent_configs, _load_yaml_with_auto_adjusting_paths, ) -from dso.cli import compile_config_cli +from dso.cli import dso_compile_config def _setup_yaml_configs(tmp_path, configs: dict[str, dict]): @@ -94,7 +94,7 @@ def test_auto_adjusting_path_with_jinja(tmp_path, test_yaml, expected): with test_file.open("w") as f: f.write(dedent(test_yaml)) - result = runner.invoke(compile_config_cli, []) + result = runner.invoke(dso_compile_config, []) print(result.output) td = Path(td) assert result.exit_code == 0 @@ -115,7 +115,7 @@ def test_compile_configs(tmp_path): "A/B/C/params.in.yaml": {"value": "C", "jinja2": "{{ only_root }}", "list": [5]}, }, ) - result = runner.invoke(compile_config_cli, []) + result = runner.invoke(dso_compile_config, []) print(result.output) td = Path(td) assert result.exit_code == 0 @@ -140,7 +140,7 @@ def test_compile_configs_null_override(tmp_path): "A/B/params.in.yaml": {"str": None, "list": None, "dict": None, "null": None}, }, ) - result = runner.invoke(compile_config_cli, []) + result = runner.invoke(dso_compile_config, []) print(result.output) td = Path(td) assert result.exit_code == 0 diff --git a/tests/test_create.py b/tests/test_create.py index 9db93ee..68fcfd7 100644 --- a/tests/test_create.py +++ b/tests/test_create.py @@ -4,7 +4,7 @@ import pytest from click.testing import CliRunner -from dso.cli import create_cli +from dso.cli import dso_create @pytest.mark.parametrize("template", ["bash", "quarto"]) @@ -12,7 +12,7 @@ def test_create_stage(dso_project, template): runner = CliRunner() chdir(dso_project) result = runner.invoke( - create_cli, ["stage", "teststage", "--template", template, "--description", "testdescription"] + dso_create, ["stage", "teststage", "--template", template, "--description", "testdescription"] ) print(result.output) assert result.exit_code == 0 @@ -31,7 +31,7 @@ def test_create_stage(dso_project, template): def test_create_folder(dso_project): runner = CliRunner() chdir(dso_project) - result = runner.invoke(create_cli, ["folder", "testfolder"]) + result = runner.invoke(dso_create, ["folder", "testfolder"]) print(result.output) assert result.exit_code == 0 assert (dso_project / "testfolder").is_dir() @@ -48,7 +48,7 @@ def test_create_folder_existing_dir(dso_project): (dso_project / "testfolder").mkdir() (dso_project / "testfolder" / "dvc.yaml").touch() chdir(dso_project) - result = runner.invoke(create_cli, ["folder", "testfolder"], input="y") + result = runner.invoke(dso_create, ["folder", "testfolder"], input="y") print(result.output) assert result.exit_code == 0 assert (dso_project / "testfolder").is_dir() diff --git a/tests/test_exec.py b/tests/test_exec.py index d7a2efe..2e0cacd 100644 --- a/tests/test_exec.py +++ b/tests/test_exec.py @@ -4,7 +4,7 @@ import pytest from click.testing import CliRunner -from dso.cli import exec_cli +from dso.cli import dso_exec @pytest.mark.parametrize("quiet", [None, "2"]) @@ -21,7 +21,7 @@ def test_exec_quarto(quarto_stage, quiet, launch_dir): if quiet is not None: os.environ["DSO_QUIET"] = quiet - result = runner.invoke(exec_cli, ["quarto", stage_path]) + result = runner.invoke(dso_exec, ["quarto", stage_path]) assert result.exit_code == 0 assert (quarto_stage / "report" / "quarto_stage.html").is_file() assert "Hello World!" in (quarto_stage / "report" / "quarto_stage.html").read_text() @@ -32,7 +32,7 @@ def test_exec_quarto_empty_params(quarto_stage_empty_configs): chdir(quarto_stage_empty_configs) stage_path = "." - result = runner.invoke(exec_cli, ["quarto", stage_path]) + result = runner.invoke(dso_exec, ["quarto", stage_path]) assert result.exit_code == 0 assert (quarto_stage_empty_configs / "report" / "quarto_stage.html").is_file() assert "Hello World!" in (quarto_stage_empty_configs / "report" / "quarto_stage.html").read_text() @@ -43,7 +43,7 @@ def test_exec_quarto_bibliography(quarto_stage_bibliography): chdir(quarto_stage_bibliography) stage_path = "." - result = runner.invoke(exec_cli, ["quarto", stage_path]) + result = runner.invoke(dso_exec, ["quarto", stage_path]) assert result.exit_code == 0 assert (quarto_stage_bibliography / "report" / "quarto_stage.html").is_file() assert "Knuth" in (quarto_stage_bibliography / "report" / "quarto_stage.html").read_text() @@ -54,7 +54,7 @@ def test_exec_quarto_stylesheet(quarto_stage_css): chdir(quarto_stage_css) stage_path = "." - result = runner.invoke(exec_cli, ["quarto", stage_path]) + result = runner.invoke(dso_exec, ["quarto", stage_path]) assert result.exit_code == 0 assert (quarto_stage_css / "report" / "quarto_stage.html").is_file() assert "h2.veryspecialclass1" in (quarto_stage_css / "report" / "quarto_stage.html").read_text() diff --git a/tests/test_get_config.py b/tests/test_get_config.py index 173e337..d797d99 100644 --- a/tests/test_get_config.py +++ b/tests/test_get_config.py @@ -5,7 +5,7 @@ from click.testing import CliRunner from dso._get_config import _filter_nested_dict, get_config -from dso.cli import get_config_cli +from dso.cli import dso_get_config @pytest.mark.parametrize( @@ -200,6 +200,6 @@ def test_get_config_invalid_stage(dso_project): def test_get_config_cli(quarto_stage): runner = CliRunner() chdir(quarto_stage) - result = runner.invoke(get_config_cli, ["quarto_stage"]) + result = runner.invoke(dso_get_config, ["quarto_stage"]) assert result.exit_code == 0 assert "quarto:" in result.output diff --git a/tests/test_init.py b/tests/test_init.py index 0fa13e6..6b7ad77 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -4,13 +4,13 @@ from click.testing import CliRunner -from dso.cli import init_cli +from dso.cli import dso_init def test_init(tmp_path): runner = CliRunner() with runner.isolated_filesystem(temp_dir=tmp_path) as td: - result = runner.invoke(init_cli, ["testproject", "--description", "testdescription"]) + result = runner.invoke(dso_init, ["testproject", "--description", "testdescription"]) print(result.output) td = Path(td) assert result.exit_code == 0 @@ -29,7 +29,7 @@ def test_init_existing_dir(dso_project): (dso_project / "README.md").unlink() rmtree(dso_project / ".git") - result = runner.invoke(init_cli, [str(dso_project), "--description", "testdescription"], input="y") + result = runner.invoke(dso_init, [str(dso_project), "--description", "testdescription"], input="y") assert result.exit_code == 0 assert (dso_project).is_dir() assert (dso_project / ".git").is_dir() diff --git a/tests/test_lint.py b/tests/test_lint.py index 3b86b66..88f32af 100644 --- a/tests/test_lint.py +++ b/tests/test_lint.py @@ -6,7 +6,7 @@ from click.testing import CliRunner from dso._lint import DSO001, DSOLinter, LintError, QuartoRule, Rule -from dso.cli import lint_cli +from dso.cli import dso_lint @pytest.mark.parametrize( @@ -204,6 +204,6 @@ def check(cls, file): def test_lint_cli(dso_project, paths): runner = CliRunner() chdir(dso_project) # proj root - result = runner.invoke(lint_cli, paths) + result = runner.invoke(dso_lint, paths) print(result.output) assert result.exit_code == 0 diff --git a/tests/test_pandocfilter.py b/tests/test_pandocfilter.py index b0fe088..efe19e3 100644 --- a/tests/test_pandocfilter.py +++ b/tests/test_pandocfilter.py @@ -5,7 +5,7 @@ from click.testing import CliRunner from dso._quarto import render_quarto -from dso.cli import exec_cli +from dso.cli import dso_exec from tests.conftest import TESTDATA @@ -105,7 +105,7 @@ def test_override_config(quarto_stage): chdir(quarto_stage) stage_path = "." - result = runner.invoke(exec_cli, ["quarto", stage_path]) + result = runner.invoke(dso_exec, ["quarto", stage_path]) assert result.exit_code == 0 out_html = (quarto_stage / "report" / "quarto_stage.html").read_text() diff --git a/tests/test_watermark.py b/tests/test_watermark.py index aa12aec..975e8b7 100644 --- a/tests/test_watermark.py +++ b/tests/test_watermark.py @@ -5,7 +5,7 @@ from PIL import Image from dso._watermark import PDFWatermarker, SVGWatermarker, Watermarker -from dso.cli import watermark_cli +from dso.cli import dso_watermark from tests.conftest import TESTDATA @@ -67,6 +67,6 @@ def test_add_watermark_cli(tmp_path, params): test_image = _get_test_image(tmp_path, format="png", size=(500, 500)) test_image_out = tmp_path / "test_image_out.png" - result = runner.invoke(watermark_cli, [str(test_image), str(test_image_out), "--text", "test text", *params]) + result = runner.invoke(dso_watermark, [str(test_image), str(test_image_out), "--text", "test text", *params]) assert result.exit_code == 0 assert test_image_out.is_file()