Skip to content

Commit 33b3fed

Browse files
[3.9] bpo-42195: Ensure consistency of Callable's __args__ in collections.abc and typing (GH-23765)
Backport of GH-23060.
1 parent 14f2a12 commit 33b3fed

File tree

7 files changed

+202
-43
lines changed

7 files changed

+202
-43
lines changed

Lib/_collections_abc.py

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
import sys
1111

1212
GenericAlias = type(list[int])
13+
EllipsisType = type(...)
14+
def _f(): pass
15+
FunctionType = type(_f)
16+
del _f
1317

1418
__all__ = ["Awaitable", "Coroutine",
1519
"AsyncIterable", "AsyncIterator", "AsyncGenerator",
@@ -409,6 +413,76 @@ def __subclasshook__(cls, C):
409413
return NotImplemented
410414

411415

416+
class _CallableGenericAlias(GenericAlias):
417+
""" Represent `Callable[argtypes, resulttype]`.
418+
419+
This sets ``__args__`` to a tuple containing the flattened``argtypes``
420+
followed by ``resulttype``.
421+
422+
Example: ``Callable[[int, str], float]`` sets ``__args__`` to
423+
``(int, str, float)``.
424+
"""
425+
426+
__slots__ = ()
427+
428+
def __new__(cls, origin, args):
429+
try:
430+
return cls.__create_ga(origin, args)
431+
except TypeError as exc:
432+
import warnings
433+
warnings.warn(f'{str(exc)} '
434+
f'(This will raise a TypeError in Python 3.10.)',
435+
DeprecationWarning)
436+
return GenericAlias(origin, args)
437+
438+
@classmethod
439+
def __create_ga(cls, origin, args):
440+
if not isinstance(args, tuple) or len(args) != 2:
441+
raise TypeError(
442+
"Callable must be used as Callable[[arg, ...], result].")
443+
t_args, t_result = args
444+
if isinstance(t_args, list):
445+
ga_args = tuple(t_args) + (t_result,)
446+
# This relaxes what t_args can be on purpose to allow things like
447+
# PEP 612 ParamSpec. Responsibility for whether a user is using
448+
# Callable[...] properly is deferred to static type checkers.
449+
else:
450+
ga_args = args
451+
return super().__new__(cls, origin, ga_args)
452+
453+
def __repr__(self):
454+
if len(self.__args__) == 2 and self.__args__[0] is Ellipsis:
455+
return super().__repr__()
456+
return (f'collections.abc.Callable'
457+
f'[[{", ".join([_type_repr(a) for a in self.__args__[:-1]])}], '
458+
f'{_type_repr(self.__args__[-1])}]')
459+
460+
def __reduce__(self):
461+
args = self.__args__
462+
if not (len(args) == 2 and args[0] is Ellipsis):
463+
args = list(args[:-1]), args[-1]
464+
return _CallableGenericAlias, (Callable, args)
465+
466+
467+
def _type_repr(obj):
468+
"""Return the repr() of an object, special-casing types (internal helper).
469+
470+
Copied from :mod:`typing` since collections.abc
471+
shouldn't depend on that module.
472+
"""
473+
if isinstance(obj, GenericAlias):
474+
return repr(obj)
475+
if isinstance(obj, type):
476+
if obj.__module__ == 'builtins':
477+
return obj.__qualname__
478+
return f'{obj.__module__}.{obj.__qualname__}'
479+
if obj is Ellipsis:
480+
return '...'
481+
if isinstance(obj, FunctionType):
482+
return obj.__name__
483+
return repr(obj)
484+
485+
412486
class Callable(metaclass=ABCMeta):
413487

414488
__slots__ = ()
@@ -423,7 +497,7 @@ def __subclasshook__(cls, C):
423497
return _check_methods(C, "__call__")
424498
return NotImplemented
425499

426-
__class_getitem__ = classmethod(GenericAlias)
500+
__class_getitem__ = classmethod(_CallableGenericAlias)
427501

428502

429503
### SETS ###

Lib/collections/abc.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
from _collections_abc import *
22
from _collections_abc import __all__
3+
from _collections_abc import _CallableGenericAlias

Lib/test/test_genericalias.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ class BaseTest(unittest.TestCase):
6262
Iterable, Iterator,
6363
Reversible,
6464
Container, Collection,
65-
Callable,
6665
Mailbox, _PartialFile,
6766
ContextVar, Token,
6867
Field,
@@ -307,6 +306,63 @@ def test_no_kwargs(self):
307306
with self.assertRaises(TypeError):
308307
GenericAlias(bad=float)
309308

309+
def test_subclassing_types_genericalias(self):
310+
class SubClass(GenericAlias): ...
311+
alias = SubClass(list, int)
312+
class Bad(GenericAlias):
313+
def __new__(cls, *args, **kwargs):
314+
super().__new__(cls, *args, **kwargs)
315+
316+
self.assertEqual(alias, list[int])
317+
with self.assertRaises(TypeError):
318+
Bad(list, int, bad=int)
319+
320+
def test_abc_callable(self):
321+
# A separate test is needed for Callable since it uses a subclass of
322+
# GenericAlias.
323+
alias = Callable[[int, str], float]
324+
with self.subTest("Testing subscription"):
325+
self.assertIs(alias.__origin__, Callable)
326+
self.assertEqual(alias.__args__, (int, str, float))
327+
self.assertEqual(alias.__parameters__, ())
328+
329+
with self.subTest("Testing instance checks"):
330+
self.assertIsInstance(alias, GenericAlias)
331+
332+
with self.subTest("Testing weakref"):
333+
self.assertEqual(ref(alias)(), alias)
334+
335+
with self.subTest("Testing pickling"):
336+
s = pickle.dumps(alias)
337+
loaded = pickle.loads(s)
338+
self.assertEqual(alias.__origin__, loaded.__origin__)
339+
self.assertEqual(alias.__args__, loaded.__args__)
340+
self.assertEqual(alias.__parameters__, loaded.__parameters__)
341+
342+
with self.subTest("Testing TypeVar substitution"):
343+
C1 = Callable[[int, T], T]
344+
C2 = Callable[[K, T], V]
345+
C3 = Callable[..., T]
346+
self.assertEqual(C1[str], Callable[[int, str], str])
347+
self.assertEqual(C2[int, float, str], Callable[[int, float], str])
348+
self.assertEqual(C3[int], Callable[..., int])
349+
350+
with self.subTest("Testing type erasure"):
351+
class C1(Callable):
352+
def __call__(self):
353+
return None
354+
a = C1[[int], T]
355+
self.assertIs(a().__class__, C1)
356+
self.assertEqual(a().__orig_class__, C1[[int], T])
357+
358+
# bpo-42195
359+
with self.subTest("Testing collections.abc.Callable's consistency "
360+
"with typing.Callable"):
361+
c1 = typing.Callable[[int, str], dict]
362+
c2 = Callable[[int, str], dict]
363+
self.assertEqual(c1.__args__, c2.__args__)
364+
self.assertEqual(hash(c1.__args__), hash(c2.__args__))
365+
310366

311367
if __name__ == "__main__":
312368
unittest.main()

Lib/test/test_typing.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -450,14 +450,6 @@ def test_cannot_instantiate(self):
450450
type(c)()
451451

452452
def test_callable_wrong_forms(self):
453-
with self.assertRaises(TypeError):
454-
Callable[[...], int]
455-
with self.assertRaises(TypeError):
456-
Callable[(), int]
457-
with self.assertRaises(TypeError):
458-
Callable[[()], int]
459-
with self.assertRaises(TypeError):
460-
Callable[[int, 1], 2]
461453
with self.assertRaises(TypeError):
462454
Callable[int]
463455

Lib/typing.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,15 @@
118118
# legitimate imports of those modules.
119119

120120

121+
def _type_convert(arg):
122+
"""For converting None to type(None), and strings to ForwardRef."""
123+
if arg is None:
124+
return type(None)
125+
if isinstance(arg, str):
126+
return ForwardRef(arg)
127+
return arg
128+
129+
121130
def _type_check(arg, msg, is_argument=True):
122131
"""Check that the argument is a type, and return it (internal helper).
123132
@@ -134,10 +143,7 @@ def _type_check(arg, msg, is_argument=True):
134143
if is_argument:
135144
invalid_generic_forms = invalid_generic_forms + (ClassVar, Final)
136145

137-
if arg is None:
138-
return type(None)
139-
if isinstance(arg, str):
140-
return ForwardRef(arg)
146+
arg = _type_convert(arg)
141147
if (isinstance(arg, _GenericAlias) and
142148
arg.__origin__ in invalid_generic_forms):
143149
raise TypeError(f"{arg} is not valid as type argument")
@@ -859,13 +865,13 @@ def __getitem__(self, params):
859865
raise TypeError("Callable must be used as "
860866
"Callable[[arg, ...], result].")
861867
args, result = params
862-
if args is Ellipsis:
863-
params = (Ellipsis, result)
864-
else:
865-
if not isinstance(args, list):
866-
raise TypeError(f"Callable[args, result]: args must be a list."
867-
f" Got {args}")
868+
# This relaxes what args can be on purpose to allow things like
869+
# PEP 612 ParamSpec. Responsibility for whether a user is using
870+
# Callable[...] properly is deferred to static type checkers.
871+
if isinstance(args, list):
868872
params = (tuple(args), result)
873+
else:
874+
params = (args, result)
869875
return self.__getitem_inner__(params)
870876

871877
@_tp_cache
@@ -875,8 +881,9 @@ def __getitem_inner__(self, params):
875881
result = _type_check(result, msg)
876882
if args is Ellipsis:
877883
return self.copy_with((_TypingEllipsis, result))
878-
msg = "Callable[[arg, ...], result]: each arg must be a type."
879-
args = tuple(_type_check(arg, msg) for arg in args)
884+
if not isinstance(args, tuple):
885+
args = (args,)
886+
args = tuple(_type_convert(arg) for arg in args)
880887
params = args + (result,)
881888
return self.copy_with(params)
882889

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
The ``__args__`` of the parameterized generics for :data:`typing.Callable`
2+
and :class:`collections.abc.Callable` are now consistent. The ``__args__``
3+
for :class:`collections.abc.Callable` are now flattened while
4+
:data:`typing.Callable`'s have not changed. To allow this change,
5+
:class:`types.GenericAlias` can now be subclassed and
6+
``collections.abc.Callable``'s ``__class_getitem__`` will now return a subclass
7+
of ``types.GenericAlias``. Tests for typing were also updated to not subclass
8+
things like ``Callable[..., T]`` as that is not a valid base class. Finally,
9+
both types no longer validate their ``argtypes``, in
10+
``Callable[[argtypes], resulttype]`` to prepare for :pep:`612`. Patch by Ken Jin.
11+

Objects/genericaliasobject.c

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -428,8 +428,8 @@ ga_getattro(PyObject *self, PyObject *name)
428428
static PyObject *
429429
ga_richcompare(PyObject *a, PyObject *b, int op)
430430
{
431-
if (!Py_IS_TYPE(a, &Py_GenericAliasType) ||
432-
!Py_IS_TYPE(b, &Py_GenericAliasType) ||
431+
if (!PyObject_TypeCheck(a, &Py_GenericAliasType) ||
432+
!PyObject_TypeCheck(b, &Py_GenericAliasType) ||
433433
(op != Py_EQ && op != Py_NE))
434434
{
435435
Py_RETURN_NOTIMPLEMENTED;
@@ -563,6 +563,29 @@ static PyGetSetDef ga_properties[] = {
563563
{0}
564564
};
565565

566+
/* A helper function to create GenericAlias' args tuple and set its attributes.
567+
* Returns 1 on success, 0 on failure.
568+
*/
569+
static inline int
570+
setup_ga(gaobject *alias, PyObject *origin, PyObject *args) {
571+
if (!PyTuple_Check(args)) {
572+
args = PyTuple_Pack(1, args);
573+
if (args == NULL) {
574+
return 0;
575+
}
576+
}
577+
else {
578+
Py_INCREF(args);
579+
}
580+
581+
Py_INCREF(origin);
582+
alias->origin = origin;
583+
alias->args = args;
584+
alias->parameters = NULL;
585+
alias->weakreflist = NULL;
586+
return 1;
587+
}
588+
566589
static PyObject *
567590
ga_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
568591
{
@@ -574,7 +597,15 @@ ga_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
574597
}
575598
PyObject *origin = PyTuple_GET_ITEM(args, 0);
576599
PyObject *arguments = PyTuple_GET_ITEM(args, 1);
577-
return Py_GenericAlias(origin, arguments);
600+
gaobject *self = (gaobject *)type->tp_alloc(type, 0);
601+
if (self == NULL) {
602+
return NULL;
603+
}
604+
if (!setup_ga(self, origin, arguments)) {
605+
type->tp_free((PyObject *)self);
606+
return NULL;
607+
}
608+
return (PyObject *)self;
578609
}
579610

580611
// TODO:
@@ -594,7 +625,7 @@ PyTypeObject Py_GenericAliasType = {
594625
.tp_hash = ga_hash,
595626
.tp_call = ga_call,
596627
.tp_getattro = ga_getattro,
597-
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC,
628+
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC | Py_TPFLAGS_BASETYPE,
598629
.tp_traverse = ga_traverse,
599630
.tp_richcompare = ga_richcompare,
600631
.tp_weaklistoffset = offsetof(gaobject, weakreflist),
@@ -609,27 +640,14 @@ PyTypeObject Py_GenericAliasType = {
609640
PyObject *
610641
Py_GenericAlias(PyObject *origin, PyObject *args)
611642
{
612-
if (!PyTuple_Check(args)) {
613-
args = PyTuple_Pack(1, args);
614-
if (args == NULL) {
615-
return NULL;
616-
}
617-
}
618-
else {
619-
Py_INCREF(args);
620-
}
621-
622643
gaobject *alias = PyObject_GC_New(gaobject, &Py_GenericAliasType);
623644
if (alias == NULL) {
624-
Py_DECREF(args);
625645
return NULL;
626646
}
627-
628-
Py_INCREF(origin);
629-
alias->origin = origin;
630-
alias->args = args;
631-
alias->parameters = NULL;
632-
alias->weakreflist = NULL;
647+
if (!setup_ga(alias, origin, args)) {
648+
PyObject_GC_Del((PyObject *)alias);
649+
return NULL;
650+
}
633651
_PyObject_GC_TRACK(alias);
634652
return (PyObject *)alias;
635653
}

0 commit comments

Comments
 (0)