8000 bpo-42195: Ensure consistency of Callable's __args__ in collections.abc and typing by Fidget-Spinner · Pull Request #23060 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

bpo-42195: Ensure consistency of Callable's __args__ in collections.abc and typing #23060

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 31 commits into from
Dec 13, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
2c4a297
Allow subclassing of GenericAlias, fix collections.abc.Callable's Gen…
Fidget-Spinner Oct 31, 2020
588d421
fix typing tests, add hash and eq methods
Fidget-Spinner Oct 31, 2020
050fa13
Fix pickling
Fidget-Spinner Oct 31, 2020
f60ea8a
whitespace
Fidget-Spinner Oct 31, 2020
2f3c6dc
Add test specifically for bpo
Fidget-Spinner Oct 31, 2020
2c4508e
update error message
Fidget-Spinner Oct 31, 2020
19d2973
Appease test_site
Fidget-Spinner Oct 31, 2020
3116c8e
Represent Callable __args__ via [tuple[args], result]
Fidget-Spinner Nov 19, 2020
f2b593a
add back tests for weakref, styling nits, add news
Fidget-Spinner Nov 19, 2020
93d51e4
remove redundant tuple checks leftover from old code
Fidget-Spinner Nov 19, 2020
327e1a5
Use _PosArgs instead of tuple
Fidget-Spinner Nov 28, 2020
e971ccb
Fix typo and news
Fidget-Spinner Nov 29, 2020
abd8b98
Refactor C code to use less duplication
Fidget-Spinner Nov 30, 2020
3ddca06
Address most of Guido's reviews (tests failing on purpose)
Fidget-Spinner Dec 1, 2020
1ab59c5
try to revert back to good old flat tuple __args__ days
Fidget-Spinner Dec 2, 2020
ee2d2e1
getting even closer
Fidget-Spinner Dec 2, 2020
2015738
finally done
Fidget-Spinner Dec 2, 2020
6704ffd
Update news
Fidget-Spinner Dec 4, 2020
598d29b
Address review partially
Fidget-Spinner Dec 5, 2020
c43ebcf
Address review fully, update news and tests, remove try-except block
Fidget-Spinner Dec 5, 2020
37ae3a9
Borrowed references don't need decref
Fidget-Spinner Dec 5, 2020
adbfcad
improve _PyArg_NoKwnames error handling, add union and subclass tests
Fidget-Spinner Dec 5, 2020
d1dd627
Don't change getargs, use _PyArg_NoKeywords instead
Fidget-Spinner Dec 5, 2020
2c21045
Merge remote-tracking branch 'upstream/master' into abc-callable-ga
Fidget-Spinner Dec 5, 2020
9f71667
remove stray whitespace
Fidget-Spinner Dec 5, 2020
a789620
refactor C code, add deprecation warning for 3.9
Fidget-Spinner Dec 6, 2020
1890b37
remove redundant check in C code, and try except in __new__
Fidget-Spinner Dec 6, 2020
4e928c6
remove check
Fidget-Spinner Dec 7, 2020
6b11d33
Loosen type checks for Callable args, cast to PyObject in genericalia…
Fidget-Spinner Dec 11, 2020
4215c3b
update news to mention about removing validation in argtypes
Fidget-Spinner Dec 11, 2020
585bf19
remove commented out code
Fidget-Spinner Dec 12, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Use _PosArgs instead of tuple
1.  allow equality between typing._PosArgs and collections.abc._PosArgs
2.  allow equality checks between genericalias subclasses
3.  add support to genericalias subclasses for union type expressions
  • Loading branch information
Fidget-Spinner committed Nov 28, 2020
commit 327e1a5bb75e4320c41ef0d3aabff2c1c407fac4
53 changes: 40 additions & 13 deletions Lib/_collections_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@ def _f(): pass
"MappingView", "KeysView", "ItemsView", "ValuesView",
"Sequence", "MutableSequence",
"ByteString",
# To allow for pickling, not actually meant to be imported.
# The following classse are to allow pickling, not actually
# meant to be imported.
"_CallableGenericAlias",
"_PosArgs",
"_PosArgsGenericAlias"
]

