Skip to content

Commit 1c5619f

Browse files
committed
MAINT/BUG: Default inplace to False in pd.eval
Deprecated in 0.18.0. xref gh-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 1c5619f

File tree

4 files changed

+97
-47
lines changed

4 files changed

+97
-47
lines changed

doc/source/whatsnew/v0.21.0.txt

+3
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ Other API Changes
6060
^^^^^^^^^^^^^^^^^
6161

6262
- Moved definition of ``MergeError`` to the ``pandas.errors`` module.
63+
- :func:`eval` will now raise a ``ValueError`` in cases where we cannot operate properly on the ``target`` parameter, or when an inplace operation is specified but there is no item assignment in the expression (:issue:`16732`)
6364

6465

6566
.. _whatsnew_0210.deprecations:
@@ -76,6 +77,7 @@ Removal of prior version deprecations/changes
7677

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

8082

8183
.. _whatsnew_0210.performance:
@@ -139,3 +141,4 @@ Categorical
139141

140142
Other
141143
^^^^^
144+
- Bug in :func:`eval` where the ``inplace`` parameter was being incorrectly handled (:issue:`16732`)

pandas/core/computation/eval.py

+49-29
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,32 @@ 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
224+
There are many instances where such an error can be raised:
225+
226+
- `target=None`, but the expression is multiline.
227+
- The expression is multiline, but not all them have item assignment.
228+
- `inplace=True`, but the expression is missing item assignment.
229+
- Item assignment is provided, but the `target` does not support
230+
string item assignment.
231+
- Item assignment is provided and `inplace=False`, but the `target`
232+
does not support the `.copy()` method
233+
222234
Notes
223235
-----
224236
The ``dtype`` of any objects involved in an arithmetic ``%`` operation are
@@ -232,8 +244,9 @@ def eval(expr, parser='pandas', engine=None, truediv=True,
232244
pandas.DataFrame.query
233245
pandas.DataFrame.eval
234246
"""
235-
inplace = validate_bool_kwarg(inplace, 'inplace')
236-
first_expr = True
247+
248+
inplace = validate_bool_kwarg(inplace, "inplace")
249+
237250
if isinstance(expr, string_types):
238251
_check_expression(expr)
239252
exprs = [e.strip() for e in expr.splitlines() if e.strip() != '']
@@ -245,7 +258,10 @@ def eval(expr, parser='pandas', engine=None, truediv=True,
245258
raise ValueError("multi-line expressions are only valid in the "
246259
"context of data, use DataFrame.eval")
247260

261+
ret = None
248262
first_expr = True
263+
target_modified = False
264+
249265
for expr in exprs:
250266
expr = _convert_expression(expr)
251267
engine = _check_engine(engine)
@@ -266,28 +282,33 @@ def eval(expr, parser='pandas', engine=None, truediv=True,
266282
eng_inst = eng(parsed_expr)
267283
ret = eng_inst.evaluate()
268284

269-
if parsed_expr.assigner is None and multi_line:
270-
raise ValueError("Multi-line expressions are only valid"
271-
" if all expressions contain an assignment")
285+
if parsed_expr.assigner is None:
286+
if multi_line:
287+
raise ValueError("Multi-line expressions are only valid"
288+
" if all expressions contain an assignment")
289+
elif inplace:
290+
raise ValueError("Cannot operate inplace "
291+
"if there is no assignment")
272292

273293
# assign if needed
274294
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
295+
target_modified = True
283296

284297
# if returning a copy, copy only on the first assignment
285298
if not inplace and first_expr:
286-
target = env.target.copy()
299+
try:
300+
target = env.target.copy()
301+
except AttributeError:
302+
raise ValueError("Cannot return a copy of the target")
287303
else:
288304
target = env.target
289305

290-
target[parsed_expr.assigner] = ret
306+
# TypeError is most commonly raised (e.g. int, list), but you
307+
# get IndexError if you try to do this assignment on np.ndarray.
308+
try:
309+
target[parsed_expr.assigner] = ret
310+
except (TypeError, IndexError):
311+
raise ValueError("Cannot assign expression output to target")
291312

292313
if not resolvers:
293314
resolvers = ({parsed_expr.assigner: ret},)
@@ -304,7 +325,6 @@ def eval(expr, parser='pandas', engine=None, truediv=True,
304325
ret = None
305326
first_expr = False
306327

307-
if not inplace and inplace is not None:
308-
return target
309-
310-
return ret
328+
# We want to exclude `inplace=None` as being False.
329+
if inplace is False:
330+
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

+40-10
Original file line numberDiff line numberDiff line change
@@ -1311,14 +1311,6 @@ def assignment_not_inplace(self):
13111311
expected['c'] = expected['a'] + expected['b']
13121312
tm.assert_frame_equal(df, expected)
13131313

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-
13221314
def test_multi_line_expression(self):
13231315
# GH 11149
13241316
df = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]})
@@ -1388,14 +1380,52 @@ def test_assignment_in_query(self):
13881380
df.query('a = 1')
13891381
assert_frame_equal(df, df_orig)
13901382

1391-
def query_inplace(self):
1392-
# GH 11149
1383+
def test_query_inplace(self):
1384+
# see gh-11149
13931385
df = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]})
13941386
expected = df.copy()
13951387
expected = expected[expected['a'] == 2]
13961388
df.query('a == 2', inplace=True)
13971389
assert_frame_equal(expected, df)
13981390

1391+
df = {}
1392+
expected = {"a": 3}
1393+
1394+
self.eval("a = 1 + 2", target=df, inplace=True)
1395+
tm.assert_dict_equal(df, expected)
1396+
1397+
@pytest.mark.parametrize("invalid_target", [1, "cat", [1, 2],
1398+
np.array([]), (1, 3)])
1399+
def test_cannot_item_assign(self, invalid_target):
1400+
msg = "Cannot assign expression output to target"
1401+
expression = "a = 1 + 2"
1402+
1403+
with tm.assert_raises_regex(ValueError, msg):
1404+
self.eval(expression, target=invalid_target, inplace=True)
1405+
1406+
if hasattr(invalid_target, "copy"):
1407+
with tm.assert_raises_regex(ValueError, msg):
1408+
self.eval(expression, target=invalid_target, inplace=False)
1409+
1410+
@pytest.mark.parametrize("invalid_target", [1, "cat", (1, 3)])
1411+
def test_cannot_copy_item(self, invalid_target):
1412+
msg = "Cannot return a copy of the target"
1413+
expression = "a = 1 + 2"
1414+
1415+
with tm.assert_raises_regex(ValueError, msg):
1416+
self.eval(expression, target=invalid_target, inplace=False)
1417+
1418+
@pytest.mark.parametrize("target", [1, "cat", [1, 2],
1419+
np.array([]), (1, 3), {1: 2}])
1420+
def test_inplace_no_assignment(self, target):
1421+
expression = "1 + 2"
1422+
1423+
assert self.eval(expression, target=target, inplace=False) == 3
1424+
1425+
msg = "Cannot operate inplace if there is no assignment"
1426+
with tm.assert_raises_regex(ValueError, msg):
1427+
self.eval(expression, target=target, inplace=True)
1428+
13991429
def test_basic_period_index_boolean_expression(self):
14001430
df = mkdf(2, 2, data_gen_f=f, c_idx_type='p', r_idx_type='i')
14011431

0 commit comments

Comments
 (0)