From 1f37fb4f9c16fcd0224f74e56d8ad64e6866ca53 Mon Sep 17 00:00:00 2001 From: Edwin Onuonga Date: Sat, 9 Dec 2023 19:01:52 +0000 Subject: [PATCH 1/2] add Group.__main__() support --- feud/_internal/_command.py | 81 ++--- feud/_internal/_metaclass.py | 25 +- feud/core/command.py | 60 ++-- feud/core/group.py | 28 +- feud/decorators.py | 2 +- pyproject.toml | 4 +- tests/unit/test_core/test_group.py | 465 ++++++++++++++++++++++++++--- 7 files changed, 536 insertions(+), 129 deletions(-) diff --git a/feud/_internal/_command.py b/feud/_internal/_command.py index 8081c25..f5ef2be 100644 --- a/feud/_internal/_command.py +++ b/feud/_internal/_command.py @@ -38,8 +38,8 @@ class ParameterSpec: class CommandState: config: Config click_kwargs: dict[str, t.Any] - context: bool is_group: bool + pass_context: bool = False # below keys are parameter name arguments: dict[str, ParameterSpec] = dataclasses.field( default_factory=dict @@ -55,45 +55,47 @@ def decorate(self: CommandState, func: t.Callable) -> click.Command: sensitive_vars: dict[str, bool] = {} params: list[click.Parameter] = [] - if self.is_group: - for param in self.overrides.values(): - params.append(param) # noqa: PERF402 - - command = func - else: - for i, param_name in enumerate(inspect.signature(func).parameters): - sensitive: bool = False - if self.context and i == 0: - continue - if param_name in self.overrides: - param: click.Parameter = self.overrides[param_name] - sensitive = param.hide_input - elif param_name in self.arguments: - spec = self.arguments[param_name] - spec.kwargs["type"] = _types.click.get_click_type( - spec.hint, config=self.config - ) - param = click.Argument(spec.args, **spec.kwargs) - elif param_name in self.options: - spec = self.options[param_name] - spec.kwargs["type"] = _types.click.get_click_type( - spec.hint, config=self.config - ) - param = click.Option(spec.args, **spec.kwargs) - meta_vars[param_name] = self.get_meta_var(param) - sensitive_vars[param_name] = sensitive + sig: inspect.signature = inspect.signature(func) + + for i, param_name in enumerate(sig.parameters): + sensitive: bool = False + if self.pass_context and i == 0: + continue + if param_name in self.overrides: + param: click.Parameter = self.overrides[param_name] + sensitive = param.hide_input + elif param_name in self.arguments: + spec = self.arguments[param_name] + spec.kwargs["type"] = _types.click.get_click_type( + spec.hint, config=self.config + ) + param = click.Argument(spec.args, **spec.kwargs) + elif param_name in self.options: + spec = self.options[param_name] + spec.kwargs["type"] = _types.click.get_click_type( + spec.hint, config=self.config + ) + param = click.Option(spec.args, **spec.kwargs) + meta_vars[param_name] = self.get_meta_var(param) + sensitive_vars[param_name] = sensitive + params.append(param) + + # add any overrides that don't appear in function signature + # e.g. version_option or anything else + for param_name, param in self.overrides.items(): + if param_name not in sig.parameters: params.append(param) - command = _decorators.validate_call( - func, - name=self.click_kwargs["name"], - meta_vars=meta_vars, - sensitive_vars=sensitive_vars, - pydantic_kwargs=self.config.pydantic_kwargs, - ) + command = _decorators.validate_call( + func, + name=self.click_kwargs["name"], + meta_vars=meta_vars, + sensitive_vars=sensitive_vars, + pydantic_kwargs=self.config.pydantic_kwargs, + ) - if self.context: - command = click.pass_context(command) + if self.pass_context: + command = click.pass_context(command) constructor = click.group if self.is_group else click.command command = constructor(**self.click_kwargs)(command) @@ -159,7 +161,7 @@ def get_alias(alias: str, *, hint: type, negate_flags: bool) -> str: def sanitize_click_kwargs( - click_kwargs: dict[str, t.Any], *, name: str + click_kwargs: dict[str, t.Any], *, name: str, help_: str | None = None ) -> None: """Sanitize click command/group arguments. @@ -170,3 +172,6 @@ def sanitize_click_kwargs( # sanitize the provided name # (only necessary for auto-naming a Group by class name) click_kwargs["name"] = click_kwargs.get("name", _inflect.sanitize(name)) + # set help if provided + if help_: + click_kwargs["help"] = help_ diff --git a/feud/_internal/_metaclass.py b/feud/_internal/_metaclass.py index 0952218..2e0bccb 100644 --- a/feud/_internal/_metaclass.py +++ b/feud/_internal/_metaclass.py @@ -20,7 +20,7 @@ class GroupBase(abc.ABCMeta): def __new__( - cls: type[GroupBase], + __cls: type[GroupBase], # noqa: N804 cls_name: str, bases: tuple[type, ...], namespace: dict[str, t.Any], @@ -57,7 +57,8 @@ def __new__( subgroups: list[type] = [] # type[Group], but circular import commands: list[str] = [] - # extend/inherit from parent group if subclassed + # extend/inherit information from parent group if subclassed + help_: str | None = None for base in bases: if config := getattr(base, "__feud_config__", None): # NOTE: may want **dict(config) depending on behaviour @@ -79,6 +80,7 @@ def __new__( for cmd in base.__feud_commands__ if cmd not in commands ] + help_ = base.__feud_click_kwargs__.get("help") # deconstruct base config, override config kwargs and click kwargs config_kwargs: dict[str, t.Any] = {} @@ -97,7 +99,9 @@ def __new__( d[k] = v # sanitize click kwargs - _command.sanitize_click_kwargs(click_kwargs, name=cls_name) + _command.sanitize_click_kwargs( + click_kwargs, name=cls_name, help_=help_ + ) # members to consider as commands funcs = { @@ -124,4 +128,17 @@ def __new__( func, config=namespace["__feud_config__"] ) - return super().__new__(cls, cls_name, bases, namespace) + group = super().__new__(__cls, cls_name, bases, namespace) + + if bases: + # use class-level docstring as help if provided + if doc := group.__doc__: + click_kwargs["help"] = doc + # use __main__ function-level docstring as help if provided + if doc := group.__main__.__doc__: + click_kwargs["help"] = doc + # use class-level click kwargs help if provided + if doc := kwargs.get("help"): + click_kwargs["help"] = doc + + return group diff --git a/feud/core/command.py b/feud/core/command.py index 7ec4225..987ff9c 100644 --- a/feud/core/command.py +++ b/feud/core/command.py @@ -114,39 +114,24 @@ def decorate(__func: typing.Callable, /) -> typing.Callable: return decorate(func) if func else decorate -def get_command( - func: typing.Callable, - /, - *, - config: Config, - click_kwargs: dict[str, typing.Any], -) -> click.Command: - if isinstance(func, staticmethod): - func = func.__func__ +def build_command_state( + state: _command.CommandState, *, func: callable, config: Config +) -> None: + doc: docstring_parser.Docstring + if state.is_group: + doc = docstring_parser.parse(state.click_kwargs.get("help", "")) + else: + doc = docstring_parser.parse_from_object(func) - doc: docstring_parser.Docstring = docstring_parser.parse_from_object(func) sig: inspect.Signature = inspect.signature(func) - pass_context: bool = _command.pass_context(sig) - - state = _command.CommandState( - config=config, - click_kwargs=click_kwargs, - context=pass_context, - is_group=False, - aliases=getattr(func, "__feud_aliases__", {}), - overrides={ - override.name: override - for override in getattr(func, "__click_params__", []) - }, - ) for param, spec in sig.parameters.items(): meta = _command.ParameterSpec() meta.hint: type = spec.annotation - if pass_context and param == _command.CONTEXT_PARAM: + if _command.pass_context(sig) and param == _command.CONTEXT_PARAM: # skip handling for click.Context argument - continue + state.pass_context = True if spec.kind in (spec.POSITIONAL_ONLY, spec.POSITIONAL_OR_KEYWORD): # function positional arguments correspond to CLI arguments @@ -231,6 +216,31 @@ def get_command( elif meta.type == _command.ParameterType.OPTION: state.options[param] = meta + +def get_command( + func: typing.Callable, + /, + *, + config: Config, + click_kwargs: dict[str, typing.Any], +) -> click.Command: + if isinstance(func, staticmethod): + func = func.__func__ + + state = _command.CommandState( + config=config, + click_kwargs=click_kwargs, + is_group=False, + aliases=getattr(func, "__feud_aliases__", {}), + overrides={ + override.name: override + for override in getattr(func, "__click_params__", []) + }, + ) + + # construct command state from signature + build_command_state(state, func=func, config=config) + # generate click.Command and attach original function reference command = state.decorate(func) command.__func__ = func diff --git a/feud/core/group.py b/feud/core/group.py index 7052824..57fe5b0 100644 --- a/feud/core/group.py +++ b/feud/core/group.py @@ -16,14 +16,11 @@ import pydantic as pyd -try: - import rich_click as click -except ImportError: - import click - import feud.exceptions +from feud import click from feud._internal import _command, _metaclass from feud.config import Config +from feud.core.command import build_command_state __all__ = ["Group", "compile"] @@ -421,6 +418,9 @@ def deregister( # deregister all subgroups cls.__feud_subgroups__ = [] + def __main__() -> None: # noqa: D105 + pass + @pyd.validate_call(config=pyd.ConfigDict(arbitrary_types_allowed=True)) def compile(group: type[Group], /) -> click.Group: # noqa: A001 @@ -448,21 +448,23 @@ def compile(group: type[Group], /) -> click.Group: # noqa: A001 def get_group(__cls: type[Group], /) -> click.Group: + func: callable = __cls.__main__ + state = _command.CommandState( config=__cls.__feud_config__, click_kwargs=__cls.__feud_click_kwargs__, - context=False, is_group=True, - aliases=getattr(__cls, "__feud_aliases__", {}), + aliases=getattr(func, "__feud_aliases__", {}), overrides={ override.name: override - for override in getattr(__cls, "__click_params__", []) + for override in getattr(func, "__click_params__", []) }, ) - def wrapper() -> None: - pass - - wrapper.__doc__ = __cls.__doc__ + # construct command state from signature + build_command_state(state, func=func, config=__cls.__feud_config__) - return state.decorate(wrapper) + # generate click.Group and attach original function reference + command = state.decorate(func) + command.__func__ = func + return command diff --git a/feud/decorators.py b/feud/decorators.py index 342d8a5..29a0603 100644 --- a/feud/decorators.py +++ b/feud/decorators.py @@ -99,7 +99,7 @@ def decorator( received = { p.name for p in sig.parameters.values() if p.kind == p.KEYWORD_ONLY } - if specified > received: + if len(specified - received) > 0: msg = ( f"Arguments provided to 'alias' decorator must " f"also be keyword parameters for function {f.__name__!r}. " diff --git a/pyproject.toml b/pyproject.toml index 43b995a..099d503 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -169,11 +169,11 @@ allow-star-arg-any = true "__init__.py" = ["PLC0414", "F403", "F401", "F405"] "feud/typing/*.py" = ["PLC0414", "F403", "F401"] "tests/**/*.py" = ["D100", "D100", "D101", "D102", "D103", "D104"] # temporary -"tests/**/test_*.py" = ["ARG001", "S101"] +"tests/**/test_*.py" = ["ARG001", "S101", "D", "FA100"] [tool.pydoclint] style = "numpy" -exclude = ".git|.tox|feud/_internal" # temporary +exclude = ".git|.tox|feud/_internal|tests" # temporary check-return-types = false arg-type-hints-in-docstring = false quiet = true diff --git a/tests/unit/test_core/test_group.py b/tests/unit/test_core/test_group.py index d5a33b6..16c3b63 100644 --- a/tests/unit/test_core/test_group.py +++ b/tests/unit/test_core/test_group.py @@ -3,11 +3,10 @@ # SPDX-License-Identifier: MIT # This source code is part of the Feud project (https://feud.wiki). -from __future__ import annotations - import typing as t from collections import OrderedDict from operator import itemgetter +from pathlib import Path import pytest @@ -16,7 +15,12 @@ def assert_help( - __obj: feud.Group | click.Group | click.Command | t.Callable, + __obj: t.Union[ + feud.Group, + click.Group, + click.Command, + t.Callable, + ], /, *, capsys: pytest.CaptureFixture, @@ -157,18 +161,6 @@ def g(*, arg1: int) -> None: ) -def test_click_version(capsys: pytest.CaptureFixture) -> None: - @click.version_option(version="0.1.0") - class Test(feud.Group): - pass - - with pytest.raises(SystemExit): - Test(["--version"]) - - out, _ = capsys.readouterr() - assert out.strip() == "pytest, version 0.1.0" - - def test_config_kwarg_propagation() -> None: class Test(feud.Group, show_help_defaults=False): def f(*, arg1: int = 1) -> None: @@ -235,17 +227,17 @@ def f(*, arg1: int = 1) -> None: def test_subgroups_parent_single_child() -> None: class Parent(feud.Group): - """This is the parent group.""" # noqa: D404 + """This is the parent group.""" def f(*, arg: int) -> None: - """This is a command in the parent group.""" # noqa: D401, D404 + """This is a command in the parent group.""" return arg class Child(feud.Group): - """This is a subgroup.""" # noqa: D404 + """This is a subgroup.""" def g(*, arg: int) -> None: - """This is a command in the subgroup.""" # noqa: D401, D404 + """This is a command in the subgroup.""" return arg Parent.register(Child) @@ -276,24 +268,24 @@ def test_subgroups_parent_multi_children() -> None: """ class Parent(feud.Group): - """This is the parent group.""" # noqa: D404 + """This is the parent group.""" def f(*, arg: int) -> int: - """This is a command in the parent group.""" # noqa: D401, D404 + """This is a command in the parent group.""" return arg class Child1(feud.Group): - """This is the first subgroup.""" # noqa: D404 + """This is the first subgroup.""" def g(*, arg: int) -> int: - """This is a command in the first subgroup.""" # noqa: D401, D404 + """This is a command in the first subgroup.""" return arg class Child2(feud.Group): - """This is the second subgroup.""" # noqa: D404 + """This is the second subgroup.""" def h(*, arg: int) -> int: - """This is a command in the second subgroup.""" # noqa: D401, D404 + """This is a command in the second subgroup.""" return arg Parent.register([Child1, Child2]) @@ -339,38 +331,38 @@ def test_subgroups_nested() -> None: # noqa: PLR0915 """ class Parent(feud.Group): - """This is the parent group.""" # noqa: D404 + """This is the parent group.""" def f(*, arg: int) -> int: - """This is a command in the parent group.""" # noqa: D401, D404 + """This is a command in the parent group.""" return arg class Child1(feud.Group): - """This is the first subgroup.""" # noqa: D404 + """This is the first subgroup.""" def g(*, arg: int) -> int: - """This is a command in the first subgroup.""" # noqa: D401, D404 + """This is a command in the first subgroup.""" return arg class Child2(feud.Group): - """This is the second subgroup.""" # noqa: D404 + """This is the second subgroup.""" def h(*, arg: int) -> int: - """This is a command in the second subgroup.""" # noqa: D401, D404 + """This is a command in the second subgroup.""" return arg class Child3(feud.Group): - """This is the third subgroup.""" # noqa: D404 + """This is the third subgroup.""" def i(*, arg: int) -> int: - """This is a command in the third subgroup.""" # noqa: D401, D404 + """This is a command in the third subgroup.""" return arg class Child4(feud.Group): - """This is the fourth subgroup.""" # noqa: D404 + """This is the fourth subgroup.""" def j(*, arg: int) -> int: - """This is a command in the fourth subgroup.""" # noqa: D401, D404 + """This is a command in the fourth subgroup.""" return arg Parent.register(Child1) @@ -492,31 +484,31 @@ def j(*, arg: int) -> int: def test_deregister_nested() -> None: # noqa: PLR0915 class Parent(feud.Group): - """This is the parent group.""" # noqa: D404 + """This is the parent group.""" def f(*, arg: int) -> int: - """This is a command in the parent group.""" # noqa: D401, D404 + """This is a command in the parent group.""" return arg class Child1(feud.Group): - """This is the first subgroup.""" # noqa: D404 + """This is the first subgroup.""" def g(*, arg: int) -> int: - """This is a command in the first subgroup.""" # noqa: D401, D404 + """This is a command in the first subgroup.""" return arg class Child2(feud.Group): - """This is the second subgroup.""" # noqa: D404 + """This is the second subgroup.""" def h(*, arg: int) -> int: - """This is a command in the second subgroup.""" # noqa: D401, D404 + """This is a command in the second subgroup.""" return arg class Child3(feud.Group): - """This is the third subgroup.""" # noqa: D404 + """This is the third subgroup.""" def i(*, arg: int) -> int: - """This is a command in the third subgroup.""" # noqa: D401, D404 + """This is a command in the third subgroup.""" return arg Parent.register([Child1, Child2]) @@ -788,15 +780,15 @@ class Parent( show_help_defaults=False, epilog="Visit https://www.com for more information.", ): - """This is the parent group.""" # noqa: D404 + """This is the parent group.""" def f(*, arg: int) -> int: - """This is a command in the parent group.""" # noqa: D401, D404 + """This is a command in the parent group.""" return arg class Subgroup(feud.Group): def g(*, arg: int) -> int: - """This is a command in a subgroup.""" # noqa: D401, D404 + """This is a command in a subgroup.""" return arg Parent.register(Subgroup) @@ -1002,3 +994,384 @@ class F(feud.Group): (D, D.descendants()), ] ) + + +def test_help_no_docstring(capsys: pytest.CaptureFixture) -> None: + class Test(feud.Group): + pass + + assert_help( + Test, + capsys=capsys, + expected=""" +Usage: pytest [OPTIONS] COMMAND [ARGS]... + +Options: + --help Show this message and exit. + """, + ) + + +def test_help_simple_docstring(capsys: pytest.CaptureFixture) -> None: + class Test(feud.Group): + """This is a group.""" + + assert_help( + Test, + capsys=capsys, + expected=""" +Usage: pytest [OPTIONS] COMMAND [ARGS]... + + This is a group. + +Options: + --help Show this message and exit. + """, + ) + + +def test_help_param_docstring(capsys: pytest.CaptureFixture) -> None: + class Test(feud.Group): + """This is a group.\f + + Parameters + ---------- + param: + Help for a parameter. + """ + + assert_help( + Test, + capsys=capsys, + expected=""" +Usage: pytest [OPTIONS] COMMAND [ARGS]... + + This is a group. + +Options: + --help Show this message and exit. + """, + ) + + +def test_help_override(capsys: pytest.CaptureFixture) -> None: + class Test(feud.Group, help="Overridden."): + """This is a group.""" + + assert_help( + Test, + capsys=capsys, + expected=""" +Usage: pytest [OPTIONS] COMMAND [ARGS]... + + Overridden. + +Options: + --help Show this message and exit. + """, + ) + + +def test_main_help_no_docstrings(capsys: pytest.CaptureFixture) -> None: + class Test(feud.Group): + def __main__() -> None: + pass + + assert_help( + Test, + capsys=capsys, + expected=""" +Usage: pytest [OPTIONS] COMMAND [ARGS]... + +Options: + --help Show this message and exit. + """, + ) + + +def test_main_help_class_docstring(capsys: pytest.CaptureFixture) -> None: + class Test(feud.Group): + """This is a class-level docstring.""" + + def __main__() -> None: + pass + + assert_help( + Test, + capsys=capsys, + expected=""" +Usage: pytest [OPTIONS] COMMAND [ARGS]... + + This is a class-level docstring. + +Options: + --help Show this message and exit. + """, + ) + + +def test_main_help_function_docstring(capsys: pytest.CaptureFixture) -> None: + class Test(feud.Group): + def __main__() -> None: + """This is a function-level docstring.""" + + assert_help( + Test, + capsys=capsys, + expected=""" +Usage: pytest [OPTIONS] COMMAND [ARGS]... + + This is a function-level docstring. + +Options: + --help Show this message and exit. + """, + ) + + +def test_main_help_both_docstrings(capsys: pytest.CaptureFixture) -> None: + class Test(feud.Group): + """This is a class-level docstring.""" + + def __main__() -> None: + """This is a function-level docstring.""" + + assert_help( + Test, + capsys=capsys, + expected=""" +Usage: pytest [OPTIONS] COMMAND [ARGS]... + + This is a function-level docstring. + +Options: + --help Show this message and exit. + """, + ) + + +def test_main_help_both_docstrings_with_override( + capsys: pytest.CaptureFixture +) -> None: + class Test(feud.Group, help="Overridden."): + """This is a class-level docstring.""" + + def __main__() -> None: + """This is a function-level docstring.""" + + assert_help( + Test, + capsys=capsys, + expected=""" +Usage: pytest [OPTIONS] COMMAND [ARGS]... + + Overridden. + +Options: + --help Show this message and exit. + """, + ) + + +def test_main(capsys: pytest.CaptureFixture) -> None: + class Test(feud.Group, invoke_without_command=True): + """This group does something relative to a root directory.\f + + Parameters + ---------- + root: + Root directory + """ + + @staticmethod + @click.version_option("0.1.0") + @feud.alias(root="-r") + def __main__(ctx: click.Context, *, root: Path = Path(".")) -> None: + ctx.obj = {"root": root} + return root + + @staticmethod + def command(ctx: click.Context, path: Path) -> Path: + """Returns a full path.\f + + Parameters + ---------- + path: + Relative path. + """ + return ctx.obj["root"] / path + + group = feud.compile(Test) + + assert_help( + group, + capsys=capsys, + expected=""" +Usage: pytest [OPTIONS] COMMAND [ARGS]... + + This group does something relative to a root directory. + +Options: + -r, --root PATH Root directory [default: .] + --version Show the version and exit. + --help Show this message and exit. + +Commands: + command Returns a full path. + """, + ) + + # check version + with pytest.raises(SystemExit): + group(["--version"]) + out, _ = capsys.readouterr() + assert out.strip() == "pytest, version 0.1.0" + + # test invoke without command + assert group(["-r", "/usr"], standalone_mode=False) == Path("/usr") + + # test command context + assert group( + ["-r", "/usr", "command", "bin/sh"], + standalone_mode=False, + ) == Path("/usr/bin/sh") + + +def test_main_inheritance_no_docstring(capsys: pytest.CaptureFixture) -> None: + class Test(feud.Group, invoke_without_command=True): + """This group does something relative to a root directory.\f + + Parameters + ---------- + root: + Root directory + """ + + @staticmethod + @click.version_option("0.1.0") + @feud.alias(root="-r") + def __main__(ctx: click.Context, *, root: Path = Path(".")) -> None: + ctx.obj = {"root": root} + return root + + @staticmethod + def command(ctx: click.Context, path: Path) -> Path: + """Returns a full path.\f + + Parameters + ---------- + path: + Relative path. + """ + return ctx.obj["root"] / path + + class Child(Test): + pass + + group = feud.compile(Child) + + assert_help( + group, + capsys=capsys, + expected=""" +Usage: pytest [OPTIONS] COMMAND [ARGS]... + + This group does something relative to a root directory. + +Options: + -r, --root PATH Root directory [default: .] + --version Show the version and exit. + --help Show this message and exit. + +Commands: + command Returns a full path. + """, + ) + + # check version + with pytest.raises(SystemExit): + group(["--version"]) + out, _ = capsys.readouterr() + assert out.strip() == "pytest, version 0.1.0" + + # test invoke without command + assert group(["-r", "/usr"], standalone_mode=False) == Path("/usr") + + # test command context + assert group( + ["-r", "/usr", "command", "bin/sh"], + standalone_mode=False, + ) == Path("/usr/bin/sh") + + +def test_main_inheritance_docstring(capsys: pytest.CaptureFixture) -> None: + class Test(feud.Group, invoke_without_command=True): + """This group does something relative to a root directory.\f + + Parameters + ---------- + root: + Root directory + """ + + @staticmethod + @click.version_option("0.1.0") + @feud.alias(root="-r") + def __main__(ctx: click.Context, *, root: Path = Path(".")) -> None: + ctx.obj = {"root": root} + return root + + @staticmethod + def command(ctx: click.Context, path: Path) -> Path: + """Returns a full path.\f + + Parameters + ---------- + path: + Relative path. + """ + return ctx.obj["root"] / path + + class Child(Test): + """This is a new docstring.\f + + Parameters + ---------- + root: + Root directory + """ + + group = feud.compile(Child) + + assert_help( + group, + capsys=capsys, + expected=""" +Usage: pytest [OPTIONS] COMMAND [ARGS]... + + This is a new docstring. + +Options: + -r, --root PATH Root directory [default: .] + --version Show the version and exit. + --help Show this message and exit. + +Commands: + command Returns a full path. + """, + ) + + # check version + with pytest.raises(SystemExit): + group(["--version"]) + out, _ = capsys.readouterr() + assert out.strip() == "pytest, version 0.1.0" + + # test invoke without command + assert group(["-r", "/usr"], standalone_mode=False) == Path("/usr") + + # test command context + assert group( + ["-r", "/usr", "command", "bin/sh"], + standalone_mode=False, + ) == Path("/usr/bin/sh") From 3e16e14d61eecdc39e92838e6cdd6ecc49378159 Mon Sep 17 00:00:00 2001 From: Edwin Onuonga Date: Sat, 9 Dec 2023 19:05:50 +0000 Subject: [PATCH 2/2] remove unused noqa directives --- pyproject.toml | 2 +- tests/unit/test_core/test_command.py | 6 +++--- tests/unit/test_internal/test_decorators.py | 2 +- .../test_types/test_click/test_metavars.py | 14 +++++++------- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 099d503..66ca4b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -169,7 +169,7 @@ allow-star-arg-any = true "__init__.py" = ["PLC0414", "F403", "F401", "F405"] "feud/typing/*.py" = ["PLC0414", "F403", "F401"] "tests/**/*.py" = ["D100", "D100", "D101", "D102", "D103", "D104"] # temporary -"tests/**/test_*.py" = ["ARG001", "S101", "D", "FA100"] +"tests/**/test_*.py" = ["ARG001", "S101", "D", "FA100", "FA102"] [tool.pydoclint] style = "numpy" diff --git a/tests/unit/test_core/test_command.py b/tests/unit/test_core/test_command.py index d3fb3b9..47abbfd 100644 --- a/tests/unit/test_core/test_command.py +++ b/tests/unit/test_core/test_command.py @@ -72,7 +72,7 @@ def f(arg1: int, *, arg2: bool) -> None: arg2: Changes something. - """ # noqa: D301, D400 + """ with pytest.raises(SystemExit): f(["--help"]) @@ -196,7 +196,7 @@ def f(ctx: click.Context, *, arg1: bool) -> bool: def test_run_undecorated() -> None: - def f(arg1: int, *, arg2: bool) -> tuple[int, bool]: # noqa: FA102 + def f(arg1: int, *, arg2: bool) -> tuple[int, bool]: return arg1, arg2 assert feud.run(f, ["1", "--no-arg2"], standalone_mode=False) == (1, False) @@ -204,7 +204,7 @@ def f(arg1: int, *, arg2: bool) -> tuple[int, bool]: # noqa: FA102 def test_run_decorated() -> None: @feud.command - def f(arg1: int, *, arg2: bool) -> tuple[int, bool]: # noqa: FA102 + def f(arg1: int, *, arg2: bool) -> tuple[int, bool]: return arg1, arg2 assert feud.run(f, ["1", "--no-arg2"], standalone_mode=False) == (1, False) diff --git a/tests/unit/test_internal/test_decorators.py b/tests/unit/test_internal/test_decorators.py index 0c3654f..7f701f1 100644 --- a/tests/unit/test_internal/test_decorators.py +++ b/tests/unit/test_internal/test_decorators.py @@ -87,7 +87,7 @@ def test_validate_call_list() -> None: sensitive_vars = {"0": False} pydantic_kwargs = {} - def f(arg1: list[t.conint(multiple_of=2)]) -> None: # noqa: FA102 + def f(arg1: list[t.conint(multiple_of=2)]) -> None: pass with pytest.raises(click.UsageError) as e: diff --git a/tests/unit/test_internal/test_types/test_click/test_metavars.py b/tests/unit/test_internal/test_types/test_click/test_metavars.py index 5257db3..837c26e 100644 --- a/tests/unit/test_internal/test_types/test_click/test_metavars.py +++ b/tests/unit/test_internal/test_types/test_click/test_metavars.py @@ -14,13 +14,13 @@ def test_union(capsys: pytest.CaptureFixture) -> None: @feud.command def f( *, - opt1: int | float, # noqa: FA102 - opt2: t.Union[int, float], # noqa: FA100 - opt3: str | int | None, # noqa: FA102 - opt4: t.Optional[t.Union[str, int]], # noqa: FA100 - opt5: t.Union[int, t.Union[float, str]], # noqa: FA100 - opt6: int | None, # noqa: FA102 - opt7: str | t.Annotated[str, "annotated"], # noqa: FA102 + opt1: int | float, + opt2: t.Union[int, float], + opt3: str | int | None, + opt4: t.Optional[t.Union[str, int]], + opt5: t.Union[int, t.Union[float, str]], + opt6: int | None, + opt7: str | t.Annotated[str, "annotated"], ) -> None: pass