# This module has been renamed from collections.abc to _collections_abc to
Expand Down Expand Up @@ -415,6 +418,35 @@ def __subclasshook__(cls, C):
return NotImplemented


class _PosArgsGenericAlias(GenericAlias):
""" Internal class specifically to represent positional arguments in
``_CallableGenericAlias``.
"""
def __repr__(self):
return f"{__name__}._PosArgsGenericAlias" \
f"[{', '.join(_type_repr(t) for t in self.__args__)}]"

def __eq__(self, other):
o_cls = other.__class__
if not (o_cls.__module__ == "typing" and o_cls.__name__
== "_GenericAlias" or isinstance(other, GenericAlias)):
return NotImplemented
return (self.__origin__ == other.__origin__
and self.__args__ == other.__args__)

def __hash__(self):
return hash((self.__origin__, self.__args__))

# Only used for _CallableGenericAlias
class _PosArgs:
""" Internal class specifically to represent positional arguments in
``_CallableGenericAlias``.
"""
def __class_getitem__(cls, item):
return _PosArgsGenericAlias(tuple, item)

# _PosArgs = type("_PosArgs", (tuple, ), {})

class _CallableGenericAlias(GenericAlias):
""" Internal class specifically for consistency between the ``__args__`` of
``collections.abc.Callable``'s and ``typing.Callable``'s ``GenericAlias``.
Expand All @@ -432,21 +464,10 @@ def __new__(cls, *args, **kwargs):
f"or Ellipsis. Got {_type_repr(t_args)}")

ga_args = (_args if t_args is Ellipsis
else (tuple[tuple(t_args)], t_result))
else (_PosArgs[tuple(t_args)], t_result))

return super().__new__(cls, origin, ga_args)

def __init__(self, *args, **kwargs):
pass

def __eq__(self, other):
if not isinstance(other, GenericAlias):
return NotImplemented
return (self.__origin__ == other.__origin__
and self.__args__ == other.__args__)

def __hash__(self):
return super().__hash__()

def __repr__(self):
t_args = self.__args__[0]
Expand All @@ -459,6 +480,12 @@ def __repr__(self):

return f"{origin}[{t_args_repr}, {_type_repr(self.__args__[-1])}]"

def __reduce__(self):
args = self.__args__
if not (len(args) == 2 and args[0] is ...):
args = list(t for t in args[0].__args__), args[-1]
return _CallableGenericAlias, (Callable, args)


def _type_repr(obj):
"""Return the repr() of an object, special-casing types (internal helper).
Expand Down
24 changes: 16 additions & 8 deletions Lib/test/test_genericalias.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,30 +303,38 @@ def test_weakref(self):

def test_abc_callable(self):
alias = Callable[[int, str], float]
with self.subTest("Testing collections.abc.Callable's subscription"):
with self.subTest("Testing subscription"):
self.assertIs(alias.__origin__, Callable)
self.assertEqual(alias.__args__, (tuple[int, str], float))
self.assertEqual(alias.__args__, (_PosArgs[int, str], float))
self.assertEqual(alias.__parameters__, ())

with self.subTest("Testing collections.abc.Callable's instance checks"):
with self.subTest("Testing nstance checks"):
self.assertIsInstance(alias, GenericAlias)

invalid_params = ('Callable[int]', 'Callable[int, str]')
with self.subTest("Testing collections.abc.Callable's parameter "
"validation"):
with self.subTest("Testing parameter validation"):
for bad in invalid_params:
with self.subTest(f'Testing expression {bad}'):
with self.assertRaises(TypeError):
eval(bad)

with self.subTest("Testing collections.abc.Callable's weakref"):
with self.subTest("Testing weakref"):
self.assertEqual(ref(alias)(), alias)

