diff --git a/doc/source/whatsnew/v1.5.0.rst b/doc/source/whatsnew/v1.5.0.rst index 4920622a15f3f..41ec9594bdb68 100644 --- a/doc/source/whatsnew/v1.5.0.rst +++ b/doc/source/whatsnew/v1.5.0.rst @@ -498,6 +498,7 @@ Conversion - 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`) - Bug in :func:`array` with ``FloatingDtype`` and values containing float-castable strings incorrectly raising (:issue:`45424`) - Bug when comparing string and datetime64ns objects causing ``OverflowError`` exception. (:issue:`45506`) +- 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`) Strings ^^^^^^^ diff --git a/pandas/core/dtypes/generic.py b/pandas/core/dtypes/generic.py index d6dbc83934db0..a6634cca12e93 100644 --- a/pandas/core/dtypes/generic.py +++ b/pandas/core/dtypes/generic.py @@ -37,14 +37,25 @@ # define abstract base classes to enable isinstance type checking on our # objects def create_pandas_abc_type(name, attr, comp): + def _check(inst): + return getattr(inst, attr, "_typ") in comp # https://github.com/python/mypy/issues/1006 # error: 'classmethod' used with a non-method @classmethod # type: ignore[misc] - def _check(cls, inst) -> bool: - return getattr(inst, attr, "_typ") in comp + def _instancecheck(cls, inst) -> bool: + return _check(inst) and not isinstance(inst, type) + + @classmethod # type: ignore[misc] + def _subclasscheck(cls, inst) -> bool: + # Raise instead of returning False + # This is consistent with default __subclasscheck__ behavior + if not isinstance(inst, type): + raise TypeError("issubclass() arg 1 must be a class") + + return _check(inst) - dct = {"__instancecheck__": _check, "__subclasscheck__": _check} + dct = {"__instancecheck__": _instancecheck, "__subclasscheck__": _subclasscheck} meta = type("ABCBase", (type,), dct) return meta(name, (), dct) diff --git a/pandas/tests/apply/test_frame_apply.py b/pandas/tests/apply/test_frame_apply.py index 98872571ae2bb..ef7ab4a469865 100644 --- a/pandas/tests/apply/test_frame_apply.py +++ b/pandas/tests/apply/test_frame_apply.py @@ -1551,3 +1551,29 @@ def foo(x): df = DataFrame({"a": [1, 2, 3]}) with tm.assert_produces_warning(UserWarning, match="Hello, World!"): df.agg([foo]) + + +def test_apply_type(): + # GH 46719 + df = DataFrame( + {"col1": [3, "string", float], "col2": [0.25, datetime(2020, 1, 1), np.nan]}, + index=["a", "b", "c"], + ) + + # applymap + result = df.applymap(type) + expected = DataFrame( + {"col1": [int, str, type], "col2": [float, datetime, float]}, + index=["a", "b", "c"], + ) + tm.assert_frame_equal(result, expected) + + # axis=0 + result = df.apply(type, axis=0) + expected = Series({"col1": Series, "col2": Series}) + tm.assert_series_equal(result, expected) + + # axis=1 + result = df.apply(type, axis=1) + expected = Series({"a": Series, "b": Series, "c": Series}) + tm.assert_series_equal(result, expected) diff --git a/pandas/tests/apply/test_series_apply.py b/pandas/tests/apply/test_series_apply.py index ccf84063d67d0..69f7bebb63986 100644 --- a/pandas/tests/apply/test_series_apply.py +++ b/pandas/tests/apply/test_series_apply.py @@ -889,3 +889,11 @@ def test_apply_retains_column_name(): index=Index(range(3), name="x"), ) tm.assert_frame_equal(result, expected) + + +def test_apply_type(): + # GH 46719 + s = Series([3, "string", float], index=["a", "b", "c"]) + result = s.apply(type) + expected = Series([int, str, type], index=["a", "b", "c"]) + tm.assert_series_equal(result, expected) diff --git a/pandas/tests/dtypes/test_generic.py b/pandas/tests/dtypes/test_generic.py index 0612348297bd0..4f73754d2708f 100644 --- a/pandas/tests/dtypes/test_generic.py +++ b/pandas/tests/dtypes/test_generic.py @@ -1,3 +1,4 @@ +import re from warnings import catch_warnings import numpy as np @@ -49,13 +50,28 @@ class TestABCClasses: @pytest.mark.parametrize("abctype1, inst", abc_pairs) @pytest.mark.parametrize("abctype2, _", abc_pairs) - def test_abc_pairs(self, abctype1, abctype2, inst, _): - # GH 38588 + def test_abc_pairs_instance_check(self, abctype1, abctype2, inst, _): + # GH 38588, 46719 if abctype1 == abctype2: assert isinstance(inst, getattr(gt, abctype2)) + assert not isinstance(type(inst), getattr(gt, abctype2)) else: assert not isinstance(inst, getattr(gt, abctype2)) + @pytest.mark.parametrize("abctype1, inst", abc_pairs) + @pytest.mark.parametrize("abctype2, _", abc_pairs) + def test_abc_pairs_subclass_check(self, abctype1, abctype2, inst, _): + # GH 38588, 46719 + if abctype1 == abctype2: + assert issubclass(type(inst), getattr(gt, abctype2)) + + with pytest.raises( + TypeError, match=re.escape("issubclass() arg 1 must be a class") + ): + issubclass(inst, getattr(gt, abctype2)) + else: + assert not issubclass(type(inst), getattr(gt, abctype2)) + abc_subclasses = { "ABCIndex": [ abctype