diff --git a/typing_extensions/src_py2/test_typing_extensions.py b/typing_extensions/src_py2/test_typing_extensions.py index 7b273748..59b1097a 100644 --- a/typing_extensions/src_py2/test_typing_extensions.py +++ b/typing_extensions/src_py2/test_typing_extensions.py @@ -8,7 +8,7 @@ import types from unittest import TestCase, main, skipUnless -from typing_extensions import NoReturn, ClassVar, Final, IntVar, Literal, TypedDict +from typing_extensions import NoReturn, CallableParametersVariable, ClassVar, Final, IntVar, Literal, TypedDict from typing_extensions import ContextManager, Counter, Deque, DefaultDict from typing_extensions import NewType, overload, Protocol, runtime import typing @@ -78,6 +78,20 @@ def test_cannot_instantiate(self): type(NoReturn)() +class CallableParametersVariableTests(BaseTestCase): + def test_valid(self): + T_params = CallableParametersVariable("T_params") + f_type = typing.Callable[T_params, int] + + def test_invalid(self): + with self.assertRaises(TypeError): + T_params = CallableParametersVariable("T_params", int) + with self.assertRaises(TypeError): + T_params = CallableParametersVariable("T_params", bound=int) + with self.assertRaises(TypeError): + T_params = CallableParametersVariable("T_params", covariant=True) + + class ClassVarTests(BaseTestCase): def test_basics(self): diff --git a/typing_extensions/src_py2/typing_extensions.py b/typing_extensions/src_py2/typing_extensions.py index 4bc64674..a9106044 100644 --- a/typing_extensions/src_py2/typing_extensions.py +++ b/typing_extensions/src_py2/typing_extensions.py @@ -26,6 +26,7 @@ 'DefaultDict', # One-off things. + 'CallableParametersVariable' 'final', 'IntVar', 'Literal', @@ -70,6 +71,34 @@ def __subclasscheck__(self, cls): NoReturn = _NoReturn(_root=True) +def CallableParametersVariable(name): + """This kind of type variable captures callable parameter specifications + instead of types, allowing the typing of decorators which transform the + return type of the given callable. For example: + + from typing import TypeVar, Callable, List + from typing_extensions import CallableParametersVariable + Tparams = CallableParametersVariable("Tparams") + Treturn = TypeVar("Treturn") + + def unwrap( + f: Callable[Tparams, List[Treturn], + ) -> Callable[Tparams, Treturn]: ... + + @unwrap + def foo(x: int, y: str, z: bool = False) -> List[int]: + return [1, 2, 3] + + decorates foo into a callable that returns int, but still has the same + parameters, including their names and whether they are required. + + The empty list is required for backwards compatibility with the runtime + implementation for callables, which requires the first argument to be + a list of types + """ + return [] + + T_co = typing.TypeVar('T_co', covariant=True) if hasattr(typing, 'ContextManager'): diff --git a/typing_extensions/src_py3/test_typing_extensions.py b/typing_extensions/src_py3/test_typing_extensions.py index e209a3f0..769d063e 100644 --- a/typing_extensions/src_py3/test_typing_extensions.py +++ b/typing_extensions/src_py3/test_typing_extensions.py @@ -13,7 +13,7 @@ from typing import Generic from typing import get_type_hints from typing import no_type_check -from typing_extensions import NoReturn, ClassVar, Final, IntVar, Literal, Type, NewType, TypedDict +from typing_extensions import NoReturn, CallableParametersVariable, ClassVar, Final, IntVar, Literal, Type, NewType, TypedDict try: from typing_extensions import Protocol, runtime except ImportError: @@ -129,6 +129,20 @@ def test_cannot_instantiate(self): type(NoReturn)() +class CallableParametersVariableTests(BaseTestCase): + def test_valid(self): + T_params = CallableParametersVariable("T_params") + f_type = typing.Callable[T_params, int] + + def test_invalid(self): + with self.assertRaises(TypeError): + T_params = CallableParametersVariable("T_params", int) + with self.assertRaises(TypeError): + T_params = CallableParametersVariable("T_params", bound=int) + with self.assertRaises(TypeError): + T_params = CallableParametersVariable("T_params", covariant=True) + + class ClassVarTests(BaseTestCase): def test_basics(self): diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index 81f60ac9..c929577b 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -126,6 +126,7 @@ def _check_methods_in_mro(C, *methods): 'DefaultDict', # One-off things. + 'CallableParametersVariable' 'final', 'IntVar', 'Literal', @@ -212,6 +213,34 @@ def stop() -> NoReturn: T_contra = typing.TypeVar('T_contra', contravariant=True) # Ditto contravariant. +def CallableParametersVariable(name): + """This kind of type variable captures callable parameter specifications + instead of types, allowing the typing of decorators which transform the + return type of the given callable. For example: + + from typing import TypeVar, Callable, List + from typing_extensions import CallableParametersVariable + Tparams = CallableParametersVariable("Tparams") + Treturn = TypeVar("Treturn") + + def unwrap( + f: Callable[Tparams, List[Treturn], + ) -> Callable[Tparams, Treturn]: ... + + @unwrap + def foo(x: int, y: str, z: bool = False) -> List[int]: + return [1, 2, 3] + + decorates foo into a callable that returns int, but still has the same + parameters, including their names and whether they are required. + + The empty list is required for backwards compatibility with the runtime + implementation for callables, which requires the first argument to be + a list of types + """ + return [] + + if hasattr(typing, 'ClassVar'): ClassVar = typing.ClassVar elif hasattr(typing, '_FinalTypingBase'):