From 92c264045ee941f89f121d32a8adf26803820aae Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Fri, 11 Jun 2021 22:08:22 +0800 Subject: [PATCH 1/9] support most use cases for pep612 with generics --- .../src_py3/test_typing_extensions.py | 51 +++++++++++++++---- .../src_py3/typing_extensions.py | 21 ++++++-- 2 files changed, 58 insertions(+), 14 deletions(-) diff --git a/typing_extensions/src_py3/test_typing_extensions.py b/typing_extensions/src_py3/test_typing_extensions.py index 3f3c2f9e5..fd2a38a9f 100644 --- a/typing_extensions/src_py3/test_typing_extensions.py +++ b/typing_extensions/src_py3/test_typing_extensions.py @@ -2012,25 +2012,33 @@ def test_valid_uses(self): P = ParamSpec('P') T = TypeVar('T') C1 = typing.Callable[P, int] + self.assertEqual(C1.__args__, (P, int)) + self.assertEqual(C1.__parameters__, (P,)) C2 = typing.Callable[P, T] + self.assertEqual(C2.__args__, (P, T)) + self.assertEqual(C2.__parameters__, (P, T)) - # Note: no tests for Callable.__args__ and Callable.__parameters__ here - # because pre-3.10 Callable sees ParamSpec as a plain list, not a - # TypeVar. # Test collections.abc.Callable too. if sys.version_info[:2] >= (3, 9): + # Note: no tests for Callable.__parameters__ here + # because types.GenericAlias Callable is hardcoded to search + # for tp_name "TypeVar" in C. This was changed in 3.10. C3 = collections.abc.Callable[P, int] + self.assertEqual(C3.__args__, (P, int)) C4 = collections.abc.Callable[P, T] + self.assertEqual(C4.__args__, (P, T)) # ParamSpec instances should also have args and kwargs attributes. - self.assertIn('args', dir(P)) - self.assertIn('kwargs', dir(P)) + # Note: not in dir(P) because of __class__ hacks + self.assertTrue(hasattr(P, 'args')) + self.assertTrue(hasattr(P, 'kwargs')) def test_args_kwargs(self): P = ParamSpec('P') - self.assertIn('args', dir(P)) - self.assertIn('kwargs', dir(P)) + # Note: not in dir(P) because of __class__ hacks + self.assertTrue(hasattr(P, 'args')) + self.assertTrue(hasattr(P, 'kwargs')) self.assertIsInstance(P.args, ParamSpecArgs) self.assertIsInstance(P.kwargs, ParamSpecKwargs) self.assertIs(P.args.__origin__, P) @@ -2038,8 +2046,33 @@ def test_args_kwargs(self): self.assertEqual(repr(P.args), "P.args") self.assertEqual(repr(P.kwargs), "P.kwargs") - # Note: ParamSpec doesn't work for pre-3.10 user-defined Generics due - # to type checks inside Generic. + def test_user_generics(self): + T = TypeVar("T") + P = ParamSpec("P") + P_2 = ParamSpec("P_2") + + class X(Generic[T, P]): + f: Callable[P, int] + x: T + + G1 = X[int, P_2] + self.assertEqual(G1.__args__, (int, P_2)) + self.assertEqual(G1.__parameters__, (P_2,)) + + G2 = X[int, Concatenate[int, P_2]] + self.assertEqual(G2.__args__, (int, Concatenate[int, P_2])) + self.assertEqual(G2.__parameters__, (P_2,)) + + # The following are some valid uses cases in PEP 612 that don't work: + # These do not work in 3.9, _type_check blocks the list and ellipsis. + # G3 = X[int, [int, bool]] + # G4 = X[int, ...] + # G5 = Z[[int, str, bool]] + # Not working because this is special-cased in 3.10. + # G6 = Z[int, str, bool] + + class Z(Generic[P]): + f: Callable[P, int] def test_pickle(self): global P, P_co, P_contra diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index 433b15feb..9f180c5b1 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -2329,6 +2329,9 @@ def add_two(x: float, y: float) -> float: be pickled. """ + # Trick Generic __parameters__. + __class__ = TypeVar + @property def args(self): return ParamSpecArgs(self) @@ -2377,14 +2380,13 @@ def __reduce__(self): def __call__(self, *args, **kwargs): pass - # Note: Can't fake ParamSpec as a TypeVar to get it to work - # with Generics. ParamSpec isn't an instance of TypeVar in 3.10. - # So encouraging code like isinstance(ParamSpec('P'), TypeVar)) - # will lead to breakage in 3.10. - # This also means no accurate __parameters__ for GenericAliases. # Inherits from list as a workaround for Callable checks in Python < 3.9.2. class _ConcatenateGenericAlias(list): + + # Trick Generic into looking into this for __parameters__. + __class__ = _GenericAlias + def __init__(self, origin, args): super().__init__(args) self.__origin__ = origin @@ -2399,6 +2401,15 @@ def __repr__(self): def __hash__(self): return hash((self.__origin__, self.__args__)) + # Hack to get typing._type_check to pass in Generic. + def __call__(self, *args, **kwargs): + pass + + @property + def __parameters__(self): + return tuple(tp for tp in self.__args__ if isinstance(tp, (TypeVar, ParamSpec))) + + @_tp_cache def _concatenate_getitem(self, parameters): if parameters == (): From 1dfc2b23bf844345bd68bb8eccaf8b4b93172baa Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Fri, 11 Jun 2021 22:14:53 +0800 Subject: [PATCH 2/9] Update README.rst --- typing_extensions/README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/typing_extensions/README.rst b/typing_extensions/README.rst index 98f621358..4166510a7 100644 --- a/typing_extensions/README.rst +++ b/typing_extensions/README.rst @@ -90,8 +90,8 @@ issues when mixing the differing implementations of modified classes. Certain types have incorrect runtime behavior due to limitations of older versions of the typing module. For example, ``ParamSpec`` and ``Concatenate`` -will not work with ``get_args``, ``get_origin`` or user-defined ``Generic``\ s -because they need to be lists to work with older versions of ``Callable``. +will not work with ``get_args``, ``get_origin``. Certain PEP 612 special cases +in user-defined ``Generic``\ s are also not available. These types are only guaranteed to work for static type checking. Running tests From 379cf924661369c2d8a406d6b715c0c887da4b83 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Fri, 11 Jun 2021 22:36:37 +0800 Subject: [PATCH 3/9] Improve 3.8, 3.6 compatibility --- typing_extensions/src_py3/typing_extensions.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index 9f180c5b1..d05446272 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -2385,7 +2385,13 @@ def __call__(self, *args, **kwargs): class _ConcatenateGenericAlias(list): # Trick Generic into looking into this for __parameters__. - __class__ = _GenericAlias + if OLD_GENERICS: + __class__ = _GenericAlias + else: + __class__ = GenericMeta + + # Flag in 3.8. + _special = False def __init__(self, origin, args): super().__init__(args) @@ -2409,6 +2415,10 @@ def __call__(self, *args, **kwargs): def __parameters__(self): return tuple(tp for tp in self.__args__ if isinstance(tp, (TypeVar, ParamSpec))) + # Only needed in 3.6 and lower. + def _get_type_vars(self, tvars): + if self not in tvars: + tvars.append(self) @_tp_cache def _concatenate_getitem(self, parameters): From 818543076558624e71bb097d5a0f792a4545a949 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Fri, 11 Jun 2021 22:56:55 +0800 Subject: [PATCH 4/9] full compatibility with 3.6 - 3.9 --- .../src_py3/typing_extensions.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index d05446272..d92f4a6a6 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -2380,18 +2380,25 @@ def __reduce__(self): def __call__(self, *args, **kwargs): pass + if not PEP_560: + # Only needed in 3.6 and lower. + def _get_type_vars(self, tvars): + if self not in tvars: + tvars.append(self) # Inherits from list as a workaround for Callable checks in Python < 3.9.2. class _ConcatenateGenericAlias(list): # Trick Generic into looking into this for __parameters__. - if OLD_GENERICS: + if PEP_560: __class__ = _GenericAlias else: - __class__ = GenericMeta + __class__ = typing._TypingBase # Flag in 3.8. _special = False + # Flag in 3.6 + _gorg = Generic def __init__(self, origin, args): super().__init__(args) @@ -2415,10 +2422,11 @@ def __call__(self, *args, **kwargs): def __parameters__(self): return tuple(tp for tp in self.__args__ if isinstance(tp, (TypeVar, ParamSpec))) - # Only needed in 3.6 and lower. - def _get_type_vars(self, tvars): - if self not in tvars: - tvars.append(self) + if not PEP_560: + # Only required in 3.6 and lower. + def _get_type_vars(self, tvars): + if self.__origin__ and self.__parameters__: + typing._get_type_vars(self.__parameters__, tvars) @_tp_cache def _concatenate_getitem(self, parameters): From 9ba681c1d49ab2d2af121f0f11e955868a48f0a1 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Fri, 11 Jun 2021 23:05:06 +0800 Subject: [PATCH 5/9] removed variable annotations; they don't exist in 3.4 and 3.5 --- typing_extensions/src_py3/test_typing_extensions.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/typing_extensions/src_py3/test_typing_extensions.py b/typing_extensions/src_py3/test_typing_extensions.py index fd2a38a9f..2e531d7bc 100644 --- a/typing_extensions/src_py3/test_typing_extensions.py +++ b/typing_extensions/src_py3/test_typing_extensions.py @@ -2052,8 +2052,7 @@ def test_user_generics(self): P_2 = ParamSpec("P_2") class X(Generic[T, P]): - f: Callable[P, int] - x: T + pass G1 = X[int, P_2] self.assertEqual(G1.__args__, (int, P_2)) @@ -2072,7 +2071,7 @@ class X(Generic[T, P]): # G6 = Z[int, str, bool] class Z(Generic[P]): - f: Callable[P, int] + pass def test_pickle(self): global P, P_co, P_contra From 014a70b98e3f284c39a1a71384420378eeaa1488 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Fri, 11 Jun 2021 23:37:13 +0800 Subject: [PATCH 6/9] use TypingMeta instead of _TypingBase as 3.5.2 doesn't have _TypingBase --- typing_extensions/src_py3/typing_extensions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index d92f4a6a6..192536b03 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -2393,12 +2393,12 @@ class _ConcatenateGenericAlias(list): if PEP_560: __class__ = _GenericAlias else: - __class__ = typing._TypingBase + __class__ = typing.TypingMeta # Flag in 3.8. _special = False - # Flag in 3.6 - _gorg = Generic + # Attribute in 3.6 + _gorg = GenericMeta def __init__(self, origin, args): super().__init__(args) From f70e05d861ac368a73bc55999032c402fd340b1e Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Fri, 11 Jun 2021 23:44:54 +0800 Subject: [PATCH 7/9] special case for 3.5.2 --- typing_extensions/src_py3/typing_extensions.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index 192536b03..e4d164404 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -2392,13 +2392,18 @@ class _ConcatenateGenericAlias(list): # Trick Generic into looking into this for __parameters__. if PEP_560: __class__ = _GenericAlias - else: + elif sys.version_info[:3] == (3, 5, 2): __class__ = typing.TypingMeta + else: + __class__ = typing._TypingBase # Flag in 3.8. _special = False - # Attribute in 3.6 - _gorg = GenericMeta + # Attribute in 3.6 and earlier. + if sys.version_info[:3] == (3, 5, 2): + _gorg = typing.GenericMeta + else: + _gorg = typing.Generic def __init__(self, origin, args): super().__init__(args) From 67d204e65ca757ad160774ca560544604c5d2cd6 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sat, 12 Jun 2021 10:48:15 +0800 Subject: [PATCH 8/9] disable broken test on 3.5.2 --- typing_extensions/src_py3/test_typing_extensions.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/typing_extensions/src_py3/test_typing_extensions.py b/typing_extensions/src_py3/test_typing_extensions.py index 2e531d7bc..0dc5366b2 100644 --- a/typing_extensions/src_py3/test_typing_extensions.py +++ b/typing_extensions/src_py3/test_typing_extensions.py @@ -2012,7 +2012,10 @@ def test_valid_uses(self): P = ParamSpec('P') T = TypeVar('T') C1 = typing.Callable[P, int] - self.assertEqual(C1.__args__, (P, int)) + # Callable in Python 3.5.2 might be bugged when collecting __args__. + # https://github.com/python/cpython/blob/91185fe0284a04162e0b3425b53be49bdbfad67d/Lib/typing.py#L1026 + if not sys.version_info[:3] == (3, 5, 2): + self.assertEqual(C1.__args__, (P, int)) self.assertEqual(C1.__parameters__, (P,)) C2 = typing.Callable[P, T] self.assertEqual(C2.__args__, (P, T)) From f4aa1443c5e899b35e1a9a130a6696a398e89856 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sat, 12 Jun 2021 10:51:46 +0800 Subject: [PATCH 9/9] disable more failing tests for 3.5.2 --- typing_extensions/src_py3/test_typing_extensions.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/typing_extensions/src_py3/test_typing_extensions.py b/typing_extensions/src_py3/test_typing_extensions.py index 0dc5366b2..06d4cc40a 100644 --- a/typing_extensions/src_py3/test_typing_extensions.py +++ b/typing_extensions/src_py3/test_typing_extensions.py @@ -2014,12 +2014,14 @@ def test_valid_uses(self): C1 = typing.Callable[P, int] # Callable in Python 3.5.2 might be bugged when collecting __args__. # https://github.com/python/cpython/blob/91185fe0284a04162e0b3425b53be49bdbfad67d/Lib/typing.py#L1026 - if not sys.version_info[:3] == (3, 5, 2): + PY_3_5_2 = sys.version_info[:3] == (3, 5, 2) + if not PY_3_5_2: self.assertEqual(C1.__args__, (P, int)) - self.assertEqual(C1.__parameters__, (P,)) + self.assertEqual(C1.__parameters__, (P,)) C2 = typing.Callable[P, T] - self.assertEqual(C2.__args__, (P, T)) - self.assertEqual(C2.__parameters__, (P, T)) + if not PY_3_5_2: + self.assertEqual(C2.__args__, (P, T)) + self.assertEqual(C2.__parameters__, (P, T)) # Test collections.abc.Callable too.