with self.subTest("Testing picling"):
s = pickle.dumps(alias)
loaded = pickle.loads(s)
self.assertEqual(alias.__origin__, loaded.__origin__)
self.assertEqual(alias.__args__, loaded.__args__)
self.assertEqual(alias.__parameters__, loaded.__parameters__)

# bpo-42195
with self.subTest("Testing collections.abc.Callable's consistency "
"with typing.Callable"):
self.assertEqual(typing.Callable[[int, str], dict].__args__,
Callable[[int, str], dict].__args__)
c1 = typing.Callable[[int, str], dict]
c2 = Callable[[int, str], dict]
self.assertEqual(c1.__args__, c2.__args__)
self.assertEqual(hash(c1.__args__), hash(c2.__args__))

if __name__ == "__main__":
unittest.main()
2 changes: 1 addition & 1 deletion Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -3058,7 +3058,7 @@ class C(Generic[T]): pass
(int, Tuple[str, int]))
self.assertEqual(get_args(typing.Dict[int, Tuple[T, T]][Optional[int]]),
(int, Tuple[Optional[int], Optional[int]]))
self.assertEqual(get_args(Callable[[], T][int]), (tuple[()], int))
self.assertEqual(get_args(Callable[[], T][int]), (typing._PosArgs[()], int))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I think get_args() should expand the PosArgs away.

self.assertEqual(get_args(Callable[..., int]), (..., int))
self.assertEqual(get_args(Union[int, Callable[[Tuple[T, ...]], str]]),
(int, Callable[[Tuple[T, ...]], str]))
Expand Down
5 changes: 4 additions & 1 deletion Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
'Text',
'TYPE_CHECKING',
'TypeAlias',
'_PosArgs', # Not meant to be imported, just for pickling.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect that here it really is not needed in __all__.

]

# The pseudo-submodules 're' and 'io' are part of the public
Expand Down Expand Up @@ -921,7 +922,7 @@ def __getitem_inner__(self, params):
return self.copy_with((_TypingEllipsis, result))
msg = "Callable[[arg, ...], result]: each arg must be a type."
args = tuple(_type_check(arg, msg) for arg in args)
params = (tuple[args], result)
params = (_PosArgs[args], result)
return self.copy_with(params)


Expand Down Expand Up @@ -1711,6 +1712,8 @@ class Other(Leaf): # Error reported by type checker
Sized = _alias(collections.abc.Sized, 0) # Not generic.
Container = _alias(collections.abc.Container, 1)
Collection = _alias(collections.abc.Collection, 1)
# Only used for Callable
_PosArgs = _TupleType(tuple, -1, inst=False, name='_PosArgs')
Callable = _CallableType(collections.abc.Callable, 2)
Callable.__doc__ = \
"""Callable type; Callable[[int], str] is a function of (int) -> str.
Expand Down
4 changes: 2 additions & 2 deletions Objects/genericaliasobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -429,8 +429,8 @@ ga_getattro(PyObject *self, PyObject *name)
static PyObject *
ga_richcompare(PyObject *a, PyObject *b, int op)
{
if (!Py_IS_TYPE(a, &Py_GenericAliasType) ||
!Py_IS_TYPE(b, &Py_GenericAliasType) ||
if (!PyObject_TypeCheck(a, &Py_GenericAliasType) ||
!PyObject_TypeCheck(b, &Py_GenericAliasType) ||
(op != Py_EQ && op != Py_NE))
{
Py_RETURN_NOTIMPLEMENTED;
Expand Down
2 changes: 1 addition & 1 deletion Objects/unionobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ is_unionable(PyObject *obj)
is_new_type(obj) ||
is_special_form(obj) ||
PyType_Check(obj) ||
type == &Py_GenericAliasType ||
PyObject_TypeCheck(obj, &Py_GenericAliasType) ||
type == &_Py_UnionType);
}

Expand Down
0