From fb9164d20c70c34043bbfb68835afa1a44f4d5f7 Mon Sep 17 00:00:00 2001 From: "Edwin (Ed) Onuonga" Date: Mon, 23 Sep 2024 01:34:20 +0100 Subject: [PATCH] feat: support postponed type hint evaluation (#150) --- README.md | 9 --- feud/_internal/_command.py | 4 +- tests/unit/test_core/test_command.py | 91 ++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 98f21ba..98ecf5b 100644 --- a/README.md +++ b/README.md @@ -778,15 +778,6 @@ This installs Feud with the optional dependencies: To install Feud without any optional dependencies, simply run `pip install feud`. -> [!CAUTION] -> Feud **will break** if used with postponed type hint evaluation ([PEP563](https://peps.python.org/pep-0563/)), i.e.: -> -> ```python -> from __future__ import annotations -> ``` -> -> This is because Feud relies on type hint evaluation in order to determine the expected input type for command parameters. - ### Improved formatting with Rich Below is a comparison of Feud with and without `rich-click`. diff --git a/feud/_internal/_command.py b/feud/_internal/_command.py index 67b8d86..afc4c7c 100644 --- a/feud/_internal/_command.py +++ b/feud/_internal/_command.py @@ -66,7 +66,7 @@ def decorate( # noqa: PLR0915 var_positional: str | None = None params: list[click.Parameter] = [] - sig: inspect.signature = inspect.signature(func) + sig: inspect.signature = inspect.signature(func, eval_str=True) for i, (param_name, param_spec) in enumerate(sig.parameters.items()): # store names of positional arguments @@ -248,7 +248,7 @@ def build_command_state( # noqa: PLR0915 state.description: str | None = _docstring.get_description(doc) - sig: inspect.Signature = inspect.signature(func) + sig: inspect.Signature = inspect.signature(func, eval_str=True) for param, spec in sig.parameters.items(): meta = ParameterSpec() diff --git a/tests/unit/test_core/test_command.py b/tests/unit/test_core/test_command.py index 2f0b979..505f1da 100644 --- a/tests/unit/test_core/test_command.py +++ b/tests/unit/test_core/test_command.py @@ -449,6 +449,97 @@ def command( assert e.default is True +def test_full_signature_string_hints( + capsys: pytest.CaptureFixture, +) -> None: + @feud.command + def command( + a: "float", + /, + b: "str", + *c: "t.PositiveInt", + d: "int", + e: "bool" = True, + **f: "float", + ) -> None: + """Does something. + + Parameters + ---------- + a: + Test 1. + b: + Test 2. + *c: + Test 3. + d: + Test 4. + e: + Test 5. + **f: + Test 6. + """ + return a, b, c, d, e, f + + # check params (**f should be ignored) + params = command.params + assert len(params) == 5 + + a = command.params[0] + assert isinstance(a, click.Argument) + assert a.name == "a" + assert a.type == click.FLOAT + assert a.opts == ["a"] + assert a.secondary_opts == [] + assert a.nargs == 1 + assert a.required + assert a.default is None + + b = command.params[1] + assert isinstance(b, click.Argument) + assert b.name == "b" + assert b.type == click.STRING + assert b.opts == ["b"] + assert b.secondary_opts == [] + assert b.nargs == 1 + assert b.required + assert b.default is None + + c = command.params[2] + assert isinstance(c, click.Argument) + assert c.name == "c" + assert isinstance(c.type, click.IntRange) + assert c.type.min == 0 + assert c.type.min_open + assert c.opts == ["c"] + assert c.secondary_opts == [] + assert c.nargs == -1 + assert not c.required + assert c.default is None + + d = command.params[3] + assert isinstance(d, click.Option) + assert d.name == "d" + assert d.help == "Test 4." + assert d.type == click.INT + assert d.opts == ["--d"] + assert d.secondary_opts == [] + assert d.nargs == 1 + assert d.required + assert d.default is None + + e = command.params[4] + assert isinstance(e, click.Option) + assert e.name == "e" + assert e.help == "Test 5." + assert e.type == click.BOOL + assert e.opts == ["--e"] + assert e.secondary_opts == ["--no-e"] + assert e.nargs == 1 + assert not e.required + assert e.default is True + + def test_argument_default() -> None: @feud.command def f(