Skip to content
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

bpo-44606: Fix __instancecheck__ and __subclasscheck__ for the union type. #27120

Merged
merged 2 commits into from
Jul 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
37 changes: 35 additions & 2 deletions Lib/test/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,39 @@ def test_or_types_operator(self):
x.__args__ = [str, int]
(int | str ) == x

def test_instancecheck(self):
x = int | str
self.assertIsInstance(1, x)
self.assertIsInstance(True, x)
self.assertIsInstance('a', x)
self.assertNotIsInstance(None, x)
self.assertTrue(issubclass(int, x))
self.assertTrue(issubclass(bool, x))
self.assertTrue(issubclass(str, x))
self.assertFalse(issubclass(type(None), x))
x = int | None
self.assertIsInstance(None, x)
self.assertTrue(issubclass(type(None), x))
x = int | collections.abc.Mapping
self.assertIsInstance({}, x)
self.assertTrue(issubclass(dict, x))

def test_bad_instancecheck(self):
class BadMeta(type):
def __instancecheck__(cls, inst):
1/0
x = int | BadMeta('A', (), {})
self.assertTrue(isinstance(1, x))
self.assertRaises(ZeroDivisionError, isinstance, [], x)

def test_bad_subclasscheck(self):
class BadMeta(type):
def __subclasscheck__(cls, sub):
1/0
x = int | BadMeta('A', (), {})
self.assertTrue(issubclass(int, x))
self.assertRaises(ZeroDivisionError, issubclass, list, x)

Copy link
Member

@Fidget-Spinner Fidget-Spinner Jul 13, 2021

Choose a reason for hiding this comment

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

Maybe you can add your test from the bpo for virtual subclasses issubclass(dict, int | collections.abc.Mapping)?

Then again we may not need that test. If the previous tests throw the ZeroDivisionError, then that means it's already looking into the __subclasscheck__ dunder.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh, I forget about this. Thank you for reminder.

def test_or_type_operator_with_TypeVar(self):
TV = typing.TypeVar('T')
assert TV | str == typing.Union[TV, str]
Expand Down Expand Up @@ -754,9 +787,9 @@ def __eq__(self, other):
for type_ in union_ga:
with self.subTest(f"check isinstance/issubclass is invalid for {type_}"):
with self.assertRaises(TypeError):
isinstance(list, type_)
isinstance(1, type_)
with self.assertRaises(TypeError):
issubclass(list, type_)
issubclass(int, type_)

def test_or_type_operator_with_bad_module(self):
class TypeVar:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix ``__instancecheck__`` and ``__subclasscheck__`` for the union type.
23 changes: 19 additions & 4 deletions Objects/unionobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,14 @@ union_instancecheck(PyObject *self, PyObject *instance)
if (arg == Py_None) {
arg = (PyObject *)&_PyNone_Type;
}
if (PyType_Check(arg) && PyObject_IsInstance(instance, arg) != 0) {
Py_RETURN_TRUE;
if (PyType_Check(arg)) {
int res = PyObject_IsInstance(instance, arg);
if (res < 0) {
return NULL;
}
if (res) {
Py_RETURN_TRUE;
}
}
}
Py_RETURN_FALSE;
Expand All @@ -93,8 +99,17 @@ union_subclasscheck(PyObject *self, PyObject *instance)
Py_ssize_t nargs = PyTuple_GET_SIZE(alias->args);
for (Py_ssize_t iarg = 0; iarg < nargs; iarg++) {
PyObject *arg = PyTuple_GET_ITEM(alias->args, iarg);
if (PyType_Check(arg) && (PyType_IsSubtype((PyTypeObject *)instance, (PyTypeObject *)arg) != 0)) {
Py_RETURN_TRUE;
if (arg == Py_None) {
Copy link
Member

Choose a reason for hiding this comment

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

Reminder for myself that this can probably be removed when we do the auto conversion of None to type(None).

arg = (PyObject *)&_PyNone_Type;
}
if (PyType_Check(arg)) {
int res = PyObject_IsSubclass(instance, arg);
if (res < 0) {
return NULL;
}
if (res) {
Py_RETURN_TRUE;
}
}
}
Py_RETURN_FALSE;
Expand Down