33"""Top level ``eval`` module.
44"""
55
6+ import warnings
67import tokenize
78from pandas .core import common as com
89from pandas .computation .expr import Expr , _parsers , tokenize_string
910from pandas .computation .scope import _ensure_scope
10- from pandas .compat import DeepChainMap , builtins
11+ from pandas .compat import string_types
1112from pandas .computation .engines import _engines
1213from distutils .version import LooseVersion
1314
@@ -138,7 +139,7 @@ def _check_for_locals(expr, stack_level, parser):
138139
139140def eval (expr , parser = 'pandas' , engine = 'numexpr' , truediv = True ,
140141 local_dict = None , global_dict = None , resolvers = (), level = 0 ,
141- target = None ):
142+ target = None , inplace = None ):
142143 """Evaluate a Python expression as a string using various backends.
143144
144145 The following arithmetic operations are supported: ``+``, ``-``, ``*``,
@@ -196,6 +197,13 @@ def eval(expr, parser='pandas', engine='numexpr', truediv=True,
196197 scope. Most users will **not** need to change this parameter.
197198 target : a target object for assignment, optional, default is None
198199 essentially this is a passed in resolver
200+ inplace : bool, default True
201+ If expression mutates, whether to modify object inplace or return
202+ copy with mutation.
203+
204+ WARNING: inplace=None currently falls back to to True, but
205+ in a future version, will default to False. Use inplace=True
206+ explicitly rather than relying on the default.
199207
200208 Returns
201209 -------
@@ -214,29 +222,78 @@ def eval(expr, parser='pandas', engine='numexpr', truediv=True,
214222 pandas.DataFrame.query
215223 pandas.DataFrame.eval
216224 """
217- expr = _convert_expression (expr )
218- _check_engine (engine )
219- _check_parser (parser )
220- _check_resolvers (resolvers )
221- _check_for_locals (expr , level , parser )
222-
223- # get our (possibly passed-in) scope
224- level += 1
225- env = _ensure_scope (level , global_dict = global_dict ,
226- local_dict = local_dict , resolvers = resolvers ,
227- target = target )
228-
229- parsed_expr = Expr (expr , engine = engine , parser = parser , env = env ,
230- truediv = truediv )
231-
232- # construct the engine and evaluate the parsed expression
233- eng = _engines [engine ]
234- eng_inst = eng (parsed_expr )
235- ret = eng_inst .evaluate ()
236-
237- # assign if needed
238- if env .target is not None and parsed_expr .assigner is not None :
239- env .target [parsed_expr .assigner ] = ret
240- return None
225+ first_expr = True
226+ if isinstance (expr , string_types ):
227+ exprs = [e for e in expr .splitlines () if e != '' ]
228+ else :
229+ exprs = [expr ]
230+ multi_line = len (exprs ) > 1
231+
232+ if multi_line and target is None :
233+ raise ValueError ("multi-line expressions are only valid in the "
234+ "context of data, use DataFrame.eval" )
235+
236+ first_expr = True
237+ for expr in exprs :
238+ expr = _convert_expression (expr )
239+ _check_engine (engine )
240+ _check_parser (parser )
241+ _check_resolvers (resolvers )
242+ _check_for_locals (expr , level , parser )
243+
244+ # get our (possibly passed-in) scope
245+ level += 1
246+ env = _ensure_scope (level , global_dict = global_dict ,
247+ local_dict = local_dict , resolvers = resolvers ,
248+ target = target )
249+
250+ parsed_expr = Expr (expr , engine = engine , parser = parser , env = env ,
251+ truediv = truediv )
252+
253+ # construct the engine and evaluate the parsed expression
254+ eng = _engines [engine ]
255+ eng_inst = eng (parsed_expr )
256+ ret = eng_inst .evaluate ()
257+
258+ if parsed_expr .assigner is None and multi_line :
259+ raise ValueError ("Multi-line expressions are only valid"
260+ " if all expressions contain an assignment" )
261+
262+ # assign if needed
263+ if env .target is not None and parsed_expr .assigner is not None :
264+ if inplace is None :
265+ warnings .warn (
266+ "eval expressions containing an assignment currently"
267+ "default to operating inplace.\n This will change in "
268+ "a future version of pandas, use inplace=True to "
269+ "avoid this warning." ,
270+ FutureWarning , stacklevel = 3 )
271+ inplace = True
272+
273+ # if returning a copy, copy only on the first assignment
274+ if not inplace and first_expr :
275+ target = env .target .copy ()
276+ else :
277+ target = env .target
278+
279+ target [parsed_expr .assigner ] = ret
280+
281+ if not resolvers :
282+ resolvers = ({parsed_expr .assigner : ret },)
283+ else :
284+ # existing resolver needs updated to handle
285+ # case of mutating existing column in copy
286+ for resolver in resolvers :
287+ if parsed_expr .assigner in resolver :
288+ resolver [parsed_expr .assigner ] = ret
289+ break
290+ else :
291+ resolvers += ({parsed_expr .assigner : ret },)
292+
293+ ret = None
294+ first_expr = False
295+
296+ if not inplace and inplace is not None :
297+ return target
241298
242299 return ret
0 commit comments