diff --git a/CHANGELOG.md b/CHANGELOG.md index 5149fd5f..9d9fea18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ - Add `typing_extensions.Any` a backport of python 3.11's Any class which is subclassable at runtime. (backport from python/cpython#31841, by Shantanu and Jelle Zijlstra). Patch by James Hilton-Balfe (@Gobot1234). +- Add initial support for TypeVarLike `default` parameter, PEP 696. + Patch by Marc Mueller (@cdce8p). # Release 4.3.0 (July 1, 2022) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 64d052b2..a7505546 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -2463,18 +2463,20 @@ class Z(Generic[P]): pass def test_pickle(self): - global P, P_co, P_contra + global P, P_co, P_contra, P_default P = ParamSpec('P') P_co = ParamSpec('P_co', covariant=True) P_contra = ParamSpec('P_contra', contravariant=True) + P_default = ParamSpec('P_default', default=int) for proto in range(pickle.HIGHEST_PROTOCOL): with self.subTest(f'Pickle protocol {proto}'): - for paramspec in (P, P_co, P_contra): + for paramspec in (P, P_co, P_contra, P_default): z = pickle.loads(pickle.dumps(paramspec, proto)) self.assertEqual(z.__name__, paramspec.__name__) self.assertEqual(z.__covariant__, paramspec.__covariant__) self.assertEqual(z.__contravariant__, paramspec.__contravariant__) self.assertEqual(z.__bound__, paramspec.__bound__) + self.assertEqual(z.__default__, paramspec.__default__) def test_eq(self): P = ParamSpec('P') @@ -2840,6 +2842,17 @@ def test_args_and_parameters(self): self.assertEqual(t.__args__, (Unpack[Ts],)) self.assertEqual(t.__parameters__, (Ts,)) + def test_pickle(self): + global Ts, Ts_default # pickle wants to reference the class by name + Ts = TypeVarTuple('Ts') + Ts_default = TypeVarTuple('Ts_default', default=Unpack[Tuple[int, str]]) + + for proto in range(pickle.HIGHEST_PROTOCOL): + for typevartuple in (Ts, Ts_default): + z = pickle.loads(pickle.dumps(typevartuple, proto)) + self.assertEqual(z.__name__, typevartuple.__name__) + self.assertEqual(z.__default__, typevartuple.__default__) + class FinalDecoratorTests(BaseTestCase): def test_final_unmodified(self): @@ -3067,8 +3080,11 @@ def test_all_names_in___all__(self): def test_typing_extensions_defers_when_possible(self): exclude = { 'overload', + 'ParamSpec', 'Text', 'TypedDict', + 'TypeVar', + 'TypeVarTuple', 'TYPE_CHECKING', 'Final', 'get_type_hints', @@ -3395,5 +3411,43 @@ def test_same_as_typing_NamedTuple_38_minus(self): ) +class TypeVarLikeDefaultsTests(BaseTestCase): + def test_typevar(self): + T = typing_extensions.TypeVar('T', default=int) + self.assertEqual(T.__default__, int) + + class A(Generic[T]): ... + Alias = Optional[T] + + def test_paramspec(self): + P = ParamSpec('P', default=(str, int)) + self.assertEqual(P.__default__, (str, int)) + + class A(Generic[P]): ... + Alias = typing.Callable[P, None] + + def test_typevartuple(self): + Ts = TypeVarTuple('Ts', default=Unpack[Tuple[str, int]]) + self.assertEqual(Ts.__default__, Unpack[Tuple[str, int]]) + + class A(Generic[Unpack[Ts]]): ... + Alias = Optional[Unpack[Ts]] + + def test_pickle(self): + global U, U_co, U_contra, U_default # pickle wants to reference the class by name + U = typing_extensions.TypeVar('U') + U_co = typing_extensions.TypeVar('U_co', covariant=True) + U_contra = typing_extensions.TypeVar('U_contra', contravariant=True) + U_default = typing_extensions.TypeVar('U_default', default=int) + for proto in range(pickle.HIGHEST_PROTOCOL): + for typevar in (U, U_co, U_contra, U_default): + z = pickle.loads(pickle.dumps(typevar, proto)) + self.assertEqual(z.__name__, typevar.__name__) + self.assertEqual(z.__covariant__, typevar.__covariant__) + self.assertEqual(z.__contravariant__, typevar.__contravariant__) + self.assertEqual(z.__bound__, typevar.__bound__) + self.assertEqual(z.__default__, typevar.__default__) + + if __name__ == '__main__': main() diff --git a/src/typing_extensions.py b/src/typing_extensions.py index b03f5dd0..a1450dc4 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -21,6 +21,7 @@ 'ParamSpecKwargs', 'Self', 'Type', + 'TypeVar', 'TypeVarTuple', 'Unpack', @@ -1147,6 +1148,43 @@ def __repr__(self): above.""") +class _DefaultMixin: + """Mixin for TypeVarLike defaults.""" + + __slots__ = () + + def __init__(self, default): + if isinstance(default, (tuple, list)): + self.__default__ = tuple((typing._type_check(d, "Default must be a type") + for d in default)) + elif default: + self.__default__ = typing._type_check(default, "Default must be a type") + else: + self.__default__ = None + + +# Add default Parameter - PEP 696 +class TypeVar(typing.TypeVar, _DefaultMixin, _root=True): + """Type variable.""" + + __module__ = 'typing' + + def __init__(self, name, *constraints, bound=None, + covariant=False, contravariant=False, + default=None): + super().__init__(name, *constraints, bound=bound, covariant=covariant, + contravariant=contravariant) + _DefaultMixin.__init__(self, default) + + # for pickling: + try: + def_mod = sys._getframe(1).f_globals.get('__name__', '__main__') + except (AttributeError, ValueError): + def_mod = None + if def_mod != 'typing_extensions': + self.__module__ = def_mod + + # Python 3.10+ has PEP 612 if hasattr(typing, 'ParamSpecArgs'): ParamSpecArgs = typing.ParamSpecArgs @@ -1211,12 +1249,32 @@ def __eq__(self, other): # 3.10+ if hasattr(typing, 'ParamSpec'): - ParamSpec = typing.ParamSpec + + # Add default Parameter - PEP 696 + class ParamSpec(typing.ParamSpec, _DefaultMixin, _root=True): + """Parameter specification variable.""" + + __module__ = 'typing' + + def __init__(self, name, *, bound=None, covariant=False, contravariant=False, + default=None): + super().__init__(name, bound=bound, covariant=covariant, + contravariant=contravariant) + _DefaultMixin.__init__(self, default) + + # for pickling: + try: + def_mod = sys._getframe(1).f_globals.get('__name__', '__main__') + except (AttributeError, ValueError): + def_mod = None + if def_mod != 'typing_extensions': + self.__module__ = def_mod + # 3.7-3.9 else: # Inherits from list as a workaround for Callable checks in Python < 3.9.2. - class ParamSpec(list): + class ParamSpec(list, _DefaultMixin): """Parameter specification variable. Usage:: @@ -1274,7 +1332,8 @@ def args(self): def kwargs(self): return ParamSpecKwargs(self) - def __init__(self, name, *, bound=None, covariant=False, contravariant=False): + def __init__(self, name, *, bound=None, covariant=False, contravariant=False, + default=None): super().__init__([self]) self.__name__ = name self.__covariant__ = bool(covariant) @@ -1283,6 +1342,7 @@ def __init__(self, name, *, bound=None, covariant=False, contravariant=False): self.__bound__ = typing._type_check(bound, 'Bound must be a type.') else: self.__bound__ = None + _DefaultMixin.__init__(self, default) # for pickling: try: @@ -1784,9 +1844,25 @@ def _is_unpack(obj): if hasattr(typing, "TypeVarTuple"): # 3.11+ - TypeVarTuple = typing.TypeVarTuple + + # Add default Parameter - PEP 696 + class TypeVarTuple(typing.TypeVarTuple, _DefaultMixin, _root=True): + """Type variable tuple.""" + + def __init__(self, name, *, default=None): + super().__init__(name) + _DefaultMixin.__init__(self, default) + + # for pickling: + try: + def_mod = sys._getframe(1).f_globals.get('__name__', '__main__') + except (AttributeError, ValueError): + def_mod = None + if def_mod != 'typing_extensions': + self.__module__ = def_mod + else: - class TypeVarTuple: + class TypeVarTuple(_DefaultMixin): """Type variable tuple. Usage:: @@ -1836,8 +1912,9 @@ def get_shape(self) -> Tuple[*Ts]: def __iter__(self): yield self.__unpacked__ - def __init__(self, name): + def __init__(self, name, *, default=None): self.__name__ = name + _DefaultMixin.__init__(self, default) # for pickling: try: