diff --git a/pyproject.toml b/pyproject.toml index c0ca3aa418..163755a742 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,8 @@ classifiers = [ ] dependencies = [ "click >= 8.0.0", - "typing-extensions >= 3.7.4.3", + "typing-extensions >= 3.7.4.3; python_version < '3.8'", + "typing-extensions >= 4.13.0; python_version >= '3.8'", ] readme = "README.md" [project.urls] diff --git a/tests/test_annotated.py b/tests/test_annotated.py index 09072b3ae1..04fe00a01a 100644 --- a/tests/test_annotated.py +++ b/tests/test_annotated.py @@ -2,7 +2,7 @@ from typer.testing import CliRunner from typing_extensions import Annotated -from .utils import needs_py310 +from .utils import needs_py38 runner = CliRunner() @@ -23,7 +23,7 @@ def cmd(val: Annotated[int, typer.Argument()] = 0): assert "hello 42" in result.output -@needs_py310 +@needs_py38 def test_annotated_argument_in_string_type_with_default(): app = typer.Typer() diff --git a/tests/utils.py b/tests/utils.py index 019b006fa0..4de8771154 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -15,6 +15,8 @@ shell = None +needs_py38 = pytest.mark.skipif(sys.version_info < (3, 8), reason="requires python3.8+") + needs_py310 = pytest.mark.skipif( sys.version_info < (3, 10), reason="requires python3.10+" ) diff --git a/typer/_inspect.py b/typer/_inspect.py new file mode 100644 index 0000000000..8fb11d1596 --- /dev/null +++ b/typer/_inspect.py @@ -0,0 +1,42 @@ +import inspect +import sys + +if sys.version_info >= (3, 10): + from inspect import signature +elif sys.version_info >= (3, 8): + from typing import Any, Callable + + from typing_extensions import get_annotations + + def signature( + func: Callable[..., Any], eval_str: bool = False, **kwargs: Any + ) -> inspect.Signature: + sig = inspect.signature(func, **kwargs) + ann = get_annotations( + func, + globals=kwargs.get("globals"), + locals=kwargs.get("locals"), + eval_str=eval_str, + ) + return sig.replace( + parameters=[ + param.replace(annotation=ann.get(name, param.annotation)) + for name, param in sig.parameters.items() + ], + return_annotation=ann.get("return", sig.return_annotation), + ) +else: + # Fallback for Python <3.8 to make `inspect.signature` accept the `eval_str` + # keyword argument as a no-op. We can't backport support for evaluating + # string annotations because only typing-extensions v4.13.0+ provides a + # backport of `inspect.get_annotations`, which requires Python 3.8+. + + from typing import Any, Callable + + def signature( + func: Callable[..., Any], eval_str: bool = False, **kwargs: Any + ) -> inspect.Signature: + return inspect.signature(func, **kwargs) + + +__all__ = ["signature"] diff --git a/typer/utils.py b/typer/utils.py index 81dc4dd61d..d969469d86 100644 --- a/typer/utils.py +++ b/typer/utils.py @@ -1,8 +1,7 @@ -import inspect -import sys from copy import copy from typing import Any, Callable, Dict, List, Tuple, Type, cast +from ._inspect import signature as inspect_signature from ._typing import Annotated, get_args, get_origin, get_type_hints from .models import ArgumentInfo, OptionInfo, ParameterInfo, ParamMeta @@ -105,11 +104,7 @@ def _split_annotation_from_typer_annotations( def get_params_from_function(func: Callable[..., Any]) -> Dict[str, ParamMeta]: - if sys.version_info >= (3, 10): - signature = inspect.signature(func, eval_str=True) - else: - signature = inspect.signature(func) - + signature = inspect_signature(func, eval_str=True) type_hints = get_type_hints(func) params = {} for param in signature.parameters.values():