Skip to content

Commit 386a8a6

Browse files
committed
Merge branch 'main' into ParamSpec/Generic-simple
2 parents 3ea6b3c + f9a2055 commit 386a8a6

File tree

3 files changed

+225
-51
lines changed

3 files changed

+225
-51
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ aliases that have a `Concatenate` special form as their argument.
2929
- Fix that lists and ... could not be used for parameter expressions for `TypeAliasType`
3030
instances before Python 3.11.
3131
Patch by [Daraan](https://github.com/Daraan).
32+
- Fix error on Python 3.10 when using `typing.Concatenate` and
33+
`typing_extensions.Concatenate` together. Patch by [Daraan](https://github.com/Daraan).
3234

3335
# Release 4.12.2 (June 7, 2024)
3436

src/test_typing_extensions.py

Lines changed: 77 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5307,6 +5307,7 @@ class ProtoZ(Protocol[P]):
53075307
G6 = klass[int, str, T]
53085308
G6args = G6.__args__[0] if sys.version_info >= (3, 10) else G6.__args__
53095309
self.assertEqual(G6args, (int, str, T))
5310+
self.assertEqual(G6.__parameters__, (T,))
53105311

53115312
# P = [int]
53125313
G7 = klass[int]
@@ -5350,54 +5351,54 @@ class ProtoZ(Protocol[P]):
53505351
things = "arguments" if sys.version_info >= (3, 10) else "parameters"
53515352
for klass in Z, ProtoZ:
53525353
with self.subTest(klass=klass.__name__):
5353-
G6 = klass[int, str, T]
53545354
G8 = klass[Concatenate[T, ...]]
5355+
5356+
H8_1 = G8[int]
5357+
self.assertEqual(H8_1.__parameters__, ())
5358+
with self.assertRaisesRegex(TypeError, "not a generic class"):
5359+
H8_1[str]
5360+
5361+
H8_2 = G8[T][int]
5362+
self.assertEqual(H8_2.__parameters__, ())
5363+
with self.assertRaisesRegex(TypeError, "not a generic class"):
5364+
H8_2[str]
5365+
53555366
G9 = klass[Concatenate[T, P_2]]
5356-
G10 = klass[int, Concatenate[str, P]]
5367+
self.assertEqual(G9.__parameters__, (T, P_2))
53575368

5358-
with self.subTest("Check generic substitution", klass=klass.__name__):
5359-
if sys.version_info < (3, 10):
5360-
self.skipTest("_ConcatenateGenericAlias not subscriptable")
5361-
with self.assertRaisesRegex(TypeError, "Expected a list of types, an ellipsis, ParamSpec, or Concatenate"):
5362-
G9[int, int]
5369+
with self.assertRaisesRegex(TypeError,
5370+
"The last parameter to Concatenate should be a ParamSpec variable or ellipsis."
5371+
if sys.version_info < (3, 10) else
5372+
# from __typing_subst__
5373+
"Expected a list of types, an ellipsis, ParamSpec, or Concatenate"
5374+
):
5375+
G9[int, int]
5376+
5377+
with self.assertRaisesRegex(TypeError, f"Too few {things}"):
5378+
G9[int]
53635379

53645380
with self.subTest("Check list as parameter expression", klass=klass.__name__):
53655381
if sys.version_info < (3, 10):
53665382
self.skipTest("Cannot pass non-types")
53675383
G5 = klass[[int, str, T]]
5384+
self.assertEqual(G5.__parameters__, (T,))
53685385
self.assertEqual(G5.__args__, ((int, str, T),))
5386+
53695387
H9 = G9[int, [T]]
5388+
self.assertEqual(H9.__parameters__, (T,))
53705389

5371-
self.assertEqual(G9.__parameters__, (T, P_2))
5372-
with self.subTest("Check parametrization", klass=klass.__name__):
5373-
if sys.version_info[:2] == (3, 10):
5374-
self.skipTest("Parameter detection fails in 3.10")
5375-
with self.assertRaisesRegex(TypeError, f"Too few {things}"):
5376-
G9[int] # for python 3.10 this has no parameters
5377-
self.assertEqual(G6.__parameters__, (T,))
5378-
if sys.version_info >= (3, 10): # skipped above
5379-
self.assertEqual(G5.__parameters__, (T,))
5380-
self.assertEqual(H9.__parameters__, (T,))
5381-
5382-
with self.subTest("Check further substitution", klass=klass.__name__):
5383-
if sys.version_info < (3, 10):
5384-
self.skipTest("_ConcatenateGenericAlias not subscriptable")
5385-
if sys.version_info[:2] == (3, 10):
5386-
self.skipTest("Parameter detection fails in 3.10")
5387-
if (3, 11, 0) <= sys.version_info[:3] < (3, 11, 3):
5388-
self.skipTest("Wrong recursive substitution")
5389-
H1 = G8[int]
5390-
self.assertEqual(H1.__parameters__, ())
5391-
with self.assertRaisesRegex(TypeError, "not a generic class"):
5392-
H1[str] # for python 3.11.0-3 this still has a parameter
5393-
5394-
H2 = G8[T][int]
5395-
self.assertEqual(H2.__parameters__, ())
5396-
with self.assertRaisesRegex(TypeError, "not a generic class"):
5397-
H2[str] # for python 3.11.0-3 this still has a parameter
5398-
5399-
H3 = G10[int]
5400-
self.assertEqual(H3.__args__, ((int, (str, int)),))
5390+
# This is an invalid parameter expression but useful for testing correct subsitution
5391+
G10 = klass[int, Concatenate[str, P]]
5392+
with self.subTest("Check invalid form substitution"):
5393+
self.assertEqual(G10.__parameters__, (P, ))
5394+
if sys.version_info < (3, 9):
5395+
self.skipTest("3.8 typing._type_subst does not support this substitution process")
5396+
H10 = G10[int]
5397+
if (3, 10) <= sys.version_info < (3, 11, 3):
5398+
self.skipTest("3.10-3.11.2 does not substitute Concatenate here")
5399+
self.assertEqual(H10.__parameters__, ())
5400+
H10args = H10.__args__[0] if sys.version_info >= (3, 10) else H10.__args__
5401+
self.assertEqual(H10args, (int, (str, int)))
54015402

54025403
@skipUnless(TYPING_3_10_0, "ParamSpec not present before 3.10")
54035404
def test_substitution_with_typing_variants(self):
@@ -5503,6 +5504,19 @@ class MyClass: ...
55035504
self.assertNotEqual(d, c)
55045505
self.assertNotEqual(d, Concatenate)
55055506

5507+
@skipUnless(TYPING_3_10_0, "Concatenate not available in <3.10")
5508+
def test_typing_compatibility(self):
5509+
P = ParamSpec('P')
5510+
C1 = Concatenate[int, P][typing.Concatenate[int, P]]
5511+
self.assertEqual(C1, Concatenate[int, int, P])
5512+
self.assertEqual(get_args(C1), (int, int, P))
5513+
5514+
C2 = typing.Concatenate[int, P][Concatenate[int, P]]
5515+
with self.subTest("typing compatibility with typing_extensions"):
5516+
if sys.version_info < (3, 10, 3):
5517+
self.skipTest("Unpacking not introduced until 3.10.3")
5518+
self.assertEqual(get_args(C2), (int, int, P))
5519+
55065520
def test_valid_uses(self):
55075521
P = ParamSpec('P')
55085522
T = TypeVar('T')
@@ -5598,6 +5612,32 @@ def test_eq(self):
55985612
self.assertEqual(hash(C4), hash(C5))
55995613
self.assertNotEqual(C4, C6)
56005614

5615+
def test_substitution(self):
5616+
T = TypeVar('T')
5617+
P = ParamSpec('P')
5618+
Ts = TypeVarTuple("Ts")
5619+
5620+
C1 = Concatenate[str, T, ...]
5621+
self.assertEqual(C1[int], Concatenate[str, int, ...])
5622+
5623+
C2 = Concatenate[str, P]
5624+
self.assertEqual(C2[...], Concatenate[str, ...])
5625+
self.assertEqual(C2[int], (str, int))
5626+
U1 = Unpack[Tuple[int, str]]
5627+
U2 = Unpack[Ts]
5628+
self.assertEqual(C2[U1], (str, int, str))
5629+
self.assertEqual(C2[U2], (str, Unpack[Ts]))
5630+
self.assertEqual(C2["U2"], (str, typing.ForwardRef("U2")))
5631+
5632+
if (3, 12, 0) <= sys.version_info < (3, 12, 4):
5633+
with self.assertRaises(AssertionError):
5634+
C2[Unpack[U2]]
5635+
else:
5636+
with self.assertRaisesRegex(TypeError, "must be used with a tuple type"):
5637+
C2[Unpack[U2]]
5638+
5639+
C3 = Concatenate[str, T, P]
5640+
self.assertEqual(C3[int, [bool]], (str, int, bool))
56015641

56025642
@skipUnless(TYPING_3_10_0, "Concatenate not present before 3.10")
56035643
def test_is_param_expr(self):

src/typing_extensions.py

Lines changed: 146 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1765,6 +1765,23 @@ def __call__(self, *args, **kwargs):
17651765
# 3.8-3.9
17661766
if not hasattr(typing, 'Concatenate'):
17671767
# Inherits from list as a workaround for Callable checks in Python < 3.9.2.
1768+
1769+
# 3.9.0-1
1770+
if not hasattr(typing, '_type_convert'):
1771+
def _type_convert(arg, module=None, *, allow_special_forms=False):
1772+
"""For converting None to type(None), and strings to ForwardRef."""
1773+
if arg is None:
1774+
return type(None)
1775+
if isinstance(arg, str):
1776+
if sys.version_info <= (3, 9, 6):
1777+
return ForwardRef(arg)
1778+
if sys.version_info <= (3, 9, 7):
1779+
return ForwardRef(arg, module=module)
1780+
return ForwardRef(arg, module=module, is_class=allow_special_forms)
1781+
return arg
1782+
else:
1783+
_type_convert = typing._type_convert
1784+
17681785
class _ConcatenateGenericAlias(list):
17691786

17701787
# Trick Generic into looking into this for __parameters__.
@@ -1795,27 +1812,122 @@ def __parameters__(self):
17951812
return tuple(
17961813
tp for tp in self.__args__ if isinstance(tp, (typing.TypeVar, ParamSpec))
17971814
)
1815+
1816+
# 3.8; needed for typing._subst_tvars
1817+
# 3.9 used by __getitem__ below
1818+
def copy_with(self, params):
1819+
if isinstance(params[-1], _ConcatenateGenericAlias):
1820+
params = (*params[:-1], *params[-1].__args__)
1821+
elif isinstance(params[-1], (list, tuple)):
1822+
return (*params[:-1], *params[-1])
1823+
elif (not (params[-1] is ... or isinstance(params[-1], ParamSpec))):
1824+
raise TypeError("The last parameter to Concatenate should be a "
1825+
"ParamSpec variable or ellipsis.")
1826+
return self.__class__(self.__origin__, params)
1827+
1828+
# 3.9; accessed during GenericAlias.__getitem__ when substituting
1829+
def __getitem__(self, args):
1830+
if self.__origin__ in (Generic, Protocol):
1831+
# Can't subscript Generic[...] or Protocol[...].
1832+
raise TypeError(f"Cannot subscript already-subscripted {self}")
1833+
if not self.__parameters__:
1834+
raise TypeError(f"{self} is not a generic class")
1835+
1836+
if not isinstance(args, tuple):
1837+
args = (args,)
1838+
args = _unpack_args(*(_type_convert(p) for p in args))
1839+
params = self.__parameters__
1840+
for param in params:
1841+
prepare = getattr(param, "__typing_prepare_subst__", None)
1842+
if prepare is not None:
1843+
args = prepare(self, args)
1844+
# 3.8 - 3.9 & typing.ParamSpec
1845+
elif isinstance(param, ParamSpec):
1846+
i = params.index(param)
1847+
if (
1848+
i == len(args)
1849+
and getattr(param, '__default__', NoDefault) is not NoDefault
1850+
):
1851+
args = [*args, param.__default__]
1852+
if i >= len(args):
1853+
raise TypeError(f"Too few arguments for {self}")
1854+
# Special case for Z[[int, str, bool]] == Z[int, str, bool]
1855+
if len(params) == 1 and not _is_param_expr(args[0]):
1856+
assert i == 0
1857+
args = (args,)
1858+
elif (
1859+
isinstance(args[i], list)
1860+
# 3.8 - 3.9
1861+
# This class inherits from list do not convert
1862+
and not isinstance(args[i], _ConcatenateGenericAlias)
1863+
):
1864+
args = (*args[:i], tuple(args[i]), *args[i + 1:])
1865+
1866+
alen = len(args)
1867+
plen = len(params)
1868+
if alen != plen:
1869+
raise TypeError(
1870+
f"Too {'many' if alen > plen else 'few'} arguments for {self};"
1871+
f" actual {alen}, expected {plen}"
1872+
)
1873+
1874+
subst = dict(zip(self.__parameters__, args))
1875+
# determine new args
1876+
new_args = []
1877+
for arg in self.__args__:
1878+
if isinstance(arg, type):
1879+
new_args.append(arg)
1880+
continue
1881+
if isinstance(arg, TypeVar):
1882+
arg = subst[arg]
1883+
if (
1884+
(isinstance(arg, typing._GenericAlias) and _is_unpack(arg))
1885+
or (
1886+
hasattr(_types, "GenericAlias")
1887+
and isinstance(arg, _types.GenericAlias)
1888+
and getattr(arg, "__unpacked__", False)
1889+
)
1890+
):
1891+
raise TypeError(f"{arg} is not valid as type argument")
1892+
1893+
elif isinstance(arg,
1894+
typing._GenericAlias
1895+
if not hasattr(_types, "GenericAlias") else
1896+
(typing._GenericAlias, _types.GenericAlias)
1897+
):
1898+
subparams = arg.__parameters__
1899+
if subparams:
1900+
subargs = tuple(subst[x] for x in subparams)
1901+
arg = arg[subargs]
1902+
new_args.append(arg)
1903+
return self.copy_with(tuple(new_args))
1904+
17981905
# 3.10+
17991906
else:
18001907
_ConcatenateGenericAlias = typing._ConcatenateGenericAlias
18011908

18021909
# 3.10
18031910
if sys.version_info < (3, 11):
1804-
_typing_ConcatenateGenericAlias = _ConcatenateGenericAlias
18051911

1806-
class _ConcatenateGenericAlias(_typing_ConcatenateGenericAlias, _root=True):
1912+
class _ConcatenateGenericAlias(typing._ConcatenateGenericAlias, _root=True):
18071913
# needed for checks in collections.abc.Callable to accept this class
18081914
__module__ = "typing"
18091915

18101916
def copy_with(self, params):
18111917
if isinstance(params[-1], (list, tuple)):
18121918
return (*params[:-1], *params[-1])
1813-
if isinstance(params[-1], _ConcatenateGenericAlias):
1919+
if isinstance(params[-1], typing._ConcatenateGenericAlias):
18141920
params = (*params[:-1], *params[-1].__args__)
18151921
elif not (params[-1] is ... or isinstance(params[-1], ParamSpec)):
18161922
raise TypeError("The last parameter to Concatenate should be a "
18171923
"ParamSpec variable or ellipsis.")
1818-
return super(_typing_ConcatenateGenericAlias, self).copy_with(params)
1924+
return super(typing._ConcatenateGenericAlias, self).copy_with(params)
1925+
1926+
def __getitem__(self, args):
1927+
value = super().__getitem__(args)
1928+
if isinstance(value, tuple) and any(_is_unpack(t) for t in value):
1929+
return tuple(_unpack_args(*(n for n in value)))
1930+
return value
18191931

18201932

18211933
# 3.8-3.9.2
@@ -2496,6 +2608,17 @@ def _is_unpack(obj):
24962608
class _UnpackAlias(typing._GenericAlias, _root=True):
24972609
__class__ = typing.TypeVar
24982610

2611+
@property
2612+
def __typing_unpacked_tuple_args__(self):
2613+
assert self.__origin__ is Unpack
2614+
assert len(self.__args__) == 1
2615+
arg, = self.__args__
2616+
if isinstance(arg, typing._GenericAlias):
2617+
if arg.__origin__ is not tuple:
2618+
raise TypeError("Unpack[...] must be used with a tuple type")
2619+
return arg.__args__
2620+
return None
2621+
24992622
@property
25002623
def __typing_is_unpacked_typevartuple__(self):
25012624
assert self.__origin__ is Unpack
@@ -2519,21 +2642,22 @@ def _is_unpack(obj):
25192642
return isinstance(obj, _UnpackAlias)
25202643

25212644

2645+
def _unpack_args(*args):
2646+
newargs = []
2647+
for arg in args:
2648+
subargs = getattr(arg, '__typing_unpacked_tuple_args__', None)
2649+
if subargs is not None and (not (subargs and subargs[-1] is ...)):
2650+
newargs.extend(subargs)
2651+
else:
2652+
newargs.append(arg)
2653+
return newargs
2654+
2655+
25222656
if _PEP_696_IMPLEMENTED:
25232657
from typing import TypeVarTuple
25242658

25252659
elif hasattr(typing, "TypeVarTuple"): # 3.11+
25262660

2527-
def _unpack_args(*args):
2528-
newargs = []
2529-
for arg in args:
2530-
subargs = getattr(arg, '__typing_unpacked_tuple_args__', None)
2531-
if subargs is not None and not (subargs and subargs[-1] is ...):
2532-
newargs.extend(subargs)
2533-
else:
2534-
newargs.append(arg)
2535-
return newargs
2536-
25372661
# Add default parameter - PEP 696
25382662
class TypeVarTuple(metaclass=_TypeVarLikeMeta):
25392663
"""Type variable tuple."""
@@ -3024,6 +3148,7 @@ def _is_param_expr(arg):
30243148
),
30253149
)
30263150

3151+
30273152
# We have to do some monkey patching to deal with the dual nature of
30283153
# Unpack/TypeVarTuple:
30293154
# - We want Unpack to be a kind of TypeVar so it gets accepted in
@@ -3199,6 +3324,13 @@ def _collect_type_vars(types, typevar_types=None):
31993324
tvars.append(t)
32003325
if _should_collect_from_parameters(t):
32013326
tvars.extend([t for t in t.__parameters__ if t not in tvars])
3327+
elif isinstance(t, tuple):
3328+
# Collect nested type_vars
3329+
# tuple wrapped by _prepare_paramspec_params(cls, params)
3330+
for x in t:
3331+
for collected in _collect_type_vars([x]):
3332+
if collected not in tvars:
3333+
tvars.append(collected)
32023334
return tuple(tvars)
32033335

32043336
typing._collect_type_vars = _collect_type_vars

0 commit comments

Comments
 (0)