Skip to content

Commit

Permalink
bpo-44796: Unify TypeVar and ParamSpec substitution (GH-31143)
Browse files Browse the repository at this point in the history
Add methods __typing_subst__() in TypeVar and ParamSpec.
Simplify code by using more object-oriented approach, especially
the C code for types.GenericAlias and the Python code for
collections.abc.Callable.
  • Loading branch information
serhiy-storchaka committed Mar 11, 2022
1 parent 2d5835a commit b6a5d85
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 152 deletions.
1 change: 1 addition & 0 deletions Include/internal/pycore_global_strings.h
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(__subclasshook__)
STRUCT_FOR_ID(__truediv__)
STRUCT_FOR_ID(__trunc__)
STRUCT_FOR_ID(__typing_subst__)
STRUCT_FOR_ID(__warningregistry__)
STRUCT_FOR_ID(__weakref__)
STRUCT_FOR_ID(__xor__)
Expand Down
1 change: 1 addition & 0 deletions Include/internal/pycore_runtime_init.h
Original file line number Diff line number Diff line change
Expand Up @@ -822,6 +822,7 @@ extern "C" {
INIT_ID(__subclasshook__), \
INIT_ID(__truediv__), \
INIT_ID(__trunc__), \
INIT_ID(__typing_subst__), \
INIT_ID(__warningregistry__), \
INIT_ID(__weakref__), \
INIT_ID(__xor__), \
Expand Down
63 changes: 9 additions & 54 deletions Lib/_collections_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,25 +430,13 @@ def __new__(cls, origin, args):
raise TypeError(
"Callable must be used as Callable[[arg, ...], result].")
t_args, t_result = args
if isinstance(t_args, list):
if isinstance(t_args, (tuple, list)):
args = (*t_args, t_result)
elif not _is_param_expr(t_args):
raise TypeError(f"Expected a list of types, an ellipsis, "
f"ParamSpec, or Concatenate. Got {t_args}")
return super().__new__(cls, origin, args)

@property
def __parameters__(self):
params = []
for arg in self.__args__:
# Looks like a genericalias
if hasattr(arg, "__parameters__") and isinstance(arg.__parameters__, tuple):
params.extend(arg.__parameters__)
else:
if _is_typevarlike(arg):
params.append(arg)
return tuple(dict.fromkeys(params))

def __repr__(self):
if len(self.__args__) == 2 and _is_param_expr(self.__args__[0]):
return super().__repr__()
Expand All @@ -468,57 +456,24 @@ def __getitem__(self, item):
# code is copied from typing's _GenericAlias and the builtin
# types.GenericAlias.

# A special case in PEP 612 where if X = Callable[P, int],
# then X[int, str] == X[[int, str]].
param_len = len(self.__parameters__)
if param_len == 0:
raise TypeError(f'{self} is not a generic class')
if not isinstance(item, tuple):
item = (item,)
if (param_len == 1 and _is_param_expr(self.__parameters__[0])
# A special case in PEP 612 where if X = Callable[P, int],
# then X[int, str] == X[[int, str]].
if (len(self.__parameters__) == 1
and _is_param_expr(self.__parameters__[0])
and item and not _is_param_expr(item[0])):
item = (list(item),)
item_len = len(item)
if item_len != param_len:
raise TypeError(f'Too {"many" if item_len > param_len else "few"}'
f' arguments for {self};'
f' actual {item_len}, expected {param_len}')
subst = dict(zip(self.__parameters__, item))
new_args = []
for arg in self.__args__:
if _is_typevarlike(arg):
if _is_param_expr(arg):
arg = subst[arg]
if not _is_param_expr(arg):
raise TypeError(f"Expected a list of types, an ellipsis, "
f"ParamSpec, or Concatenate. Got {arg}")
else:
arg = subst[arg]
# Looks like a GenericAlias
elif hasattr(arg, '__parameters__') and isinstance(arg.__parameters__, tuple):
subparams = arg.__parameters__
if subparams:
subargs = tuple(subst[x] for x in subparams)
arg = arg[subargs]
if isinstance(arg, tuple):
new_args.extend(arg)
else:
new_args.append(arg)
item = (item,)

new_args = super().__getitem__(item).__args__

# args[0] occurs due to things like Z[[int, str, bool]] from PEP 612
if not isinstance(new_args[0], list):
if not isinstance(new_args[0], (tuple, list)):
t_result = new_args[-1]
t_args = new_args[:-1]
new_args = (t_args, t_result)
return _CallableGenericAlias(Callable, tuple(new_args))


def _is_typevarlike(arg):
obj = type(arg)
# looks like a TypeVar/ParamSpec
return (obj.__module__ == 'typing'
and obj.__name__ in {'ParamSpec', 'TypeVar'})

def _is_param_expr(obj):
"""Checks if obj matches either a list of types, ``...``, ``ParamSpec`` or
``_ConcatenateGenericAlias`` from typing.py
Expand Down
60 changes: 40 additions & 20 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,10 +360,31 @@ def test_no_bivariant(self):
with self.assertRaises(ValueError):
TypeVar('T', covariant=True, contravariant=True)

def test_var_substitution(self):
T = TypeVar('T')
subst = T.__typing_subst__
self.assertIs(subst(int), int)
self.assertEqual(subst(list[int]), list[int])
self.assertEqual(subst(List[int]), List[int])
self.assertEqual(subst(List), List)
self.assertIs(subst(Any), Any)
self.assertIs(subst(None), type(None))
self.assertIs(subst(T), T)
self.assertEqual(subst(int|str), int|str)
self.assertEqual(subst(Union[int, str]), Union[int, str])

def test_bad_var_substitution(self):
T = TypeVar('T')
for arg in (), (int, str):
P = ParamSpec("P")
bad_args = (
42, ..., [int], (), (int, str), Union,
Generic, Generic[T], Protocol, Protocol[T],
Final, Final[int], ClassVar, ClassVar[int],
)
for arg in bad_args:
with self.subTest(arg=arg):
with self.assertRaises(TypeError):
T.__typing_subst__(arg)
with self.assertRaises(TypeError):
List[T][arg]
with self.assertRaises(TypeError):
Expand Down Expand Up @@ -1110,8 +1131,7 @@ def test_var_substitution(self):
C2 = Callable[[KT, T], VT]
C3 = Callable[..., T]
self.assertEqual(C1[str], Callable[[int, str], str])
if Callable is typing.Callable:
self.assertEqual(C1[None], Callable[[int, type(None)], type(None)])
self.assertEqual(C1[None], Callable[[int, type(None)], type(None)])
self.assertEqual(C2[int, float, str], Callable[[int, float], str])
self.assertEqual(C3[int], Callable[..., int])
self.assertEqual(C3[NoReturn], Callable[..., NoReturn])
Expand Down Expand Up @@ -2696,7 +2716,10 @@ def test_all_repr_eq_any(self):
for obj in objs:
self.assertNotEqual(repr(obj), '')
self.assertEqual(obj, obj)
if getattr(obj, '__parameters__', None) and len(obj.__parameters__) == 1:
if (getattr(obj, '__parameters__', None)
and not isinstance(obj, typing.TypeVar)
and isinstance(obj.__parameters__, tuple)
and len(obj.__parameters__) == 1):
self.assertEqual(obj[Any].__args__, (Any,))
if isinstance(obj, type):
for base in obj.__mro__:
Expand Down Expand Up @@ -5748,33 +5771,30 @@ class X(Generic[P, P2]):
self.assertEqual(G1.__args__, ((int, str), (bytes,)))
self.assertEqual(G2.__args__, ((int,), (str, bytes)))

def test_var_substitution(self):
T = TypeVar("T")
P = ParamSpec("P")
subst = P.__typing_subst__
self.assertEqual(subst((int, str)), (int, str))
self.assertEqual(subst([int, str]), (int, str))
self.assertEqual(subst([None]), (type(None),))
self.assertIs(subst(...), ...)
self.assertIs(subst(P), P)
self.assertEqual(subst(Concatenate[int, P]), Concatenate[int, P])

def test_bad_var_substitution(self):
T = TypeVar('T')
P = ParamSpec('P')
bad_args = (42, int, None, T, int|str, Union[int, str])
for arg in bad_args:
with self.subTest(arg=arg):
with self.assertRaises(TypeError):
P.__typing_subst__(arg)
with self.assertRaises(TypeError):
typing.Callable[P, T][arg, str]
with self.assertRaises(TypeError):
collections.abc.Callable[P, T][arg, str]

def test_no_paramspec_in__parameters__(self):
# ParamSpec should not be found in __parameters__
# of generics. Usages outside Callable, Concatenate
# and Generic are invalid.
T = TypeVar("T")
P = ParamSpec("P")
self.assertNotIn(P, List[P].__parameters__)
self.assertIn(T, Tuple[T, P].__parameters__)

# Test for consistency with builtin generics.
self.assertNotIn(P, list[P].__parameters__)
self.assertIn(T, tuple[T, P].__parameters__)

self.assertNotIn(P, (list[P] | int).__parameters__)
self.assertIn(T, (tuple[T, P] | int).__parameters__)

def test_paramspec_in_nested_generics(self):
# Although ParamSpec should not be found in __parameters__ of most
# generics, they probably should be found when nested in
Expand Down
Loading

0 comments on commit b6a5d85

Please sign in to comment.