Skip to content

Commit a535a69

Browse files
leonarduschenyehoshuadimarsky
authored andcommitted
BUG: Fix __instancecheck__ and __subclasscheck__ of metaclass in pandas.core.dtypes.generic (pandas-dev#46719)
1 parent 3296cf8 commit a535a69

File tree

5 files changed

+67
-5
lines changed

5 files changed

+67
-5
lines changed

doc/source/whatsnew/v1.5.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,7 @@ Conversion
499499
- Bug in :meth:`Series.astype` and :meth:`DataFrame.astype` from floating dtype to unsigned integer dtype failing to raise in the presence of negative values (:issue:`45151`)
500500
- Bug in :func:`array` with ``FloatingDtype`` and values containing float-castable strings incorrectly raising (:issue:`45424`)
501501
- Bug when comparing string and datetime64ns objects causing ``OverflowError`` exception. (:issue:`45506`)
502+
- Bug in metaclass of generic abstract dtypes causing :meth:`DataFrame.apply` and :meth:`Series.apply` to raise for the built-in function ``type`` (:issue:`46684`)
502503

503504
Strings
504505
^^^^^^^

pandas/core/dtypes/generic.py

+14-3
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,25 @@
3737
# define abstract base classes to enable isinstance type checking on our
3838
# objects
3939
def create_pandas_abc_type(name, attr, comp):
40+
def _check(inst):
41+
return getattr(inst, attr, "_typ") in comp
4042

4143
# https://github.com/python/mypy/issues/1006
4244
# error: 'classmethod' used with a non-method
4345
@classmethod # type: ignore[misc]
44-
def _check(cls, inst) -> bool:
45-
return getattr(inst, attr, "_typ") in comp
46+
def _instancecheck(cls, inst) -> bool:
47+
return _check(inst) and not isinstance(inst, type)
48+
49+
@classmethod # type: ignore[misc]
50+
def _subclasscheck(cls, inst) -> bool:
51+
# Raise instead of returning False
52+
# This is consistent with default __subclasscheck__ behavior
53+
if not isinstance(inst, type):
54+
raise TypeError("issubclass() arg 1 must be a class")
55+
56+
return _check(inst)
4657

47-
dct = {"__instancecheck__": _check, "__subclasscheck__": _check}
58+
dct = {"__instancecheck__": _instancecheck, "__subclasscheck__": _subclasscheck}
4859
meta = type("ABCBase", (type,), dct)
4960
return meta(name, (), dct)
5061

pandas/tests/apply/test_frame_apply.py

+26
Original file line numberDiff line numberDiff line change
@@ -1551,3 +1551,29 @@ def foo(x):
15511551
df = DataFrame({"a": [1, 2, 3]})
15521552
with tm.assert_produces_warning(UserWarning, match="Hello, World!"):
15531553
df.agg([foo])
1554+
1555+
1556+
def test_apply_type():
1557+
# GH 46719
1558+
df = DataFrame(
1559+
{"col1": [3, "string", float], "col2": [0.25, datetime(2020, 1, 1), np.nan]},
1560+
index=["a", "b", "c"],
1561+
)
1562+
1563+
# applymap
1564+
result = df.applymap(type)
1565+
expected = DataFrame(
1566+
{"col1": [int, str, type], "col2": [float, datetime, float]},
1567+
index=["a", "b", "c"],
1568+
)
1569+
tm.assert_frame_equal(result, expected)
1570+
1571+
# axis=0
1572+
result = df.apply(type, axis=0)
1573+
expected = Series({"col1": Series, "col2": Series})
1574+
tm.assert_series_equal(result, expected)
1575+
1576+
# axis=1
1577+
result = df.apply(type, axis=1)
1578+
expected = Series({"a": Series, "b": Series, "c": Series})
1579+
tm.assert_series_equal(result, expected)

pandas/tests/apply/test_series_apply.py

+8
Original file line numberDiff line numberDiff line change
@@ -889,3 +889,11 @@ def test_apply_retains_column_name():
889889
index=Index(range(3), name="x"),
890890
)
891891
tm.assert_frame_equal(result, expected)
892+
893+
894+
def test_apply_type():
895+
# GH 46719
896+
s = Series([3, "string", float], index=["a", "b", "c"])
897+
result = s.apply(type)
898+
expected = Series([int, str, type], index=["a", "b", "c"])
899+
tm.assert_series_equal(result, expected)

pandas/tests/dtypes/test_generic.py

+18-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import re
12
from warnings import catch_warnings
23

34
import numpy as np
@@ -49,13 +50,28 @@ class TestABCClasses:
4950

5051
@pytest.mark.parametrize("abctype1, inst", abc_pairs)
5152
@pytest.mark.parametrize("abctype2, _", abc_pairs)
52-
def test_abc_pairs(self, abctype1, abctype2, inst, _):
53-
# GH 38588
53+
def test_abc_pairs_instance_check(self, abctype1, abctype2, inst, _):
54+
# GH 38588, 46719
5455
if abctype1 == abctype2:
5556
assert isinstance(inst, getattr(gt, abctype2))
57+
assert not isinstance(type(inst), getattr(gt, abctype2))
5658
else:
5759
assert not isinstance(inst, getattr(gt, abctype2))
5860

61+
@pytest.mark.parametrize("abctype1, inst", abc_pairs)
62+
@pytest.mark.parametrize("abctype2, _", abc_pairs)
63+
def test_abc_pairs_subclass_check(self, abctype1, abctype2, inst, _):
64+
# GH 38588, 46719
65+
if abctype1 == abctype2:
66+
assert issubclass(type(inst), getattr(gt, abctype2))
67+
68+
with pytest.raises(
69+
TypeError, match=re.escape("issubclass() arg 1 must be a class")
70+
):
71+
issubclass(inst, getattr(gt, abctype2))
72+
else:
73+
assert not issubclass(type(inst), getattr(gt, abctype2))
74+
5975
abc_subclasses = {
6076
"ABCIndex": [
6177
abctype

0 commit comments

Comments
 (0)