Skip to content

BUG: inspect.getmembers(Series) #38782

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 9 commits into from
Feb 7, 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
18 changes: 4 additions & 14 deletions doc/source/development/extending.rst
Original file line number Diff line number Diff line change
Expand Up @@ -329,21 +329,11 @@ Each data structure has several *constructor properties* for returning a new
data structure as the result of an operation. By overriding these properties,
you can retain subclasses through ``pandas`` data manipulations.

There are 3 constructor properties to be defined:
There are 3 possible constructor properties to be defined on a subclass:

* ``_constructor``: Used when a manipulation result has the same dimensions as the original.
* ``_constructor_sliced``: Used when a manipulation result has one lower dimension(s) as the original, such as ``DataFrame`` single columns slicing.
* ``_constructor_expanddim``: Used when a manipulation result has one higher dimension as the original, such as ``Series.to_frame()``.

Following table shows how ``pandas`` data structures define constructor properties by default.

=========================== ======================= =============
Property Attributes ``Series`` ``DataFrame``
=========================== ======================= =============
``_constructor`` ``Series`` ``DataFrame``
``_constructor_sliced`` ``NotImplementedError`` ``Series``
``_constructor_expanddim`` ``DataFrame`` ``NotImplementedError``
=========================== ======================= =============
* ``DataFrame/Series._constructor``: Used when a manipulation result has the same dimension as the original.
* ``DataFrame._constructor_sliced``: Used when a ``DataFrame`` (sub-)class manipulation result should be a ``Series`` (sub-)class.
* ``Series._constructor_expanddim``: Used when a ``Series`` (sub-)class manipulation result should be a ``DataFrame`` (sub-)class, e.g. ``Series.to_frame()``.

Below example shows how to define ``SubclassedSeries`` and ``SubclassedDataFrame`` overriding constructor properties.

Expand Down
3 changes: 2 additions & 1 deletion doc/source/whatsnew/v1.3.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ See :ref:`install.dependencies` and :ref:`install.optional_dependencies` for mor
Other API changes
^^^^^^^^^^^^^^^^^
- Partially initialized :class:`CategoricalDtype` (i.e. those with ``categories=None`` objects will no longer compare as equal to fully initialized dtype objects.
-
- Accessing ``_constructor_expanddim`` on a :class:`DataFrame` and ``_constructor_sliced`` on a :class:`Series` now raise an ``AttributeError``. Previously a ``NotImplementedError`` was raised (:issue:`38782`)
-

.. ---------------------------------------------------------------------------
Expand Down Expand Up @@ -445,6 +445,7 @@ Other
- Bug in :class:`Index` constructor sometimes silently ignorning a specified ``dtype`` (:issue:`38879`)
- Bug in constructing a :class:`Series` from a list and a :class:`PandasDtype` (:issue:`39357`)
- Bug in :class:`Styler` which caused CSS to duplicate on multiple renders. (:issue:`39395`)
- ``inspect.getmembers(Series)`` no longer raises an ``AbstractMethodError`` (:issue:`38782`)
- :meth:`Index.where` behavior now mirrors :meth:`Index.putmask` behavior, i.e. ``index.where(mask, other)`` matches ``index.putmask(~mask, other)`` (:issue:`39412`)
- Bug in :func:`pandas.testing.assert_series_equal`, :func:`pandas.testing.assert_frame_equal`, :func:`pandas.testing.assert_index_equal` and :func:`pandas.testing.assert_extension_array_equal` incorrectly raising when an attribute has an unrecognized NA type (:issue:`39461`)
- Bug in :class:`Styler` where ``subset`` arg in methods raised an error for some valid multiindex slices (:issue:`33562`)
Expand Down
13 changes: 2 additions & 11 deletions pandas/core/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -490,23 +490,14 @@ class DataFrame(NDFrame, OpsMixin):
_internal_names_set = {"columns", "index"} | NDFrame._internal_names_set
_typ = "dataframe"
_HANDLED_TYPES = (Series, Index, ExtensionArray, np.ndarray)
_accessors: Set[str] = {"sparse"}
_hidden_attrs: FrozenSet[str] = NDFrame._hidden_attrs | frozenset([])

@property
def _constructor(self) -> Type[DataFrame]:
return DataFrame

_constructor_sliced: Type[Series] = Series
_hidden_attrs: FrozenSet[str] = NDFrame._hidden_attrs | frozenset([])
_accessors: Set[str] = {"sparse"}

@property
def _constructor_expanddim(self):
# GH#31549 raising NotImplementedError on a property causes trouble
# for `inspect`
def constructor(*args, **kwargs):
raise NotImplementedError("Not supported for DataFrames!")

return constructor

# ----------------------------------------------------------------------
# Constructors
Expand Down
16 changes: 0 additions & 16 deletions pandas/core/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,22 +375,6 @@ def _constructor(self: FrameOrSeries) -> Type[FrameOrSeries]:
"""
raise AbstractMethodError(self)

@property
def _constructor_sliced(self):
"""
Used when a manipulation result has one lower dimension(s) as the
original, such as DataFrame single columns slicing.
"""
raise AbstractMethodError(self)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This isn't used on Series. Calling it on a Series should just result in an AttributeError.

@property
def _constructor_expanddim(self):
"""
Used when a manipulation result has one higher dimension as the
original, such as Series.to_frame()
"""
raise NotImplementedError

# ----------------------------------------------------------------------
# Internals

Expand Down
4 changes: 4 additions & 0 deletions pandas/core/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,10 @@ def _constructor(self) -> Type[Series]:

@property
def _constructor_expanddim(self) -> Type[DataFrame]:
"""
Used when a manipulation result has one higher dimension as the
original, such as Series.to_frame()
"""
from pandas.core.frame import DataFrame

return DataFrame
Expand Down
10 changes: 6 additions & 4 deletions pandas/tests/frame/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,12 +321,14 @@ def test_set_flags(self, allows_duplicate_labels, frame_or_series):
result.iloc[key] = 10
assert obj.iloc[key] == 0

def test_constructor_expanddim_lookup(self):
# GH#33628 accessing _constructor_expanddim should not
# raise NotImplementedError
def test_constructor_expanddim(self):
# GH#33628 accessing _constructor_expanddim should not raise NotImplementedError
# GH38782 pandas has no container higher than DataFrame (two-dim), so
# DataFrame._constructor_expand_dim, doesn't make sense, so is removed.
df = DataFrame()

with pytest.raises(NotImplementedError, match="Not supported for DataFrames!"):
msg = "'DataFrame' object has no attribute '_constructor_expanddim'"
with pytest.raises(AttributeError, match=msg):
df._constructor_expanddim(np.arange(27).reshape(3, 3, 3))

@skip_if_no("jinja2")
Expand Down
10 changes: 10 additions & 0 deletions pandas/tests/series/test_api.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import inspect
import pydoc

import numpy as np
import pytest

from pandas.util._test_decorators import skip_if_no

import pandas as pd
from pandas import DataFrame, Index, Series, date_range
import pandas._testing as tm
Expand Down Expand Up @@ -167,3 +170,10 @@ def test_attrs(self):
s.attrs["version"] = 1
result = s + 1
assert result.attrs == {"version": 1}

@skip_if_no("jinja2")
def test_inspect_getmembers(self):
# GH38782
ser = Series()
with tm.assert_produces_warning(None):
inspect.getmembers(ser)