Skip to content

Commit 59d0115

Browse files
committed
MAINT/BUG: Default inplace to False in pd.eval
Deprecated in 0.18.0. xref pandas-devgh-11149. Also patches bug where we were improperly handling the inplace=False condition, as we were assuming that target input was non-None when that wasn't necessarily enforced.
1 parent 18f7b1c commit 59d0115

File tree

4 files changed

+76
-42
lines changed

4 files changed

+76
-42
lines changed

doc/source/whatsnew/v0.21.0.txt

+2
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ Removal of prior version deprecations/changes
7676

7777
- :func:`read_excel()` has dropped the ``has_index_names`` parameter (:issue:`10967`)
7878
- ``Categorical`` has dropped the ``.order()`` and ``.sort()`` methods in favor of ``.sort_values()`` (:issue:`12882`)
79+
- :func:`eval` and :method:`DataFrame.eval` have changed the default of ``inplace`` from ``None`` to ``False`` (:issue:`11149`)
7980

8081

8182
.. _whatsnew_0210.performance:
@@ -139,3 +140,4 @@ Categorical
139140

140141
Other
141142
^^^^^
143+
- Bug in :func:`eval` where the ``inplace`` parameter was being incorrectly handled (:issue:`16732`)

pandas/core/computation/eval.py

+36-26
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
"""Top level ``eval`` module.
44
"""
55

6-
import warnings
76
import tokenize
87
from pandas.io.formats.printing import pprint_thing
98
from pandas.core.computation import _NUMEXPR_INSTALLED
@@ -148,7 +147,7 @@ def _check_for_locals(expr, stack_level, parser):
148147

