Skip to content

Commit a114379

Browse files
Support most use cases for PEP 612 with Generic (#817)
1 parent 2de0a93 commit a114379

File tree

3 files changed

+87
-16
lines changed

3 files changed

+87
-16
lines changed

typing_extensions/README.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ issues when mixing the differing implementations of modified classes.
9090

9191
Certain types have incorrect runtime behavior due to limitations of older
9292
versions of the typing module. For example, ``ParamSpec`` and ``Concatenate``
93-
will not work with ``get_args``, ``get_origin`` or user-defined ``Generic``\ s
94-
because they need to be lists to work with older versions of ``Callable``.
93+
will not work with ``get_args``, ``get_origin``. Certain PEP 612 special cases
94+
in user-defined ``Generic``\ s are also not available.
9595
These types are only guaranteed to work for static type checking.
9696

9797
Running tests

typing_extensions/src_py3/test_typing_extensions.py

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2012,34 +2012,71 @@ def test_valid_uses(self):
20122012
P = ParamSpec('P')
20132013
T = TypeVar('T')
20142014
C1 = typing.Callable[P, int]
2015+
# Callable in Python 3.5.2 might be bugged when collecting __args__.
2016+
# https://github.com/python/cpython/blob/91185fe0284a04162e0b3425b53be49bdbfad67d/Lib/typing.py#L1026
2017+
PY_3_5_2 = sys.version_info[:3] == (3, 5, 2)
2018+
if not PY_3_5_2:
2019+
self.assertEqual(C1.__args__, (P, int))
2020+
self.assertEqual(C1.__parameters__, (P,))
20152021
C2 = typing.Callable[P, T]
2022+
if not PY_3_5_2:
2023+
self.assertEqual(C2.__args__, (P, T))
2024+
self.assertEqual(C2.__parameters__, (P, T))
20162025

2017-
# Note: no tests for Callable.__args__ and Callable.__parameters__ here
2018-
# because pre-3.10 Callable sees ParamSpec as a plain list, not a
2019-
# TypeVar.
20202026

20212027
# Test collections.abc.Callable too.
20222028
if sys.version_info[:2] >= (3, 9):
2029+
# Note: no tests for Callable.__parameters__ here
2030+
# because types.GenericAlias Callable is hardcoded to search
2031+
# for tp_name "TypeVar" in C. This was changed in 3.10.
20232032
C3 = collections.abc.Callable[P, int]
2033+
self.assertEqual(C3.__args__, (P, int))
20242034
C4 = collections.abc.Callable[P, T]
2035+
self.assertEqual(C4.__args__, (P, T))
20252036

20262037
# ParamSpec instances should also have args and kwargs attributes.
2027-
self.assertIn('args', dir(P))
2028-
self.assertIn('kwargs', dir(P))
2038+
# Note: not in dir(P) because of __class__ hacks
2039+
self.assertTrue(hasattr(P, 'args'))
2040+
self.assertTrue(hasattr(P, 'kwargs'))
20292041

20302042
def test_args_kwargs(self):
20312043
P = ParamSpec('P')
2032-
self.assertIn('args', dir(P))
2033-
self.assertIn('kwargs', dir(P))
2044+
# Note: not in dir(P) because of __class__ hacks
2045+
self.assertTrue(hasattr(P, 'args'))
2046+
self.assertTrue(hasattr(P, 'kwargs'))
20342047
self.assertIsInstance(P.args, ParamSpecArgs)
20352048
self.assertIsInstance(P.kwargs, ParamSpecKwargs)
20362049
self.assertIs(P.args.__origin__, P)
20372050
self.assertIs(P.kwargs.__origin__, P)
20382051
self.assertEqual(repr(P.args), "P.args")
20392052
self.assertEqual(repr(P.kwargs), "P.kwargs")
20402053

2041-
# Note: ParamSpec doesn't work for pre-3.10 user-defined Generics due
2042-
# to type checks inside Generic.
2054+
def test_user_generics(self):
2055+
T = TypeVar("T")
2056+
P = ParamSpec("P")
2057+
P_2 = ParamSpec("P_2")
2058+
2059+
class X(Generic[T, P]):
2060+
pass
2061+
2062+
G1 = X[int, P_2]
2063+
self.assertEqual(G1.__args__, (int, P_2))
2064+
self.assertEqual(G1.__parameters__, (P_2,))
2065+
2066+
G2 = X[int, Concatenate[int, P_2]]
2067+
self.assertEqual(G2.__args__, (int, Concatenate[int, P_2]))
2068+
self.assertEqual(G2.__parameters__, (P_2,))
2069+
2070+
# The following are some valid uses cases in PEP 612 that don't work:
2071+
# These do not work in 3.9, _type_check blocks the list and ellipsis.
2072+
# G3 = X[int, [int, bool]]
2073+
# G4 = X[int, ...]
2074+
# G5 = Z[[int, str, bool]]
2075+
# Not working because this is special-cased in 3.10.
2076+
# G6 = Z[int, str, bool]
2077+
2078+
class Z(Generic[P]):
2079+
pass
20432080

20442081
def test_pickle(self):
20452082
global P, P_co, P_contra

typing_extensions/src_py3/typing_extensions.py

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2329,6 +2329,9 @@ def add_two(x: float, y: float) -> float:
23292329
be pickled.
23302330
"""
23312331

2332+
# Trick Generic __parameters__.
2333+
__class__ = TypeVar
2334+
23322335
@property
23332336
def args(self):
23342337
return ParamSpecArgs(self)
@@ -2377,14 +2380,31 @@ def __reduce__(self):
23772380
def __call__(self, *args, **kwargs):
23782381
pass
23792382

2380-
# Note: Can't fake ParamSpec as a TypeVar to get it to work
2381-
# with Generics. ParamSpec isn't an instance of TypeVar in 3.10.
2382-
# So encouraging code like isinstance(ParamSpec('P'), TypeVar))
2383-
# will lead to breakage in 3.10.
2384-
# This also means no accurate __parameters__ for GenericAliases.
2383+
if not PEP_560:
2384+
# Only needed in 3.6 and lower.
2385+
def _get_type_vars(self, tvars):
2386+
if self not in tvars:
2387+
tvars.append(self)
23852388

23862389
# Inherits from list as a workaround for Callable checks in Python < 3.9.2.
23872390
class _ConcatenateGenericAlias(list):
2391+
2392+
# Trick Generic into looking into this for __parameters__.
2393+
if PEP_560:
2394+
__class__ = _GenericAlias
2395+
elif sys.version_info[:3] == (3, 5, 2):
2396+
__class__ = typing.TypingMeta
2397+
else:
2398+
__class__ = typing._TypingBase
2399+
2400+
# Flag in 3.8.
2401+
_special = False
2402+
# Attribute in 3.6 and earlier.
2403+
if sys.version_info[:3] == (3, 5, 2):
2404+
_gorg = typing.GenericMeta
2405+
else:
2406+
_gorg = typing.Generic
2407+
23882408
def __init__(self, origin, args):
23892409
super().__init__(args)
23902410
self.__origin__ = origin
@@ -2399,6 +2419,20 @@ def __repr__(self):
23992419
def __hash__(self):
24002420
return hash((self.__origin__, self.__args__))
24012421

2422+
# Hack to get typing._type_check to pass in Generic.
2423+
def __call__(self, *args, **kwargs):
2424+
pass
2425+
2426+
@property
2427+
def __parameters__(self):
2428+
return tuple(tp for tp in self.__args__ if isinstance(tp, (TypeVar, ParamSpec)))
2429+
2430+
if not PEP_560:
2431+
# Only required in 3.6 and lower.
2432+
def _get_type_vars(self, tvars):
2433+
if self.__origin__ and self.__parameters__:
2434+
typing._get_type_vars(self.__parameters__, tvars)
2435+
24022436
@_tp_cache
24032437
def _concatenate_getitem(self, parameters):
24042438
if parameters == ():

0 commit comments

Comments
 (0)