Skip to content

Commit 33a2075

Browse files
committed
🐛 Fix evaluating stringified annotations in Python 3.8 & 3.9 (also from __future__ import annotations)
1 parent 3b17788 commit 33a2075

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
@@ -2,7 +2,7 @@
22
from typer.testing import CliRunner
33
from typing_extensions import Annotated
44

5-
from .utils import needs_py310
5+
from .utils import needs_py38
66

77
runner = CliRunner()
88

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

2525

26-
@needs_py310
26+
@needs_py38
2727
def test_annotated_argument_in_string_type_with_default():
2828
app = typer.Typer()
2929

tests/utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
shell = None
1616

1717

18+
needs_py38 = pytest.mark.skipif(sys.version_info < (3, 8), reason="requires python3.8+")
19+
1820
needs_py310 = pytest.mark.skipif(
1921
sys.version_info < (3, 10), reason="requires python3.10+"
2022
)

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, Dict, List, Tuple, Type, 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)