diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 3e113c8..c4e9e82 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -1,4 +1,4 @@ -name: Tests +name: Run tests on: push: @@ -24,7 +24,7 @@ jobs: - name: Install dependencies run: | pip install --upgrade pip - pip install -e . --group test + pip install . --group test - name: Run tests run: pytest diff --git a/src/clickdump/__init__.py b/src/clickdump/__init__.py index 5c7618a..a3a8a2a 100644 --- a/src/clickdump/__init__.py +++ b/src/clickdump/__init__.py @@ -30,13 +30,13 @@ def cli(name, verbose, files): __version__ = "0.1.0" __all__ = [ - "dump", - "dumps", + "ActionInfo", "ActionType", - "TypeInfo", + "ArgumentGroup", "FileTypeInfo", - "ActionInfo", "MutualExclusionGroup", - "ArgumentGroup", "ParserInfo", + "TypeInfo", + "dump", + "dumps", ] diff --git a/src/clickdump/_serializer.py b/src/clickdump/_serializer.py index 97adaaf..450b0e7 100644 --- a/src/clickdump/_serializer.py +++ b/src/clickdump/_serializer.py @@ -6,7 +6,7 @@ import platform from dataclasses import MISSING, fields, is_dataclass from enum import Enum -from typing import Any, Dict, List, Optional +from typing import Any import click @@ -125,7 +125,7 @@ def _extract_param_info(param: click.Parameter) -> ActionInfo: return info -def _create_help_action(cmd: click.Command) -> Optional[ActionInfo]: +def _create_help_action(cmd: click.Command) -> ActionInfo | None: """Create a synthetic HELP action for auto-generated --help.""" if not getattr(cmd, "add_help_option", True): return None @@ -149,8 +149,8 @@ def _extract_command_actions(cmd: click.Command, include_hidden: bool = True) -> Returns: Tuple of (actions_list, argument_groups_list, mutex_groups_list) """ - actions: List[ActionInfo] = [] - action_to_dest: Dict[int, str] = {} + actions: list[ActionInfo] = [] + action_to_dest: dict[int, str] = {} params = list(cmd.params) @@ -171,7 +171,7 @@ def _extract_command_actions(cmd: click.Command, include_hidden: bool = True) -> def _create_parser_info( - cmd: click.Command, *, prog_override: Optional[str] = None + cmd: click.Command, *, prog_override: str | None = None ) -> ParserInfo: """Create base ParserInfo from a Click Command.""" info = ParserInfo( @@ -206,7 +206,7 @@ def serialize_command( cmd: click.Command, include_hidden: bool = True, *, - prog_override: Optional[str] = None, + prog_override: str | None = None, ) -> ParserInfo: """Serialize a click Command to ParserInfo.""" info = _create_parser_info(cmd, prog_override=prog_override) @@ -219,7 +219,7 @@ def serialize_group( group: click.Group, include_hidden: bool = True, *, - prog_override: Optional[str] = None, + prog_override: str | None = None, ) -> ParserInfo: """Serialize a click Group to ParserInfo. @@ -243,10 +243,10 @@ def _build_subparsers_action( group: click.Group, include_hidden: bool = True ) -> ActionInfo: """Build a synthetic PARSERS action for a Group's subcommands.""" - subparsers: Dict[str, Any] = {} - subparsers_aliases: Dict[str, List[str]] = {} - serialized_parsers: Dict[int, str] = {} - parser_to_names: Dict[int, List[str]] = {} + subparsers: dict[str, Any] = {} + subparsers_aliases: dict[str, list[str]] = {} + serialized_parsers: dict[int, str] = {} + parser_to_names: dict[int, list[str]] = {} for name, cmd in group.commands.items(): if cmd.name is None: @@ -301,7 +301,7 @@ def _get_clickdump_version() -> str: return "unknown" -def _get_environment_info() -> Dict[str, str]: +def _get_environment_info() -> dict[str, str]: return { "python_version": platform.python_version(), "python_implementation": platform.python_implementation(), @@ -313,20 +313,17 @@ def _get_environment_info() -> Dict[str, str]: def _extract_help_option_names(cmd: click.Command) -> None: - """Populate HELP_OPTION_NAMES if a context is available, otherwise use defaults.""" + """Populate HELP_OPTION_NAMES with the command's actual help option names.""" global _HELP_OPTION_NAMES - try: - ctx = click.Context(cmd) - _HELP_OPTION_NAMES = list(ctx.help_option_names) - except Exception: - pass + ctx = cmd.make_context(cmd.name or "", [], resilient_parsing=True) + _HELP_OPTION_NAMES = list(ctx.help_option_names) def _serialize( cmd: click.Command, include_hidden: bool = True, *, - prog_override: Optional[str] = None, + prog_override: str | None = None, ) -> ParserInfo: """Serialize any click Command or Group.""" _extract_help_option_names(cmd) @@ -395,15 +392,13 @@ def default(self, o: Any) -> Any: return serialize_value(o) -def _find_command_path( - root: click.Command, target: click.Command -) -> Optional[List[str]]: +def _find_command_path(root: click.Command, target: click.Command) -> list[str] | None: """Search root's command tree for target, returning the full name path. Returns list of names from root to target (e.g. ["cli", "config", "get"]), or None if target is not found under root. """ - stack: List[tuple] = [(root, [getattr(root, "name", "")])] + stack: list[tuple] = [(root, [getattr(root, "name", "")])] while stack: node, path = stack.pop() @@ -412,7 +407,7 @@ def _find_command_path( return path commands = getattr(node, "commands", {}) - for name, cmd in commands.items(): + for cmd in commands.values(): if cmd.name is None: continue stack.append((cmd, [*path, cmd.name])) @@ -425,9 +420,9 @@ def dump( *, include_env: bool = True, include_hidden: bool = True, - parent: Optional[click.Command] = None, - prog: Optional[str] = None, -) -> Dict[str, Any]: + parent: click.Command | None = None, + prog: str | None = None, +) -> dict[str, Any]: """Serialize a click Command or Group to a dictionary. Args: @@ -450,9 +445,9 @@ def dump( path[0] = prog prog = " ".join(path) info = _serialize(cmd, include_hidden=include_hidden, prog_override=prog) - data: Dict[str, Any] = json.loads(json.dumps(info, cls=_Encoder)) + data: dict[str, Any] = json.loads(json.dumps(info, cls=_Encoder)) - result: Dict[str, Any] = {"$schema": SCHEMA_URL_V1} + result: dict[str, Any] = {"$schema": SCHEMA_URL_V1} if include_env: result["$env"] = _get_environment_info() @@ -466,8 +461,8 @@ def dumps( *, include_env: bool = True, include_hidden: bool = True, - parent: Optional[click.Command] = None, - prog: Optional[str] = None, + parent: click.Command | None = None, + prog: str | None = None, **json_kwargs: Any, ) -> str: """Serialize a click Command or Group to a JSON string. diff --git a/src/clickdump/_values.py b/src/clickdump/_values.py index bc69900..5a9b2d4 100644 --- a/src/clickdump/_values.py +++ b/src/clickdump/_values.py @@ -4,10 +4,10 @@ import base64 from enum import Enum -from typing import Any, Dict, FrozenSet, List, Optional, Set +from typing import Any -def serialize_value(value: Any, _seen: Optional[Set[int]] = None) -> Any: +def serialize_value(value: Any, _seen: set[int] | None = None) -> Any: """Serialize a value for JSON compatibility.""" if _seen is None: _seen = set() @@ -53,7 +53,7 @@ def serialize_value(value: Any, _seen: Optional[Set[int]] = None) -> Any: } -def _serialize_sequence(value: Any, seen: Set[int], obj_id: int) -> List[Any]: +def _serialize_sequence(value: Any, seen: set[int], obj_id: int) -> list[Any]: seen.add(obj_id) result = [serialize_value(v, seen) for v in value] seen.discard(obj_id) @@ -61,8 +61,8 @@ def _serialize_sequence(value: Any, seen: Set[int], obj_id: int) -> List[Any]: def _serialize_dict( - value: Dict[Any, Any], seen: Set[int], obj_id: int -) -> Dict[str, Any]: + value: dict[Any, Any], seen: set[int], obj_id: int +) -> dict[str, Any]: seen.add(obj_id) result = {str(k): serialize_value(v, seen) for k, v in value.items()} seen.discard(obj_id) @@ -70,10 +70,10 @@ def _serialize_dict( def _serialize_set( - value: Set[Any], seen: Set[int], obj_id: int -) -> Dict[str, List[Any]]: + value: set[Any], seen: set[int], obj_id: int +) -> dict[str, list[Any]]: seen.add(obj_id) - result: Dict[str, List[Any]] = { + result: dict[str, list[Any]] = { "__set__": [serialize_value(v, seen) for v in sorted(value, key=str)] } seen.discard(obj_id) @@ -81,17 +81,17 @@ def _serialize_set( def _serialize_frozenset( - value: FrozenSet[Any], seen: Set[int], obj_id: int -) -> Dict[str, List[Any]]: + value: frozenset[Any], seen: set[int], obj_id: int +) -> dict[str, list[Any]]: seen.add(obj_id) - result: Dict[str, List[Any]] = { + result: dict[str, list[Any]] = { "__frozenset__": [serialize_value(v, seen) for v in sorted(value, key=str)] } seen.discard(obj_id) return result -def _serialize_enum(value: Enum) -> Dict[str, Any]: +def _serialize_enum(value: Enum) -> dict[str, Any]: return { "__enum__": True, "class": type(value).__name__, @@ -101,7 +101,7 @@ def _serialize_enum(value: Enum) -> Dict[str, Any]: } -def _serialize_bytes(value: bytes) -> Dict[str, str]: +def _serialize_bytes(value: bytes) -> dict[str, str]: try: return {"__bytes__": value.decode("utf-8")} except UnicodeDecodeError: diff --git a/src/clickdump/models.py b/src/clickdump/models.py index a8e9f94..e746287 100644 --- a/src/clickdump/models.py +++ b/src/clickdump/models.py @@ -4,7 +4,7 @@ from dataclasses import dataclass, field from enum import Enum -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any class ActionType(str, Enum): @@ -25,7 +25,7 @@ class ActionType(str, Enum): UNKNOWN = "unknown" @classmethod - def from_string(cls, value: str) -> "ActionType": + def from_string(cls, value: str) -> ActionType: try: return cls(value) except ValueError: @@ -37,7 +37,7 @@ class TypeInfo: """Type converter information.""" name: str - module: Optional[str] = None + module: str | None = None builtin: bool = False serializable: bool = True @@ -48,42 +48,42 @@ class FileTypeInfo: mode: str = "r" bufsize: int = -1 - encoding: Optional[str] = None - errors: Optional[str] = None + encoding: str | None = None + errors: str | None = None @dataclass class ActionInfo: """Serialized Click Parameter (Option or Argument).""" - option_strings: List[str] + option_strings: list[str] dest: str action_type: ActionType - nargs: Union[str, int, None] = None + nargs: str | int | None = None const: Any = None default: Any = None - type_info: Optional[TypeInfo] = None - file_type_info: Optional[FileTypeInfo] = None - choices: Optional[List[Any]] = None + type_info: TypeInfo | None = None + file_type_info: FileTypeInfo | None = None + choices: list[Any] | None = None required: bool = False - help: Optional[str] = None - metavar: Union[str, Tuple[str, ...], None] = None + help: str | None = None + metavar: str | tuple[str, ...] | None = None deprecated: bool = False - version: Optional[str] = None - subparsers: Optional[Dict[str, "ParserInfo"]] = None - subparsers_title: Optional[str] = None - subparsers_description: Optional[str] = None - subparsers_dest: Optional[str] = None + version: str | None = None + subparsers: dict[str, ParserInfo] | None = None + subparsers_title: str | None = None + subparsers_description: str | None = None + subparsers_dest: str | None = None subparsers_required: bool = False - subparsers_aliases: Optional[Dict[str, List[str]]] = None - custom_action_class: Optional[str] = None + subparsers_aliases: dict[str, list[str]] | None = None + custom_action_class: str | None = None # Click-specific extensions hidden: bool = False - show_default: Optional[Union[bool, str]] = None + show_default: bool | str | None = None show_envvar: bool = False - prompt: Optional[Union[bool, str]] = None - envvar: Optional[Union[str, List[str]]] = None + prompt: bool | str | None = None + envvar: str | list[str] | None = None is_eager: bool = False expose_value: bool = True count: bool = False @@ -105,54 +105,54 @@ class MutualExclusionGroup: """Mutually exclusive argument group.""" required: bool - actions: List[str] + actions: list[str] @dataclass class ArgumentGroup: """Argument group for help organization.""" - title: Optional[str] - description: Optional[str] - actions: List[str] + title: str | None + description: str | None + actions: list[str] @dataclass class ParserInfo: """Complete serialized Click Command or Group.""" - prog: Optional[str] = None - description: Optional[str] = None - epilog: Optional[str] = None - usage: Optional[str] = None + prog: str | None = None + description: str | None = None + epilog: str | None = None + usage: str | None = None add_help: bool = True allow_abbrev: bool = True - formatter_class: Optional[str] = None + formatter_class: str | None = None prefix_chars: str = "-" - fromfile_prefix_chars: Optional[str] = None + fromfile_prefix_chars: str | None = None argument_default: Any = None conflict_handler: str = "error" exit_on_error: bool = True suggest_on_error: bool = False color: bool = True - actions: List[ActionInfo] = field(default_factory=list) - argument_groups: List[ArgumentGroup] = field(default_factory=list) - mutually_exclusive_groups: List[MutualExclusionGroup] = field(default_factory=list) + actions: list[ActionInfo] = field(default_factory=list) + argument_groups: list[ArgumentGroup] = field(default_factory=list) + mutually_exclusive_groups: list[MutualExclusionGroup] = field(default_factory=list) # Click-specific extensions - short_help: Optional[str] = None + short_help: str | None = None hidden: bool = False deprecated: bool = False no_args_is_help: bool = False invoke_without_command: bool = False chain: bool = False - subcommand_metavar: Optional[str] = None + subcommand_metavar: str | None = None allow_extra_args: bool = False allow_interspersed_args: bool = True ignore_unknown_options: bool = False - def get_action_by_dest(self, dest: str) -> Optional[ActionInfo]: + def get_action_by_dest(self, dest: str) -> ActionInfo | None: for action in self.actions: if action.dest == dest: return action diff --git a/tests/conftest.py b/tests/conftest.py index ffef80c..386cafa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,6 +7,7 @@ import click import pytest +from factories import make_command, make_group SCHEMA_PATH = Path(__file__).parent / "schema" / "schema-v1.json" @@ -57,27 +58,19 @@ def cli(count, pi, flag, switch, color, path, num, uid, since, ratio, point, inp @pytest.fixture def command_with_envvar(): """Command with envvar.""" - - @click.command() - @click.option("--host", envvar="HOST", default="localhost") - @click.option("--port", envvar="PORT", type=int, default=8080) - def cli(host, port): - """Command with env vars.""" - - return cli + return make_command( + click.Option(["--host"], envvar="HOST", default="localhost"), + click.Option(["--port"], envvar="PORT", type=int, default=8080), + ) @pytest.fixture def command_with_hidden(): """Command with hidden option.""" - - @click.command() - @click.option("--visible", help="I am visible") - @click.option("--hidden", hidden=True, help="I am hidden") - def cli(visible, hidden): - """Command with hidden options.""" - - return cli + return make_command( + click.Option(["--visible"], help="I am visible"), + click.Option(["--hidden"], hidden=True, help="I am hidden"), + ) @pytest.fixture @@ -132,207 +125,31 @@ def set(value): @pytest.fixture def command_no_help(): """Command with add_help_option=False.""" - - @click.command(add_help_option=False) - def cli(): - """No help.""" - - return cli + return make_command(add_help_option=False) @pytest.fixture def deprecated_command(): """Command with deprecated=True.""" - - @click.command(deprecated=True) - def cli(): - """Deprecated command.""" - - return cli - - -@pytest.fixture -def invoke_without_command_group(): - """Group with invoke_without_command=True.""" - - @click.group(invoke_without_command=True) - def cli(): - """Group.""" - - @cli.command() - def sub(): - """Sub.""" - - return cli + return make_command(deprecated=True) @pytest.fixture def chain_group(): """Group with chain=True.""" - - @click.group(chain=True) - def cli(): - """Chain group.""" - - @cli.command() - def step_a(): - """Step A.""" - - @cli.command() - def step_b(): - """Step B.""" - - return cli - - -@pytest.fixture -def metavar_group(): - """Group with subcommand_metavar.""" - - @click.group(subcommand_metavar="COMMAND") - def cli(): - """Group.""" - - @cli.command() - def sub(): - """Sub.""" - - return cli - - -@pytest.fixture -def empty_group(): - """Group with no subcommands.""" - - @click.group() - def cli(): - """Empty group.""" - - return cli + return make_group("step_a", "step_b", chain=True) @pytest.fixture def command_prompt_true(): """Command with prompt=True.""" - - @click.command() - @click.option("--name", prompt=True) - def cli(name): - """Prompt.""" - - return cli - - -@pytest.fixture -def command_prompt_string(): - """Command with prompt string.""" - - @click.command() - @click.option("--name", prompt="Enter name: ") - def cli(name): - """Prompt.""" - - return cli - - -@pytest.fixture -def command_show_default(): - """Command with show_default=True.""" - - @click.command() - @click.option("--output", default="out.txt", show_default=True) - def cli(output): - """Show default.""" - - return cli - - -@pytest.fixture -def command_show_envvar(): - """Command with show_envvar=True.""" - - @click.command() - @click.option("--host", envvar="HOST", show_envvar=True) - def cli(host): - """Show envvar.""" - - return cli - - -@pytest.fixture -def command_is_eager(): - """Command with is_eager=True.""" - - @click.command() - @click.option("--verbose", is_eager=True) - def cli(verbose): - """Eager.""" - - return cli - - -@pytest.fixture -def command_expose_false(): - """Command with expose_value=False.""" - - @click.command() - @click.option("--secret", expose_value=False) - def cli(secret): - """Hidden dest.""" - - return cli + return make_command(click.Option(["--name"], prompt=True)) @pytest.fixture def command_required(): """Command with required=True.""" - - @click.command() - @click.option("--token", required=True) - def cli(token): - """Required.""" - - return cli - - -@pytest.fixture -def command_metavar(): - """Command with metavar.""" - - @click.command() - @click.option("--config", metavar="FILE") - def cli(config): - """Metavar.""" - - return cli - - -@pytest.fixture -def command_envvar_list(): - """Command with envvar list.""" - - @click.command() - @click.option("--host", envvar=["HOST", "SERVER_HOST"]) - def cli(host): - """Envvar list.""" - - return cli - - -@pytest.fixture -def group_with_aliases(): - """Group with aliased commands.""" - - build_cmd = click.Command("build", help="Build it.") - - @click.group() - def cli(): - """CLI.""" - - cli.add_command(build_cmd, "build") - cli.add_command(build_cmd, "compile") - - return cli + return make_command(click.Option(["--token"], required=True)) @pytest.fixture @@ -357,9 +174,4 @@ def visible(): @pytest.fixture def empty_command(): """Command with no parameters.""" - - @click.command() - def cli(): - """Empty.""" - - return cli + return make_command() diff --git a/tests/factories.py b/tests/factories.py new file mode 100644 index 0000000..610545a --- /dev/null +++ b/tests/factories.py @@ -0,0 +1,16 @@ +"""Factories for building click commands and groups in tests.""" + +import click + + +def make_command(*params, **command_kwargs): + """Build a click Command from option/argument specs.""" + return click.Command("cli", params=list(params), **command_kwargs) + + +def make_group(*subcommands, **group_kwargs): + """Build a click Group from subcommand names.""" + group = click.Group("cli", **group_kwargs) + for name in subcommands: + group.add_command(click.Command(name)) + return group diff --git a/tests/test_schema.py b/tests/test_schema.py index 544ef49..5c7bfb3 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -3,9 +3,10 @@ from __future__ import annotations import pytest -import clickdump from jsonschema import Draft202012Validator +import clickdump + class TestSchemaValidates: @pytest.mark.parametrize( diff --git a/tests/test_serialization.py b/tests/test_serialization.py index 4761820..4078735 100644 --- a/tests/test_serialization.py +++ b/tests/test_serialization.py @@ -5,6 +5,8 @@ import json import click +from factories import make_command, make_group + import clickdump @@ -78,7 +80,7 @@ class TestTypes: def test_int_type(self, command_with_types): result = clickdump.dump(command_with_types) actions = result["actions"] - count = [a for a in actions if a["dest"] == "count"][0] + count = next(a for a in actions if a["dest"] == "count") assert count["type_info"]["name"] == "int" assert count["type_info"]["module"] == "builtins" assert count["type_info"]["builtin"] is True @@ -86,34 +88,34 @@ def test_int_type(self, command_with_types): def test_float_type(self, command_with_types): result = clickdump.dump(command_with_types) actions = result["actions"] - pi = [a for a in actions if a["dest"] == "pi"][0] + pi = next(a for a in actions if a["dest"] == "pi") assert pi["type_info"]["name"] == "float" def test_boolean_flag(self, command_with_types): result = clickdump.dump(command_with_types) actions = result["actions"] - flag = [a for a in actions if a["dest"] == "flag"][0] + flag = next(a for a in actions if a["dest"] == "flag") assert flag["action_type"] == "boolean_optional" assert flag["default"] is True def test_is_flag_option(self, command_with_types): result = clickdump.dump(command_with_types) actions = result["actions"] - switch = [a for a in actions if a["dest"] == "switch"][0] + switch = next(a for a in actions if a["dest"] == "switch") assert switch["action_type"] == "store_true" assert switch["default"] is False def test_choice_type(self, command_with_types): result = clickdump.dump(command_with_types) actions = result["actions"] - color = [a for a in actions if a["dest"] == "color"][0] + color = next(a for a in actions if a["dest"] == "color") assert color["type_info"]["name"] == "choice" assert color["choices"] == ["red", "green", "blue"] def test_path_type(self, command_with_types): result = clickdump.dump(command_with_types) actions = result["actions"] - path = [a for a in actions if a["dest"] == "path"][0] + path = next(a for a in actions if a["dest"] == "path") assert path["type_info"]["name"] == "Path" assert path["type_info"]["module"] == "pathlib" @@ -122,7 +124,7 @@ class TestEnvvar: def test_envvar_option(self, command_with_envvar): result = clickdump.dump(command_with_envvar) actions = result["actions"] - host = [a for a in actions if a["dest"] == "host"][0] + host = next(a for a in actions if a["dest"] == "host") assert host["envvar"] == "HOST" @@ -159,7 +161,7 @@ def test_group_has_subparsers(self, simple_group): def test_subcommand_has_params(self, simple_group): result = clickdump.dump(simple_group) actions = result["actions"] - parsers = [a for a in actions if a["action_type"] == "parsers"][0] + parsers = next(a for a in actions if a["action_type"] == "parsers") build = parsers["subparsers"]["build"] assert build["prog"] == "build" assert build["description"] == "Build the project." @@ -171,7 +173,7 @@ def test_subcommand_has_params(self, simple_group): def test_nested_group_subparsers(self, nested_group): result = clickdump.dump(nested_group) actions = result["actions"] - parsers = [a for a in actions if a["action_type"] == "parsers"][0] + parsers = next(a for a in actions if a["action_type"] == "parsers") assert "config" in parsers["subparsers"] config = parsers["subparsers"]["config"] config_actions = config["actions"] @@ -205,24 +207,19 @@ def test_dumps_indent(self, simple_command): class TestEdgeCases: - def test_command_with_no_params(self): - @click.command() - def simple(): - """Simple command.""" - - result = clickdump.dump(simple) - assert result["prog"] == "simple" + def test_command_with_no_params(self, empty_command): + result = clickdump.dump(empty_command) + assert result["prog"] == "cli" assert len(result["actions"]) == 1 # just help def test_command_with_multiple_options(self): - @click.command() - @click.option("--a", multiple=True, type=int) - @click.option("--b", is_flag=True) - @click.option("--c", flag_value="custom") - def cli(a, b, c): - """Multi options.""" - - result = clickdump.dump(cli) + result = clickdump.dump( + make_command( + click.Option(["--a"], multiple=True, type=int), + click.Option(["--b"], is_flag=True), + click.Option(["--c"], flag_value="custom"), + ) + ) actions = {a["dest"]: a for a in result["actions"]} assert actions["a"]["action_type"] == "append" assert actions["a"]["multiple"] is True @@ -275,15 +272,11 @@ def test_prog_overrides_root_only(self, nested_group): result = clickdump.dump(get, parent=nested_group, prog="mycli") assert result["prog"] == "mycli config get" - def test_cmd_not_found_falls_back(self, nested_group): + def test_cmd_not_found_falls_back(self, nested_group, empty_command): config = nested_group.commands["config"] - @click.command() - def standalone(): - pass - - # standalone is not under config, so parent lookup fails -> falls back to prog - result = clickdump.dump(standalone, parent=config, prog="fallback") + # empty_command is not under config, so parent lookup fails -> falls back to prog + result = clickdump.dump(empty_command, parent=config, prog="fallback") assert result["prog"] == "fallback" def test_prog_overrides_root_when_parent_found(self, nested_group): @@ -311,6 +304,20 @@ def test_add_help_false_parser_info(self, command_no_help): assert result["add_help"] is False +class TestHelpOptionNames: + def test_custom_help_option_names(self): + result = clickdump.dump( + make_command(context_settings={"help_option_names": ["-h", "--help"]}) + ) + help_actions = [a for a in result["actions"] if a["action_type"] == "help"] + assert help_actions[0]["option_strings"] == ["-h", "--help"] + + def test_required_positional_arg_serializes(self): + result = clickdump.dump(make_command(click.Argument(["src"]))) + actions = {a["dest"]: a for a in result["actions"]} + assert actions["src"]["required"] is True + + class TestCommandDeprecated: def test_deprecated(self, deprecated_command): result = clickdump.dump(deprecated_command) @@ -318,20 +325,20 @@ def test_deprecated(self, deprecated_command): class TestGroupFlags: - def test_invoke_without_command(self, invoke_without_command_group): - result = clickdump.dump(invoke_without_command_group) + def test_invoke_without_command(self): + result = clickdump.dump(make_group("sub", invoke_without_command=True)) assert result["invoke_without_command"] is True def test_chain(self, chain_group): result = clickdump.dump(chain_group) assert result["chain"] is True - def test_subcommand_metavar(self, metavar_group): - result = clickdump.dump(metavar_group) + def test_subcommand_metavar(self): + result = clickdump.dump(make_group("sub", subcommand_metavar="COMMAND")) assert result["subcommand_metavar"] == "COMMAND" - def test_group_no_subcommands(self, empty_group): - result = clickdump.dump(empty_group) + def test_group_no_subcommands(self): + result = clickdump.dump(make_group()) parsers = [a for a in result["actions"] if a["action_type"] == "parsers"] assert len(parsers) == 0 @@ -339,54 +346,75 @@ def test_group_no_subcommands(self, empty_group): class TestOptionFields: def test_prompt_true(self, command_prompt_true): result = clickdump.dump(command_prompt_true) - name_action = [a for a in result["actions"] if a["dest"] == "name"][0] + name_action = next(a for a in result["actions"] if a["dest"] == "name") assert name_action["prompt"] == "Name" - def test_prompt_string(self, command_prompt_string): - result = clickdump.dump(command_prompt_string) - name_action = [a for a in result["actions"] if a["dest"] == "name"][0] + def test_prompt_string(self): + result = clickdump.dump( + make_command(click.Option(["--name"], prompt="Enter name: ")) + ) + name_action = next(a for a in result["actions"] if a["dest"] == "name") assert name_action["prompt"] == "Enter name: " - def test_show_default(self, command_show_default): - result = clickdump.dump(command_show_default) - output_action = [a for a in result["actions"] if a["dest"] == "output"][0] + def test_show_default(self): + result = clickdump.dump( + make_command( + click.Option(["--output"], default="out.txt", show_default=True) + ) + ) + output_action = next(a for a in result["actions"] if a["dest"] == "output") assert output_action["show_default"] is True - def test_show_envvar(self, command_show_envvar): - result = clickdump.dump(command_show_envvar) - host_action = [a for a in result["actions"] if a["dest"] == "host"][0] + def test_show_envvar(self): + result = clickdump.dump( + make_command(click.Option(["--host"], envvar="HOST", show_envvar=True)) + ) + host_action = next(a for a in result["actions"] if a["dest"] == "host") assert host_action["show_envvar"] is True - def test_is_eager(self, command_is_eager): - result = clickdump.dump(command_is_eager) - verbose_action = [a for a in result["actions"] if a["dest"] == "verbose"][0] + def test_is_eager(self): + result = clickdump.dump( + make_command(click.Option(["--verbose"], is_eager=True)) + ) + verbose_action = next(a for a in result["actions"] if a["dest"] == "verbose") assert verbose_action["is_eager"] is True - def test_expose_value_false(self, command_expose_false): - result = clickdump.dump(command_expose_false) - secret_action = [a for a in result["actions"] if a["dest"] == "secret"][0] + def test_expose_value_false(self): + result = clickdump.dump( + make_command(click.Option(["--secret"], expose_value=False)) + ) + secret_action = next(a for a in result["actions"] if a["dest"] == "secret") assert secret_action["expose_value"] is False def test_required(self, command_required): result = clickdump.dump(command_required) - token_action = [a for a in result["actions"] if a["dest"] == "token"][0] + token_action = next(a for a in result["actions"] if a["dest"] == "token") assert token_action["required"] is True - def test_metavar(self, command_metavar): - result = clickdump.dump(command_metavar) - config_action = [a for a in result["actions"] if a["dest"] == "config"][0] + def test_metavar(self): + result = clickdump.dump( + make_command(click.Option(["--config"], metavar="FILE")) + ) + config_action = next(a for a in result["actions"] if a["dest"] == "config") assert config_action["metavar"] == "FILE" - def test_envvar_list(self, command_envvar_list): - result = clickdump.dump(command_envvar_list) - host_action = [a for a in result["actions"] if a["dest"] == "host"][0] + def test_envvar_list(self): + result = clickdump.dump( + make_command(click.Option(["--host"], envvar=["HOST", "SERVER_HOST"])) + ) + host_action = next(a for a in result["actions"] if a["dest"] == "host") assert host_action["envvar"] == ["HOST", "SERVER_HOST"] class TestCommandAliases: - def test_aliases(self, group_with_aliases): - result = clickdump.dump(group_with_aliases) - parsers = [a for a in result["actions"] if a["action_type"] == "parsers"][0] + def test_aliases(self): + build_cmd = click.Command("build", help="Build it.") + cli = click.Group("cli") + cli.add_command(build_cmd, "build") + cli.add_command(build_cmd, "compile") + + result = clickdump.dump(cli) + parsers = next(a for a in result["actions"] if a["action_type"] == "parsers") assert "build" in parsers["subparsers"] assert "compile" not in parsers["subparsers"] assert parsers["subparsers_aliases"] == {"build": ["compile"]} @@ -395,11 +423,11 @@ def test_aliases(self, group_with_aliases): class TestHiddenSubcommand: def test_hidden_subcommand_omitted(self, group_with_hidden_subcommand): result = clickdump.dump(group_with_hidden_subcommand, include_hidden=False) - parsers = [a for a in result["actions"] if a["action_type"] == "parsers"][0] + parsers = next(a for a in result["actions"] if a["action_type"] == "parsers") assert "secret" not in parsers["subparsers"] assert "visible" in parsers["subparsers"] def test_hidden_subcommand_included_by_default(self, group_with_hidden_subcommand): result = clickdump.dump(group_with_hidden_subcommand) - parsers = [a for a in result["actions"] if a["action_type"] == "parsers"][0] + parsers = next(a for a in result["actions"] if a["action_type"] == "parsers") assert "secret" in parsers["subparsers"] diff --git a/tests/test_types.py b/tests/test_types.py index edab868..53dfebb 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -68,7 +68,7 @@ def test_file_defaults(self): assert file_type_info.mode == "r" def test_file_custom(self): - type_info, file_type_info, _ = type_info_from_param_type( + _, file_type_info, _ = type_info_from_param_type( click.File(mode="rb", encoding="utf-8") ) assert file_type_info.mode == "rb" diff --git a/tests/test_values.py b/tests/test_values.py index 6ce16a0..71a1269 100644 --- a/tests/test_values.py +++ b/tests/test_values.py @@ -12,7 +12,7 @@ class Color(Enum): RED = "red" - GREEN = [1, 2] + GREEN = (1, 2) class TestPrimitives: