Skip to content

Commit 9fea46f

Browse files
committed
🐛 Fix evaluating stringified annotations in Python 3.8 & 3.9 (also from __future__ import annotations)
1 parent 753cb32 commit 9fea46f

File tree

5 files changed

+50
-10
lines changed

5 files changed

+50
-10
lines changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ classifiers = [
3535
]
3636
dependencies = [
3737
"click >= 8.0.0",
38-
"typing-extensions >= 3.7.4.3",
38+
"typing-extensions >= 3.7.4.3; python_version < '3.8'",
39+
"typing-extensions >= 4.13.0; python_version >= '3.8'",
3940
]
4041
readme = "README.md"
4142
[project.urls]

tests/test_annotated.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import typer
66
from typer.testing import CliRunner
77

8-
from .utils import needs_py310
8+
from .utils import needs_py38
99

1010
runner = CliRunner()
1111

@@ -26,7 +26,7 @@ def cmd(val: Annotated[int, typer.Argument()] = 0):
2626
assert "hello 42" in result.output
2727

2828

29-
@needs_py310
29+
@needs_py38
3030
def test_annotated_argument_in_string_type_with_default():
3131
app = typer.Typer()
3232

tests/utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import pytest
55
from typer._completion_shared import _get_shell_name
66

7+
needs_py38 = pytest.mark.skipif(sys.version_info < (3, 8), reason="requires python3.8+")
8+
79
needs_py310 = pytest.mark.skipif(
810
sys.version_info < (3, 10), reason="requires python3.10+"
911
)

typer/_inspect.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import inspect
2+
import sys
3+
4+
if sys.version_info >= (3, 10):
5+
from inspect import signature
6+
elif sys.version_info >= (3, 8):
7+
from typing import Any, Callable
8+
9+
from typing_extensions import get_annotations
10+
11+
def signature(
12+
func: Callable[..., Any], eval_str: bool = False, **kwargs: Any
13+
) -> inspect.Signature:
14+
sig = inspect.signature(func, **kwargs)
15+
ann = get_annotations(
16+
func,
17+
globals=kwargs.get("globals"),
18+
locals=kwargs.get("locals"),
19+
eval_str=eval_str,
20+
)
21+
return sig.replace(
22+
parameters=[
23+
param.replace(annotation=ann.get(name, param.annotation))
24+
for name, param in sig.parameters.items()
25+
],
26+
return_annotation=ann.get("return", sig.return_annotation),
27+
)
28+
else:
29+
# Fallback for Python <3.8 to make `inspect.signature` accept the `eval_str`
30+
# keyword argument as a no-op. We can't backport support for evaluating
31+
# string annotations because only typing-extensions v4.13.0+ provides a
32+
# backport of `inspect.get_annotations`, which requires Python 3.8+.
33+
34+
from typing import Any, Callable
35+
36+
def signature(
37+
func: Callable[..., Any], eval_str: bool = False, **kwargs: Any
38+
) -> inspect.Signature:
39+
return inspect.signature(func, **kwargs)
40+
41+
42+
__all__ = ["signature"]

typer/utils.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import inspect
2-
import sys
31
from copy import copy
42
from typing import Any, Callable, cast
53

4+
from ._inspect import signature as inspect_signature
65
from ._typing import Annotated, get_args, get_origin, get_type_hints
76
from .models import ArgumentInfo, OptionInfo, ParameterInfo, ParamMeta
87

@@ -105,11 +104,7 @@ def _split_annotation_from_typer_annotations(
105104

106105

107106
def get_params_from_function(func: Callable[..., Any]) -> dict[str, ParamMeta]:
108-
if sys.version_info >= (3, 10):
109-
signature = inspect.signature(func, eval_str=True)
110-
else:
111-
signature = inspect.signature(func)
112-
107+
signature = inspect_signature(func, eval_str=True)
113108
type_hints = get_type_hints(func)
114109
params = {}
115110
for param in signature.parameters.values():

0 commit comments

Comments
 (0)