diff --git a/doc/source/reference/frame.rst b/doc/source/reference/frame.rst index 4982edeb7f85b..4b5faed0f4d2d 100644 --- a/doc/source/reference/frame.rst +++ b/doc/source/reference/frame.rst @@ -274,6 +274,19 @@ Time series-related DataFrame.tz_convert DataFrame.tz_localize +.. _api.frame.metadata: + +Metadata +~~~~~~~~ + +:attr:`DataFrame.attrs` is a dictionary for storing global metadata for this DataFrame. + +.. autosummary:: + :toctree: api/ + + DataFrame.attrs + + .. _api.dataframe.plotting: Plotting diff --git a/doc/source/reference/series.rst b/doc/source/reference/series.rst index 5d825c8092efc..59910ba357130 100644 --- a/doc/source/reference/series.rst +++ b/doc/source/reference/series.rst @@ -531,6 +531,19 @@ Sparse-dtype specific methods and attributes are provided under the Series.sparse.to_coo +.. _api.series.metadata: + +Metadata +~~~~~~~~ + +:attr:`Series.attrs` is a dictionary for storing global metadata for this Series. + +.. autosummary:: + :toctree: api/ + + Series.attrs + + Plotting -------- ``Series.plot`` is both a callable method and a namespace attribute for diff --git a/pandas/core/generic.py b/pandas/core/generic.py index e97772a418982..55e2cceac91c3 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -8,12 +8,14 @@ import re from textwrap import dedent from typing import ( + TYPE_CHECKING, Any, Callable, Dict, FrozenSet, Hashable, List, + Mapping, Optional, Sequence, Set, @@ -188,6 +190,12 @@ class NDFrame(PandasObject, SelectionMixin): _is_copy = None _data = None # type: BlockManager + if TYPE_CHECKING: + # TODO(PY36): replace with _attrs : Dict[Hashable, Any] + # We need the TYPE_CHECKING, because _attrs is not a class attribute + # and Py35 doesn't support the new syntax. + _attrs = {} # type: Dict[Hashable, Any] + # ---------------------------------------------------------------------- # Constructors @@ -197,6 +205,7 @@ def __init__( axes: Optional[List[Index]] = None, copy: bool = False, dtype: Optional[Dtype] = None, + attrs: Optional[Mapping[Hashable, Any]] = None, fastpath: bool = False, ): @@ -213,6 +222,11 @@ def __init__( object.__setattr__(self, "_is_copy", None) object.__setattr__(self, "_data", data) object.__setattr__(self, "_item_cache", {}) + if attrs is None: + attrs = {} + else: + attrs = dict(attrs) + object.__setattr__(self, "_attrs", attrs) def _init_mgr(self, mgr, axes=None, dtype=None, copy=False): """ passed a manager and a axes dict """ @@ -233,6 +247,19 @@ def _init_mgr(self, mgr, axes=None, dtype=None, copy=False): # ---------------------------------------------------------------------- + @property + def attrs(self) -> Dict[Hashable, Any]: + """ + Dictionary of global attributes on this object. + """ + if self._attrs is None: + self._attrs = {} + return self._attrs + + @attrs.setter + def attrs(self, value: Mapping[Hashable, Any]) -> None: + self._attrs = dict(value) + @property def is_copy(self): """ @@ -2029,7 +2056,13 @@ def to_dense(self): def __getstate__(self): meta = {k: getattr(self, k, None) for k in self._metadata} - return dict(_data=self._data, _typ=self._typ, _metadata=self._metadata, **meta) + return dict( + _data=self._data, + _typ=self._typ, + _metadata=self._metadata, + attrs=self.attrs, + **meta + ) def __setstate__(self, state): @@ -2038,6 +2071,8 @@ def __setstate__(self, state): elif isinstance(state, dict): typ = state.get("_typ") if typ is not None: + attrs = state.get("_attrs", {}) + object.__setattr__(self, "_attrs", attrs) # set in the order of internal names # to avoid definitional recursion @@ -5213,6 +5248,9 @@ def __finalize__(self, other, method=None, **kwargs): """ if isinstance(other, NDFrame): + for name in other.attrs: + self.attrs[name] = other.attrs[name] + # For subclasses using _metadata. for name in self._metadata: object.__setattr__(self, name, getattr(other, name, None)) return self diff --git a/pandas/core/series.py b/pandas/core/series.py index 1039e9af929d4..e3604a2924cec 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -5,7 +5,7 @@ from io import StringIO from shutil import get_terminal_size from textwrap import dedent -from typing import Any, Callable +from typing import Any, Callable, Hashable, List import warnings import numpy as np @@ -29,7 +29,6 @@ is_dict_like, is_extension_array_dtype, is_extension_type, - is_hashable, is_integer, is_iterator, is_list_like, @@ -45,6 +44,7 @@ ABCSeries, ABCSparseArray, ) +from pandas.core.dtypes.inference import is_hashable from pandas.core.dtypes.missing import ( isna, na_value_for_dtype, @@ -173,7 +173,7 @@ class Series(base.IndexOpsMixin, generic.NDFrame): Copy input data. """ - _metadata = ["name"] + _metadata = [] # type: List[str] _accessors = {"dt", "cat", "str", "sparse"} _deprecations = ( base.IndexOpsMixin._deprecations @@ -324,7 +324,6 @@ def __init__( data = SingleBlockManager(data, index, fastpath=True) generic.NDFrame.__init__(self, data, fastpath=True) - self.name = name self._set_axis(0, index, fastpath=True) @@ -457,19 +456,6 @@ def _update_inplace(self, result, **kwargs): # we want to call the generic version and not the IndexOpsMixin return generic.NDFrame._update_inplace(self, result, **kwargs) - @property - def name(self): - """ - Return name of the Series. - """ - return self._name - - @name.setter - def name(self, value): - if value is not None and not is_hashable(value): - raise TypeError("Series.name must be a hashable type") - object.__setattr__(self, "_name", value) - # ndarray compatibility @property def dtype(self): @@ -485,6 +471,16 @@ def dtypes(self): """ return self._data.dtype + @property + def name(self) -> Hashable: + return self.attrs.get("name", None) + + @name.setter + def name(self, value: Hashable) -> None: + if not is_hashable(value): + raise TypeError("Series.name must be a hashable type") + self.attrs["name"] = value + @property def ftype(self): """