149148
def eval(expr, parser='pandas', engine=None, truediv=True,
150149
local_dict=None, global_dict=None, resolvers=(), level=0,
151-
target=None, inplace=None):
150+
target=None, inplace=False):
152151
"""Evaluate a Python expression as a string using various backends.
153152
154153
The following arithmetic operations are supported: ``+``, ``-``, ``*``,
@@ -206,19 +205,26 @@ def eval(expr, parser='pandas', engine=None, truediv=True,
206205
The number of prior stack frames to traverse and add to the current
207206
scope. Most users will **not** need to change this parameter.
208207
target : a target object for assignment, optional, default is None
209-
essentially this is a passed in resolver
210-
inplace : bool, default True
211-
If expression mutates, whether to modify object inplace or return
212-
copy with mutation.
213-
214-
WARNING: inplace=None currently falls back to to True, but
215-
in a future version, will default to False. Use inplace=True
216-
explicitly rather than relying on the default.
208+
This is essentially passed into the resolver and is used when there
209+
is variable assignment in the expression. If so, then `target` must
210+
support item assignment with string keys, and if a copy is being
211+
returned, it must also support `.copy()`.
212+
inplace : bool, default False
213+
If `target` is provided, and the expression mutates `target`, whether
214+
to modify `target` inplace. Otherwise, return a copy of `target` with
215+
the mutation.
217216
218217
Returns
219218
-------
220219
ndarray, numeric scalar, DataFrame, Series
221220
221+
Raises
222+
------
223+
ValueError : 1) A non-None `target` was passed in that does not support
224+
string item assignment.
225+
2) `inplace=False`, and a non-None `target` was passed in
226+
that does not support `.copy()`.
227+
222228
Notes
223229
-----
224230
The ``dtype`` of any objects involved in an arithmetic ``%`` operation are
@@ -232,8 +238,9 @@ def eval(expr, parser='pandas', engine=None, truediv=True,
232238
pandas.DataFrame.query
233239
pandas.DataFrame.eval
234240
"""
235-
inplace = validate_bool_kwarg(inplace, 'inplace')
236-
first_expr = True
241+
242+
inplace = validate_bool_kwarg(inplace, "inplace")
243+
237244
if isinstance(expr, string_types):
238245
_check_expression(expr)
239246
exprs = [e.strip() for e in expr.splitlines() if e.strip() != '']
@@ -245,7 +252,10 @@ def eval(expr, parser='pandas', engine=None, truediv=True,
245252
raise ValueError("multi-line expressions are only valid in the "
246253
"context of data, use DataFrame.eval")
247254

255+
ret = None
248256
first_expr = True
257+
target_modified = False
258+
249259
for expr in exprs:
250260
expr = _convert_expression(expr)
251261
engine = _check_engine(engine)
@@ -272,22 +282,23 @@ def eval(expr, parser='pandas', engine=None, truediv=True,
272282

273283
# assign if needed
274284
if env.target is not None and parsed_expr.assigner is not None:
275-
if inplace is None:
276-
warnings.warn(
277-
"eval expressions containing an assignment currently"
278-
"default to operating inplace.\nThis will change in "
279-
"a future version of pandas, use inplace=True to "
280-
"avoid this warning.",
281-
FutureWarning, stacklevel=3)
282-
inplace = True
285+
target_modified = True
283286

284287
# if returning a copy, copy only on the first assignment
285288
if not inplace and first_expr:
286-
target = env.target.copy()
289+
try:
290+
target = env.target.copy()
291+
except AttributeError:
292+
raise ValueError("Cannot return a copy of the target")
287293
else:
288294
target = env.target
289295

290-
target[parsed_expr.assigner] = ret
296+
# TypeError is most commonly raised (e.g. int, list), but you
297+
# get IndexError if you try to do this assignment on np.ndarray.
298+
try:
299+
target[parsed_expr.assigner] = ret
300+
except (TypeError, IndexError):
301+
raise ValueError("Cannot assign expression output to target")
291302

292303
if not resolvers:
293304
resolvers = ({parsed_expr.assigner: ret},)
@@ -304,7 +315,6 @@ def eval(expr, parser='pandas', engine=None, truediv=True,
304315
ret = None
305316
first_expr = False
306317

307-
if not inplace and inplace is not None:
308-
return target
309-
310-
return ret
318+
# We want to exclude `inplace=None` as being False.
319+
if inplace is False:
320+
return target if target_modified else ret

pandas/core/frame.py

+5-8
Original file line numberDiff line numberDiff line change
@@ -2224,21 +2224,18 @@ def query(self, expr, inplace=False, **kwargs):
22242224
else:
22252225
return new_data
22262226

2227-
def eval(self, expr, inplace=None, **kwargs):
2227+
def eval(self, expr, inplace=False, **kwargs):
22282228
"""Evaluate an expression in the context of the calling DataFrame
22292229
instance.
22302230
22312231
Parameters
22322232
----------
22332233
expr : string
22342234
The expression string to evaluate.
2235-
inplace : bool
2236-
If the expression contains an assignment, whether to return a new
2237-
DataFrame or mutate the existing.
2238-
2239-
WARNING: inplace=None currently falls back to to True, but
2240-
in a future version, will default to False. Use inplace=True
2241-
explicitly rather than relying on the default.
2235+
inplace : bool, default False
2236+
If the expression contains an assignment, whether to perform the
2237+
operation inplace and mutate the existing DataFrame. Otherwise,
2238+
a new DataFrame is returned.
22422239
22432240
.. versionadded:: 0.18.0
22442241

pandas/tests/computation/test_eval.py

+33-8
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ def engine_has_neg_frac(engine):
5858
return _engines[engine].has_neg_frac
5959

6060

61+
@pytest.fixture(params=[1, "cat", [1, 2], np.array([]), (1, 3)])
62+
def invalid_target(request):
63+
return request.param
64+
65+
6166
def _eval_single_bin(lhs, cmp1, rhs, engine):
6267
c = _binary_ops_dict[cmp1]
6368
if engine_has_neg_frac(engine):
@@ -1311,14 +1316,6 @@ def assignment_not_inplace(self):
13111316
expected['c'] = expected['a'] + expected['b']
13121317
tm.assert_frame_equal(df, expected)
13131318

1314-
# Default for inplace will change
1315-
with tm.assert_produces_warnings(FutureWarning):
1316-
df.eval('c = a + b')
1317-
1318-
# but don't warn without assignment
1319-
with tm.assert_produces_warnings(None):
1320-
df.eval('a + b')
1321-
13221319
def test_multi_line_expression(self):
13231320
# GH 11149
13241321
df = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]})
@@ -1396,6 +1393,34 @@ def query_inplace(self):
13961393
df.query('a == 2', inplace=True)
13971394
assert_frame_equal(expected, df)
13981395

1396+
df = {}
1397+
expected = {"a": 3}
1398+
1399+
self.eval("a = 1 + 2", target=df, inplace=True)
1400+
tm.assert_dict_equal(df, expected)
1401+
1402+
def cannot_item_assign(self, invalid_target):
1403+
msg = "Cannot assign expression output to target"
1404+
expr = "a = 1 + 2"
1405+
1406+
with tm.assert_raises_regex(ValueError, msg):
1407+
self.eval(expr, target=invalid_target, inplace=True)
1408+
1409+
def cannot_copy_item(self, invalid_target):
1410+
msg = "Cannot return a copy of the target"
1411+
expr = "a = 1 + 2"
1412+
1413+
with tm.assert_raises_regex(ValueError, msg):
1414+
self.eval(expr, target=invalid_target, inplace=False)
1415+
1416+
def ignore_invalid_target_no_assignment(self, invalid_target):
1417+
# No Exception should be raised because we are
1418+
# not performing item assignment in this test.
1419+
expr = "1 + 2"
1420+
1421+
assert self.eval(expr, target=invalid_target, inplace=False) == 3
1422+
assert self.eval(expr, target=invalid_target, inplace=True) is None
1423+
13991424
def test_basic_period_index_boolean_expression(self):
14001425
df = mkdf(2, 2, data_gen_f=f, c_idx_type='p', r_idx_type='i')
14011426

0 commit comments

Comments
 (0)