Skip to content

Commit

Permalink
DEPR: Deprecate is_copy (#18801)
Browse files Browse the repository at this point in the history
- Renamed 'is_copy' attribute to '_is_copy' for internal use
- Setup getter and setter for 'is_copy'
- Added tests for deprecation warning
  • Loading branch information
chris committed Dec 18, 2017
1 parent b5f1e71 commit 9b8f2af
Show file tree
Hide file tree
Showing 8 changed files with 64 additions and 39 deletions.
1 change: 1 addition & 0 deletions doc/source/whatsnew/v0.22.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ Deprecations
- ``DataFrame.as_matrix`` is deprecated. Use ``DataFrame.values`` instead (:issue:`18458`).
- ``Series.asobject``, ``DatetimeIndex.asobject``, ``PeriodIndex.asobject`` and ``TimeDeltaIndex.asobject`` have been deprecated. Use ``.astype(object)`` instead (:issue:`18572`)
- ``Series.valid`` is deprecated. Use :meth:`Series.dropna` instead (:issue:`18800`).
- The ``is_copy`` attribute is deprecated and will be removed in a future version (:issue:`18801`).

.. _whatsnew_0220.prior_deprecations:

Expand Down
42 changes: 27 additions & 15 deletions pandas/core/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ class NDFrame(PandasObject, SelectionMixin):
axes : list
copy : boolean, default False
"""
_internal_names = ['_data', '_cacher', '_item_cache', '_cache', 'is_copy',
_internal_names = ['_data', '_cacher', '_item_cache', '_cache', '_is_copy',
'_subtyp', '_name', '_index', '_default_kind',
'_default_fill_value', '_metadata', '__array_struct__',
'__array_interface__']
Expand All @@ -117,7 +117,7 @@ class NDFrame(PandasObject, SelectionMixin):
_deprecations = frozenset(['as_blocks', 'blocks',
'consolidate', 'convert_objects'])
_metadata = []
is_copy = None
_is_copy = None

def __init__(self, data, axes=None, copy=False, dtype=None,
fastpath=False):
Expand All @@ -132,10 +132,22 @@ def __init__(self, data, axes=None, copy=False, dtype=None,
for i, ax in enumerate(axes):
data = data.reindex_axis(ax, axis=i)

object.__setattr__(self, 'is_copy', None)
object.__setattr__(self, '_is_copy', None)
object.__setattr__(self, '_data', data)
object.__setattr__(self, '_item_cache', {})

@property
def is_copy(self):
warnings.warn("Attribute 'is_copy' is deprecated and will be removed "
"in a future version.", FutureWarning, stacklevel=2)
return self._is_copy

@is_copy.setter
def is_copy(self, msg):
warnings.warn("Attribute 'is_copy' is deprecated and will be removed "
"in a future version.", FutureWarning, stacklevel=2)
self._is_copy = msg

def _repr_data_resource_(self):
"""
Not a real Jupyter special repr method, but we use the same
Expand Down Expand Up @@ -2153,7 +2165,7 @@ def _get_item_cache(self, item):
res._set_as_cached(item, self)

# for a chain
res.is_copy = self.is_copy
res._is_copy = self._is_copy
return res

def _set_as_cached(self, item, cacher):
Expand Down Expand Up @@ -2264,12 +2276,12 @@ def _set_item(self, key, value):

def _set_is_copy(self, ref=None, copy=True):
if not copy:
self.is_copy = None
self._is_copy = None
else:
if ref is not None:
self.is_copy = weakref.ref(ref)
self._is_copy = weakref.ref(ref)
else:
self.is_copy = None
self._is_copy = None

def _check_is_chained_assignment_possible(self):
"""
Expand All @@ -2288,7 +2300,7 @@ def _check_is_chained_assignment_possible(self):
self._check_setitem_copy(stacklevel=4, t='referant',
force=True)
return True
elif self.is_copy:
elif self._is_copy:
self._check_setitem_copy(stacklevel=4, t='referant')
return False

Expand Down Expand Up @@ -2323,7 +2335,7 @@ def _check_setitem_copy(self, stacklevel=4, t='setting', force=False):
"""

if force or self.is_copy:
if force or self._is_copy:

value = config.get_option('mode.chained_assignment')
if value is None:
Expand All @@ -2333,23 +2345,23 @@ def _check_setitem_copy(self, stacklevel=4, t='setting', force=False):
# the copy weakref
try:
gc.collect(2)
if not gc.get_referents(self.is_copy()):
self.is_copy = None
if not gc.get_referents(self._is_copy()):
self._is_copy = None
return
except Exception:
pass

# we might be a false positive
try:
if self.is_copy().shape == self.shape:
self.is_copy = None
if self._is_copy().shape == self.shape:
self._is_copy = None
return
except Exception:
pass

# a custom message
if isinstance(self.is_copy, string_types):
t = self.is_copy
if isinstance(self._is_copy, string_types):
t = self._is_copy

elif t == 'referant':
t = ("\n"
Expand Down
12 changes: 6 additions & 6 deletions pandas/core/indexes/accessors.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,9 @@ def _delegate_property_get(self, name):
result = Series(result, index=self.index, name=self.name)

# setting this object will show a SettingWithCopyWarning/Error
result.is_copy = ("modifications to a property of a datetimelike "
"object are not supported and are discarded. "
"Change values on the original.")
result._is_copy = ("modifications to a property of a datetimelike "
"object are not supported and are discarded. "
"Change values on the original.")

return result

Expand All @@ -136,9 +136,9 @@ def _delegate_method(self, name, *args, **kwargs):
result = Series(result, index=self.index, name=self.name)

# setting this object will show a SettingWithCopyWarning/Error
result.is_copy = ("modifications to a method of a datetimelike object "
"are not supported and are discarded. Change "
"values on the original.")
result._is_copy = ("modifications to a method of a datetimelike "
"object are not supported and are discarded. "
"Change values on the original.")

return result

Expand Down
2 changes: 1 addition & 1 deletion pandas/core/indexing.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ def _setitem_with_indexer(self, indexer, value):
labels = index.insert(len(index), key)
self.obj._data = self.obj.reindex(labels, axis=i)._data
self.obj._maybe_update_cacher(clear=True)
self.obj.is_copy = None
self.obj._is_copy = None

nindexer.append(labels.get_loc(key))

Expand Down
4 changes: 2 additions & 2 deletions pandas/tests/indexes/test_multi.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,10 +475,10 @@ def test_set_value_keeps_names(self):
columns=['one', 'two', 'three', 'four'],
index=idx)
df = df.sort_index()
assert df.is_copy is None
assert df._is_copy is None
assert df.index.names == ('Name', 'Number')
df.at[('grethe', '4'), 'one'] = 99.34
assert df.is_copy is None
assert df._is_copy is None
assert df.index.names == ('Name', 'Number')

def test_copy_names(self):
Expand Down
38 changes: 25 additions & 13 deletions pandas/tests/indexing/test_chaining_and_caching.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from pandas import (compat, DataFrame, option_context,
Series, MultiIndex, date_range, Timestamp)
from pandas.util import testing as tm
from pandas.core.common import SettingWithCopyError, SettingWithCopyWarning


class TestCaching(object):
Expand Down Expand Up @@ -136,7 +137,7 @@ def test_detect_chained_assignment(self):
expected = DataFrame([[-5, 1], [-6, 3]], columns=list('AB'))
df = DataFrame(np.arange(4).reshape(2, 2),
columns=list('AB'), dtype='int64')
assert df.is_copy is None
assert df._is_copy is None

df['A'][0] = -5
df['A'][1] = -6
Expand All @@ -145,15 +146,15 @@ def test_detect_chained_assignment(self):
# test with the chaining
df = DataFrame({'A': Series(range(2), dtype='int64'),
'B': np.array(np.arange(2, 4), dtype=np.float64)})
assert df.is_copy is None
assert df._is_copy is None

with pytest.raises(com.SettingWithCopyError):
df['A'][0] = -5

with pytest.raises(com.SettingWithCopyError):
df['A'][1] = np.nan

assert df['A'].is_copy is None
assert df['A']._is_copy is None

# Using a copy (the chain), fails
df = DataFrame({'A': Series(range(2), dtype='int64'),
Expand All @@ -166,7 +167,7 @@ def test_detect_chained_assignment(self):
df = DataFrame({'a': ['one', 'one', 'two', 'three',
'two', 'one', 'six'],
'c': Series(range(7), dtype='int64')})
assert df.is_copy is None
assert df._is_copy is None

with pytest.raises(com.SettingWithCopyError):
indexer = df.a.str.startswith('o')
Expand All @@ -186,7 +187,7 @@ def test_detect_chained_assignment(self):

# gh-5475: Make sure that is_copy is picked up reconstruction
df = DataFrame({"A": [1, 2]})
assert df.is_copy is None
assert df._is_copy is None

with tm.ensure_clean('__tmp__pickle') as path:
df.to_pickle(path)
Expand All @@ -211,39 +212,39 @@ def random_text(nobs=100):

# Always a copy
x = df.iloc[[0, 1, 2]]
assert x.is_copy is not None
assert x._is_copy is not None

x = df.iloc[[0, 1, 2, 4]]
assert x.is_copy is not None
assert x._is_copy is not None

# Explicitly copy
indexer = df.letters.apply(lambda x: len(x) > 10)
df = df.loc[indexer].copy()

assert df.is_copy is None
assert df._is_copy is None
df['letters'] = df['letters'].apply(str.lower)

# Implicitly take
df = random_text(100000)
indexer = df.letters.apply(lambda x: len(x) > 10)
df = df.loc[indexer]

assert df.is_copy is not None
assert df._is_copy is not None
df['letters'] = df['letters'].apply(str.lower)

# Implicitly take 2
df = random_text(100000)
indexer = df.letters.apply(lambda x: len(x) > 10)

df = df.loc[indexer]
assert df.is_copy is not None
assert df._is_copy is not None
df.loc[:, 'letters'] = df['letters'].apply(str.lower)

# Should be ok even though it's a copy!
assert df.is_copy is None
assert df._is_copy is None

df['letters'] = df['letters'].apply(str.lower)
assert df.is_copy is None
assert df._is_copy is None

df = random_text(100000)
indexer = df.letters.apply(lambda x: len(x) > 10)
Expand All @@ -252,7 +253,7 @@ def random_text(nobs=100):

# an identical take, so no copy
df = DataFrame({'a': [1]}).dropna()
assert df.is_copy is None
assert df._is_copy is None
df['a'] += 1

# Inplace ops, originally from:
Expand Down Expand Up @@ -418,3 +419,14 @@ def test_cache_updating(self):
tm.assert_frame_equal(df, expected)
expected = Series([0, 0, 0, 2, 0], name='f')
tm.assert_series_equal(df.f, expected)

def test_deprecate_is_copy(self):
#GH18801
df = DataFrame({"A": [1, 2, 3]})
with tm.assert_produces_warning(FutureWarning, check_stacklevel=False):
# getter
is_copy = df.is_copy

with tm.assert_produces_warning(FutureWarning, check_stacklevel=False):
# setter
df.is_copy = "test deprecated is_copy"
2 changes: 1 addition & 1 deletion pandas/tests/test_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,7 @@ def test_xs(self):
# Mixed-type yields a copy.
self.panel['strings'] = 'foo'
result = self.panel.xs('D', axis=2)
assert result.is_copy is not None
assert result._is_copy is not None

def test_getitem_fancy_labels(self):
with catch_warnings(record=True):
Expand Down
2 changes: 1 addition & 1 deletion pandas/tests/test_panel4d.py
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,7 @@ def test_xs(self):
with catch_warnings(record=True):
result = self.panel4d.xs('D', axis=3)

assert result.is_copy is not None
assert result._is_copy is not None

def test_getitem_fancy_labels(self):
with catch_warnings(record=True):
Expand Down

0 comments on commit 9b8f2af

Please sign in to comment.