From 90bdf0d3df7923bd2bf58f2c9dac3433e6c92b2f Mon Sep 17 00:00:00 2001 From: Matt Bogosian Date: Tue, 1 Feb 2022 10:55:28 -0600 Subject: [PATCH 1/9] bpo-46581: Propagate private vars via _GenericAlias.copy_with GH-26091 added the _typevar_types and _paramspec_tvars instance variables to _GenericAlias. However, they were not propagated consistently. This commit addresses the most prominent deficiency identified in bpo-46581 (namely their absence from _GenericAlias.copy_with), but there could be others. --- Lib/test/test_genericalias.py | 8 ++++++++ Lib/typing.py | 4 +++- Misc/ACKS | 1 + .../next/Library/2022-02-01-11-32-47.bpo-46581.t7Zw65.rst | 3 +++ 4 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2022-02-01-11-32-47.bpo-46581.t7Zw65.rst diff --git a/Lib/test/test_genericalias.py b/Lib/test/test_genericalias.py index 1407657c9bb206..980f36cff6d949 100644 --- a/Lib/test/test_genericalias.py +++ b/Lib/test/test_genericalias.py @@ -305,6 +305,14 @@ def __deepcopy__(self, memo): self.assertEqual(copied.__args__, alias.__args__) self.assertEqual(copied.__parameters__, alias.__parameters__) + def test_copy_with(self): + # bpo-46581 + from typing import Callable, ParamSpec + P = ParamSpec('P') + original = Callable[P, int] + copied = original.copy_with((P, int)) + self.assertEqual(original.__parameters__, copied.__parameters__) + def test_union(self): a = typing.Union[list[int], list[str]] self.assertEqual(a.__args__, (list[int], list[str])) diff --git a/Lib/typing.py b/Lib/typing.py index abb8bcefc5c04a..f08eb9c6e08482 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1339,7 +1339,9 @@ def _determine_new_args(self, args): return tuple(new_args) def copy_with(self, args): - return self.__class__(self.__origin__, args, name=self._name, inst=self._inst) + return self.__class__(self.__origin__, args, name=self._name, inst=self._inst, + _typevar_types=self._typevar_types, + _paramspec_tvars=self._paramspec_tvars) def __repr__(self): if self._name: diff --git a/Misc/ACKS b/Misc/ACKS index df851bb834cd4e..84fcb322d4086a 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -190,6 +190,7 @@ Paul Boddie Matthew Boedicker Robin Boerdijk Andra Bogildea +Matt Bogosian Nikolay Bogoychev David Bolen Wouter Bolsterlee diff --git a/Misc/NEWS.d/next/Library/2022-02-01-11-32-47.bpo-46581.t7Zw65.rst b/Misc/NEWS.d/next/Library/2022-02-01-11-32-47.bpo-46581.t7Zw65.rst new file mode 100644 index 00000000000000..896bcda5414e8f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-02-01-11-32-47.bpo-46581.t7Zw65.rst @@ -0,0 +1,3 @@ +Propagates :attribute:`~typing._GenericAlias._typevar_types` and +:attribute:`~typing._GenericAlias._paramspec_tvars` via +:method:`~typing._GenericAlias.copy_with`. From 19e9207cca392c57b23c07102bfdbd21ab962854 Mon Sep 17 00:00:00 2001 From: Matt Bogosian Date: Sat, 26 Feb 2022 13:16:37 -0600 Subject: [PATCH 2/9] Move tests to test_typing --- Lib/test/test_genericalias.py | 8 -------- Lib/test/test_typing.py | 8 ++++++++ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_genericalias.py b/Lib/test/test_genericalias.py index 980f36cff6d949..1407657c9bb206 100644 --- a/Lib/test/test_genericalias.py +++ b/Lib/test/test_genericalias.py @@ -305,14 +305,6 @@ def __deepcopy__(self, memo): self.assertEqual(copied.__args__, alias.__args__) self.assertEqual(copied.__parameters__, alias.__parameters__) - def test_copy_with(self): - # bpo-46581 - from typing import Callable, ParamSpec - P = ParamSpec('P') - original = Callable[P, int] - copied = original.copy_with((P, int)) - self.assertEqual(original.__parameters__, copied.__parameters__) - def test_union(self): a = typing.Union[list[int], list[str]] self.assertEqual(a.__args__, (list[int], list[str])) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 5c1e907070ee41..a614d9db07020c 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -2802,6 +2802,14 @@ def __init__(self, attr: T) -> None: self.assertEqual(deepcopy(ci).attr, 1) self.assertEqual(ci.__orig_class__, C[int]) + def test_copy_with(self): + # bpo-46581 + from typing import Callable, ParamSpec + P = ParamSpec('P') + original = Callable[P, int] + copied = original.copy_with((P, int)) + self.assertEqual(original.__parameters__, copied.__parameters__) + def test_weakref_all(self): T = TypeVar('T') things = [Any, Union[T, int], Callable[..., T], Tuple[Any, Any], From e30f7eabf8e7b2fc9c728f109af837d4e99ff254 Mon Sep 17 00:00:00 2001 From: Matt Bogosian Date: Fri, 4 Mar 2022 09:04:07 -0600 Subject: [PATCH 3/9] Move copy edits to #31684 --- Lib/typing.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index f08eb9c6e08482..72d5b9d34e962d 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -332,8 +332,8 @@ def inner(*args, **kwds): def _eval_type(t, globalns, localns, recursive_guard=frozenset()): """Evaluate all forward references in the given type t. For use of globalns and localns see the docstring for get_type_hints(). - recursive_guard is used to prevent infinite recursion with a recursive - ForwardRef. + recursive_guard is used to prevent prevent infinite recursion + with recursive ForwardRef. """ if isinstance(t, ForwardRef): return t._evaluate(globalns, localns, recursive_guard) @@ -1097,7 +1097,7 @@ def __getattr__(self, attr): return self._name or self.__origin__.__name__ # We are careful for copy and pickle. - # Also for simplicity we don't relay any dunder names + # Also for simplicity we just don't relay all dunder names if '__origin__' in self.__dict__ and not _is_dunder(attr): return getattr(self.__origin__, attr) raise AttributeError(attr) From 06bd0c308cf91f973f4a499b0e6e84bcfecd5f6c Mon Sep 17 00:00:00 2001 From: Matt Bogosian Date: Sun, 6 Mar 2022 07:48:54 -0600 Subject: [PATCH 4/9] Update Lib/typing.py Adopt @Fidget-Spinner's suggestion. Co-authored-by: Ken Jin <28750310+Fidget-Spinner@users.noreply.github.com> --- Lib/typing.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/typing.py b/Lib/typing.py index 72d5b9d34e962d..de89bcfac48f38 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1339,6 +1339,8 @@ def _determine_new_args(self, args): return tuple(new_args) def copy_with(self, args): + if isinstance(self, _ConcatenateGenericAlias): + return self.__class__(self.__origin__, args) return self.__class__(self.__origin__, args, name=self._name, inst=self._inst, _typevar_types=self._typevar_types, _paramspec_tvars=self._paramspec_tvars) From 2cd81996ba337cb2f201286c3f017a48d5a55cc2 Mon Sep 17 00:00:00 2001 From: Matt Bogosian Date: Tue, 8 Mar 2022 13:39:04 -0600 Subject: [PATCH 5/9] Adopt @serhiy-storchaka's suggestions for approach --- Lib/typing.py | 17 ++++++----------- .../2022-02-01-11-32-47.bpo-46581.t7Zw65.rst | 5 ++--- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index de89bcfac48f38..e3015563b3e8ce 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -332,8 +332,8 @@ def inner(*args, **kwds): def _eval_type(t, globalns, localns, recursive_guard=frozenset()): """Evaluate all forward references in the given type t. For use of globalns and localns see the docstring for get_type_hints(). - recursive_guard is used to prevent prevent infinite recursion - with recursive ForwardRef. + recursive_guard is used to prevent infinite recursion with a recursive + ForwardRef. """ if isinstance(t, ForwardRef): return t._evaluate(globalns, localns, recursive_guard) @@ -670,7 +670,9 @@ def Concatenate(self, parameters): "ParamSpec variable.") msg = "Concatenate[arg, ...]: each arg must be a type." parameters = (*(_type_check(p, msg) for p in parameters[:-1]), parameters[-1]) - return _ConcatenateGenericAlias(self, parameters) + return _ConcatenateGenericAlias(self, parameters, + _typevar_types=(TypeVar, ParamSpec), + _paramspec_tvars=True) @_SpecialForm @@ -1097,7 +1099,7 @@ def __getattr__(self, attr): return self._name or self.__origin__.__name__ # We are careful for copy and pickle. - # Also for simplicity we just don't relay all dunder names + # Also for simplicity we don't relay any dunder names if '__origin__' in self.__dict__ and not _is_dunder(attr): return getattr(self.__origin__, attr) raise AttributeError(attr) @@ -1339,8 +1341,6 @@ def _determine_new_args(self, args): return tuple(new_args) def copy_with(self, args): - if isinstance(self, _ConcatenateGenericAlias): - return self.__class__(self.__origin__, args) return self.__class__(self.__origin__, args, name=self._name, inst=self._inst, _typevar_types=self._typevar_types, _paramspec_tvars=self._paramspec_tvars) @@ -1552,11 +1552,6 @@ def __hash__(self): class _ConcatenateGenericAlias(_GenericAlias, _root=True): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs, - _typevar_types=(TypeVar, ParamSpec), - _paramspec_tvars=True) - def copy_with(self, params): if isinstance(params[-1], (list, tuple)): return (*params[:-1], *params[-1]) diff --git a/Misc/NEWS.d/next/Library/2022-02-01-11-32-47.bpo-46581.t7Zw65.rst b/Misc/NEWS.d/next/Library/2022-02-01-11-32-47.bpo-46581.t7Zw65.rst index 896bcda5414e8f..1982c1d70093f7 100644 --- a/Misc/NEWS.d/next/Library/2022-02-01-11-32-47.bpo-46581.t7Zw65.rst +++ b/Misc/NEWS.d/next/Library/2022-02-01-11-32-47.bpo-46581.t7Zw65.rst @@ -1,3 +1,2 @@ -Propagates :attribute:`~typing._GenericAlias._typevar_types` and -:attribute:`~typing._GenericAlias._paramspec_tvars` via -:method:`~typing._GenericAlias.copy_with`. +Brings :class:`ParamSpec` propagation for :class:`GenericAlias` in line with +:class:`Concatenate` (and others). From b222b3d4e1b259acbcfaa7d2c498b3f325cb77d2 Mon Sep 17 00:00:00 2001 From: Matt Bogosian Date: Wed, 9 Mar 2022 08:41:14 -0600 Subject: [PATCH 6/9] Remove redunant in-test imports --- Lib/test/test_typing.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index a614d9db07020c..a1f5accf5c5f74 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -2804,7 +2804,6 @@ def __init__(self, attr: T) -> None: def test_copy_with(self): # bpo-46581 - from typing import Callable, ParamSpec P = ParamSpec('P') original = Callable[P, int] copied = original.copy_with((P, int)) From ad0431d2ee8d06d0304a83276fa25117cdd04760 Mon Sep 17 00:00:00 2001 From: Matt Bogosian Date: Wed, 9 Mar 2022 12:26:22 -0600 Subject: [PATCH 7/9] Recraft test to not depend on internal details --- Lib/test/test_typing.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index a1f5accf5c5f74..0f7c9e7a44f547 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -2802,11 +2802,12 @@ def __init__(self, attr: T) -> None: self.assertEqual(deepcopy(ci).attr, 1) self.assertEqual(ci.__orig_class__, C[int]) - def test_copy_with(self): + def test_parameter_propagation(self): # bpo-46581 P = ParamSpec('P') original = Callable[P, int] - copied = original.copy_with((P, int)) + self.assertEqual(original.__parameters__, (P,)) + copied = original[P] self.assertEqual(original.__parameters__, copied.__parameters__) def test_weakref_all(self): From 6db57f55ef12866fe70547d655dd4b49370a3bb9 Mon Sep 17 00:00:00 2001 From: Matt Bogosian Date: Wed, 9 Mar 2022 18:10:04 -0600 Subject: [PATCH 8/9] Move test to ParamSpecTests --- Lib/test/test_typing.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 0f7c9e7a44f547..3943af51a76954 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -2802,14 +2802,6 @@ def __init__(self, attr: T) -> None: self.assertEqual(deepcopy(ci).attr, 1) self.assertEqual(ci.__orig_class__, C[int]) - def test_parameter_propagation(self): - # bpo-46581 - P = ParamSpec('P') - original = Callable[P, int] - self.assertEqual(original.__parameters__, (P,)) - copied = original[P] - self.assertEqual(original.__parameters__, copied.__parameters__) - def test_weakref_all(self): T = TypeVar('T') things = [Any, Union[T, int], Callable[..., T], Tuple[Any, Any], @@ -5801,6 +5793,14 @@ def test_paramspec_in_nested_generics(self): self.assertEqual(G2[[int, str], float], list[C]) self.assertEqual(G3[[int, str], float], list[C] | int) + def test_paramspec_gets_copied(self): + # bpo-46581 + P = ParamSpec('P') + original = Callable[P, int] + self.assertEqual(original.__parameters__, (P,)) + copied = original[P] + self.assertEqual(original.__parameters__, copied.__parameters__) + class ConcatenateTests(BaseTestCase): def test_basics(self): From 031c6175b548effe35bd673048d1269b22e43727 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 10 Mar 2022 15:16:21 +0200 Subject: [PATCH 9/9] Update Lib/test/test_typing.py --- Lib/test/test_typing.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 3943af51a76954..fc596e4d90b210 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -5796,10 +5796,23 @@ def test_paramspec_in_nested_generics(self): def test_paramspec_gets_copied(self): # bpo-46581 P = ParamSpec('P') - original = Callable[P, int] - self.assertEqual(original.__parameters__, (P,)) - copied = original[P] - self.assertEqual(original.__parameters__, copied.__parameters__) + P2 = ParamSpec('P2') + C1 = Callable[P, int] + self.assertEqual(C1.__parameters__, (P,)) + self.assertEqual(C1[P2].__parameters__, (P2,)) + self.assertEqual(C1[str].__parameters__, ()) + self.assertEqual(C1[str, T].__parameters__, (T,)) + self.assertEqual(C1[Concatenate[str, P2]].__parameters__, (P2,)) + self.assertEqual(C1[Concatenate[T, P2]].__parameters__, (T, P2)) + self.assertEqual(C1[...].__parameters__, ()) + + C2 = Callable[Concatenate[str, P], int] + self.assertEqual(C2.__parameters__, (P,)) + self.assertEqual(C2[P2].__parameters__, (P2,)) + self.assertEqual(C2[str].__parameters__, ()) + self.assertEqual(C2[str, T].__parameters__, (T,)) + self.assertEqual(C2[Concatenate[str, P2]].__parameters__, (P2,)) + self.assertEqual(C2[Concatenate[T, P2]].__parameters__, (T, P2)) class ConcatenateTests(BaseTestCase):