From 8af133f77c609ba28bd7c1c1d3217f48f31827c1 Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Sun, 6 Apr 2025 15:10:45 -0400 Subject: [PATCH] Broaden support for core classes --- CHANGELOG.md | 10 +++++++++ mkdocs_click/_docs.py | 28 +++++++++++++++++++------ mkdocs_click/_extension.py | 2 +- mkdocs_click/_loader.py | 6 +++--- tests/app/cli.py | 6 +++--- tests/test_docs.py | 42 +++++++++++++++++++------------------- tests/test_extension.py | 12 +++++------ 7 files changed, 66 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d68172..a3b56ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## Unreleased + +### Added + +- Add support for `click.Command`-like, `click.Group`-like and `click.Context`-like objects without requiring them to be actual subclasses. (Pull #82) + +### Fixed + +- Remove explicit reference to `click.BaseCommand` and `click.MultiCommand` objects in anticipation of their deprecation. (Pull #82) + ## 0.8.1 - 2023-09-18 ### Fixed diff --git a/mkdocs_click/_docs.py b/mkdocs_click/_docs.py index d9a26bc..b5a6cf3 100644 --- a/mkdocs_click/_docs.py +++ b/mkdocs_click/_docs.py @@ -15,7 +15,7 @@ def make_command_docs( prog_name: str, - command: click.BaseCommand, + command: click.Command, depth: int = 0, style: str = "plain", remove_ascii_art: bool = False, @@ -42,7 +42,7 @@ def make_command_docs( def _recursively_make_command_docs( prog_name: str, - command: click.BaseCommand, + command: click.Command, parent: click.Context | None = None, depth: int = 0, style: str = "plain", @@ -90,20 +90,26 @@ def _recursively_make_command_docs( def _build_command_context( - prog_name: str, command: click.BaseCommand, parent: click.Context | None + prog_name: str, command: click.Command, parent: click.Context | None ) -> click.Context: - return click.Context(cast(click.Command, command), info_name=prog_name, parent=parent) + return _get_context_class(command)( + cast(click.Command, command), info_name=prog_name, parent=parent + ) -def _get_sub_commands(command: click.Command, ctx: click.Context) -> list[click.Command]: +def _get_sub_commands( + command: click.Command | click.Group, ctx: click.Context +) -> list[click.Command]: """Return subcommands of a Click command.""" subcommands = getattr(command, "commands", {}) if subcommands: return list(subcommands.values()) - if not isinstance(command, click.MultiCommand): + if not _is_command_group(command): return [] + command = cast(click.Group, command) + subcommands = [] for name in command.list_commands(ctx): @@ -360,3 +366,13 @@ def _make_subcommands_links( help_string = "*No description was provided with this command.*" yield f"- *{command_bullet}*: {help_string}" yield "" + + +def _get_context_class(command: click.Command) -> type[click.Context]: + # https://github.com/pallets/click/blob/8.1.8/src/click/core.py#L859-L862 + return command.context_class + + +def _is_command_group(command: click.Command) -> bool: + # https://github.com/pallets/click/blob/8.1.8/src/click/core.py#L1806-L1811 + return isinstance(command, click.Group) or hasattr(command, "command_class") diff --git a/mkdocs_click/_extension.py b/mkdocs_click/_extension.py index 60f1e07..e98d472 100644 --- a/mkdocs_click/_extension.py +++ b/mkdocs_click/_extension.py @@ -22,7 +22,7 @@ def replace_command_docs(has_attr_list: bool = False, **options: Any) -> Iterato module = options["module"] command = options["command"] - prog_name = options.get("prog_name", None) + prog_name = options.get("prog_name") depth = int(options.get("depth", 0)) style = options.get("style", "plain") remove_ascii_art = options.get("remove_ascii_art", False) diff --git a/mkdocs_click/_loader.py b/mkdocs_click/_loader.py index 1e95d95..ecf0131 100644 --- a/mkdocs_click/_loader.py +++ b/mkdocs_click/_loader.py @@ -11,15 +11,15 @@ from ._exceptions import MkDocsClickException -def load_command(module: str, attribute: str) -> click.BaseCommand: +def load_command(module: str, attribute: str) -> click.Command: """ Load and return the Click command object located at ':'. """ command = _load_obj(module, attribute) - if not isinstance(command, click.BaseCommand): + if not (isinstance(command, click.Command) or hasattr(command, "context_class")): raise MkDocsClickException( - f"{attribute!r} must be a 'click.BaseCommand' object, got {type(command)}" + f"{attribute!r} must be a 'click.Command'-like object, got {type(command)}" ) return command diff --git a/tests/app/cli.py b/tests/app/cli.py index 38dbc97..411f5e2 100644 --- a/tests/app/cli.py +++ b/tests/app/cli.py @@ -50,7 +50,7 @@ def hidden(): cli_named.add_command(hidden) -class MultiCLI(click.MultiCommand): +class GroupCLI(click.Group): def list_commands(self, ctx): return ["foo", "bar"] @@ -59,5 +59,5 @@ def get_command(self, ctx, name): return cmds.get(name) -multi_named = MultiCLI(name="multi", help="Main entrypoint for this dummy program") -multi = MultiCLI(help="Main entrypoint for this dummy program") +group_named = GroupCLI(name="group", help="Main entrypoint for this dummy program") +group = GroupCLI(help="Main entrypoint for this dummy program") diff --git a/tests/test_docs.py b/tests/test_docs.py index fb25587..56ce5f3 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -175,7 +175,7 @@ def test_style_table(command, expected): assert output == expected -class MultiCLI(click.MultiCommand): +class GroupCLI(click.Group): def list_commands(self, ctx): return ["single-command"] @@ -184,26 +184,26 @@ def get_command(self, ctx, name): @pytest.mark.parametrize( - "multi", + "group", [ - pytest.param(MultiCLI("multi", help="Multi help"), id="explicit-name"), - pytest.param(MultiCLI(help="Multi help"), id="no-name"), + pytest.param(GroupCLI("group", help="Group help"), id="explicit-name"), + pytest.param(GroupCLI(help="Group help"), id="no-name"), ], ) -def test_custom_multicommand(multi): +def test_custom_group(group): """ - Custom `MultiCommand` objects are supported (i.e. not just `Group` multi-commands). + Custom `Group` objects are supported. """ expected = dedent( """ - # multi + # group - Multi help + Group help **Usage:** ```text - multi [OPTIONS] COMMAND [ARGS]... + group [OPTIONS] COMMAND [ARGS]... ``` **Options:** @@ -219,7 +219,7 @@ def test_custom_multicommand(multi): **Usage:** ```text - multi hello [OPTIONS] + group hello [OPTIONS] ``` **Options:** @@ -231,31 +231,31 @@ def test_custom_multicommand(multi): """ ).lstrip() - output = "\n".join(make_command_docs("multi", multi)) + output = "\n".join(make_command_docs("group", group)) assert output == expected @pytest.mark.parametrize( - "multi", + "group", [ - pytest.param(MultiCLI("multi", help="Multi help"), id="explicit-name"), - pytest.param(MultiCLI(help="Multi help"), id="no-name"), + pytest.param(GroupCLI("group", help="Group help"), id="explicit-name"), + pytest.param(GroupCLI(help="Group help"), id="no-name"), ], ) -def test_custom_multicommand_with_list_subcommands(multi): +def test_custom_group_with_list_subcommands(group): """ - Custom `MultiCommand` objects are supported (i.e. not just `Group` multi-commands). + Custom `Group` objects are supported. """ expected = dedent( """ - # multi + # group - Multi help + Group help **Usage:** ```text - multi [OPTIONS] COMMAND [ARGS]... + group [OPTIONS] COMMAND [ARGS]... ``` **Options:** @@ -275,7 +275,7 @@ def test_custom_multicommand_with_list_subcommands(multi): **Usage:** ```text - multi hello [OPTIONS] + group hello [OPTIONS] ``` **Options:** @@ -287,7 +287,7 @@ def test_custom_multicommand_with_list_subcommands(multi): """ ).lstrip() - output = "\n".join(make_command_docs("multi", multi, list_subcommands=True)) + output = "\n".join(make_command_docs("group", group, list_subcommands=True)) assert output == expected diff --git a/tests/test_extension.py b/tests/test_extension.py index 8ecf2c9..3f97672 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -20,8 +20,8 @@ [ pytest.param("cli", "cli", id="cli-simple"), pytest.param("cli_named", "cli", id="cli-explicit-name"), - pytest.param("multi_named", "multi", id="multi-explicit-name"), - pytest.param("multi", "multi", id="no-name"), + pytest.param("group_named", "group", id="group-explicit-name"), + pytest.param("group", "group", id="no-name"), ], ) def test_extension(command, expected_name): @@ -133,8 +133,8 @@ def test_enhanced_titles(): [ pytest.param("cli", "cli", id="cli-simple"), pytest.param("cli_named", "cli", id="cli-explicit-name"), - pytest.param("multi_named", "multi", id="multi-explicit-name"), - pytest.param("multi", "multi", id="no-name"), + pytest.param("group_named", "group", id="group-explicit-name"), + pytest.param("group", "group", id="no-name"), ], ) def test_extension_with_subcommand(command, expected_name): @@ -162,8 +162,8 @@ def test_extension_with_subcommand(command, expected_name): [ pytest.param("cli", "cli", id="cli-simple"), pytest.param("cli_named", "cli", id="cli-explicit-name"), - pytest.param("multi_named", "multi", id="multi-explicit-name"), - pytest.param("multi", "multi", id="no-name"), + pytest.param("group_named", "group", id="group-explicit-name"), + pytest.param("group", "group", id="no-name"), ], ) def test_enhanced_titles_with_subcommand(command, expected_name):