From aeeb1e87f8f0a4a1c449d4bb4db6f0759edf5b5e Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 2 Oct 2022 12:44:05 +0200 Subject: [PATCH 1/6] Initial support for TypeVarLike default parameter (PEP 696) --- CHANGELOG.md | 2 + src/test_typing_extensions.py | 26 +++++++++++ src/typing_extensions.py | 88 ++++++++++++++++++++++++++++++++--- 3 files changed, 110 insertions(+), 6 deletions(-) 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..3a464968 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -3067,8 +3067,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 +3398,28 @@ 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]] + + if __name__ == '__main__': main() diff --git a/src/typing_extensions.py b/src/typing_extensions.py index b03f5dd0..434a2b7c 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -21,6 +21,7 @@ 'ParamSpecKwargs', 'Self', 'Type', + 'TypeVar', 'TypeVarTuple', 'Unpack', @@ -1147,6 +1148,42 @@ def __repr__(self): above.""") +class _DefaultMixin: + """Mixin for TypeVarLike defaults.""" + + def __init__(self, default): + if isinstance(default, tuple): + 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.""" + + __slots__ = ('__default__',) + __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 +1248,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 +1331,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 +1341,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 +1843,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 +1911,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: From b90088551f2d1b54342dbf2d5291f63a6a15c138 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 2 Oct 2022 13:30:37 +0200 Subject: [PATCH 2/6] Update src/typing_extensions.py Co-authored-by: James Hilton-Balfe --- src/typing_extensions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 434a2b7c..711a42cf 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1152,7 +1152,7 @@ class _DefaultMixin: """Mixin for TypeVarLike defaults.""" def __init__(self, default): - if isinstance(default, tuple): + if isinstance(default, (tuple, list)): self.__default__ = tuple((typing._type_check(d, "Default must be a type") for d in default)) elif default: From 4c412cf58df1648eea3c121033d5dfb1ba2951a1 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 2 Oct 2022 16:46:42 +0200 Subject: [PATCH 3/6] Code review --- src/test_typing_extensions.py | 38 +++++++++++++++++++++---- src/typing_extensions.py | 52 +++++++++++++++++++++-------------- 2 files changed, 65 insertions(+), 25 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 3a464968..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): @@ -3400,26 +3413,41 @@ def test_same_as_typing_NamedTuple_38_minus(self): class TypeVarLikeDefaultsTests(BaseTestCase): def test_typevar(self): - T = typing_extensions.TypeVar("T", default=int) + 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)) + 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]]) + 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 711a42cf..a3dec414 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1151,6 +1151,8 @@ def __repr__(self): 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") @@ -1161,27 +1163,31 @@ def __init__(self, default): self.__default__ = None -# Add default Parameter - PEP 696 -class TypeVar(typing.TypeVar, _DefaultMixin, _root=True): - """Type variable.""" +if sys.version_info >= (3, 12): + TypeVar = typing.TypeVar - __slots__ = ('__default__',) - __module__ = "typing" +else: + # Add default Parameter - PEP 696 + class TypeVar(typing.TypeVar, _DefaultMixin, _root=True): + """Type variable.""" - 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) + __slots__ = ('__default__',) + __module__ = 'typing' - # 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 + 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 @@ -1246,6 +1252,10 @@ def __eq__(self, other): return NotImplemented return self.__origin__ == other.__origin__ + +if sys.version_info >= (3, 12): + ParamSpec = typing.ParamSpec + # 3.10+ if hasattr(typing, 'ParamSpec'): @@ -1253,7 +1263,7 @@ def __eq__(self, other): class ParamSpec(typing.ParamSpec, _DefaultMixin, _root=True): """Parameter specification variable.""" - __module__ = "typing" + __module__ = 'typing' def __init__(self, name, *, bound=None, covariant=False, contravariant=False, default=None): @@ -1841,8 +1851,10 @@ def add_batch_axis( def _is_unpack(obj): return isinstance(obj, _UnpackAlias) +if sys.version_info >= (3, 12): + TypeVarTuple = typing.TypeVarTuple -if hasattr(typing, "TypeVarTuple"): # 3.11+ +elif hasattr(typing, "TypeVarTuple"): # 3.11+ # Add default Parameter - PEP 696 class TypeVarTuple(typing.TypeVarTuple, _DefaultMixin, _root=True): From b24b4f8f5e74c8158d5daf7f8e875e7032ed1508 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 2 Oct 2022 16:53:00 +0200 Subject: [PATCH 4/6] Update src/typing_extensions.py Co-authored-by: Alex Waygood --- src/typing_extensions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index a3dec414..bd0eff67 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1257,7 +1257,7 @@ def __eq__(self, other): ParamSpec = typing.ParamSpec # 3.10+ -if hasattr(typing, 'ParamSpec'): +elif hasattr(typing, 'ParamSpec'): # Add default Parameter - PEP 696 class ParamSpec(typing.ParamSpec, _DefaultMixin, _root=True): From c04112a66ab4a48f43d43ea44f1e10601e9855d4 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 3 Oct 2022 22:26:45 +0200 Subject: [PATCH 5/6] Revert conditional exports --- src/typing_extensions.py | 49 ++++++++++++++++------------------------ 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index bd0eff67..04730e07 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1163,31 +1163,27 @@ def __init__(self, default): self.__default__ = None -if sys.version_info >= (3, 12): - TypeVar = typing.TypeVar +# Add default Parameter - PEP 696 +class TypeVar(typing.TypeVar, _DefaultMixin, _root=True): + """Type variable.""" -else: - # Add default Parameter - PEP 696 - class TypeVar(typing.TypeVar, _DefaultMixin, _root=True): - """Type variable.""" + __slots__ = ('__default__',) + __module__ = 'typing' - __slots__ = ('__default__',) - __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) - 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 + # 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 @@ -1253,11 +1249,8 @@ def __eq__(self, other): return self.__origin__ == other.__origin__ -if sys.version_info >= (3, 12): - ParamSpec = typing.ParamSpec - # 3.10+ -elif hasattr(typing, 'ParamSpec'): +if hasattr(typing, 'ParamSpec'): # Add default Parameter - PEP 696 class ParamSpec(typing.ParamSpec, _DefaultMixin, _root=True): @@ -1851,10 +1844,8 @@ def add_batch_axis( def _is_unpack(obj): return isinstance(obj, _UnpackAlias) -if sys.version_info >= (3, 12): - TypeVarTuple = typing.TypeVarTuple -elif hasattr(typing, "TypeVarTuple"): # 3.11+ +if hasattr(typing, "TypeVarTuple"): # 3.11+ # Add default Parameter - PEP 696 class TypeVarTuple(typing.TypeVarTuple, _DefaultMixin, _root=True): From 088dd3eddf57006fb14a0029556fd42021b19021 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 3 Oct 2022 23:02:44 +0200 Subject: [PATCH 6/6] Remove __slots__ from TypeVar --- src/typing_extensions.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 04730e07..a1450dc4 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1167,7 +1167,6 @@ def __init__(self, default): class TypeVar(typing.TypeVar, _DefaultMixin, _root=True): """Type variable.""" - __slots__ = ('__default__',) __module__ = 'typing' def __init__(self, name, *constraints, bound=None, @@ -1248,7 +1247,6 @@ def __eq__(self, other): return NotImplemented return self.__origin__ == other.__origin__ - # 3.10+ if hasattr(typing, 'ParamSpec'):