From 22ad14f210b3259fccb9edfc3c0b49722a402f29 2018-11-29 11:45:07 From: Min RK Date: 2018-11-29 11:45:07 Subject: [PATCH] Don't expand user variables in execution magics Adds magic.no_var_expand decorator to mark a magic as opting out of variable expansion Most useful for magics that take Python code on the line, e.g. %timeit, etc. --- diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index ac46abe..696bab2 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -2273,10 +2273,14 @@ class InteractiveShell(SingletonConfigurable): # Note: this is the distance in the stack to the user's frame. # This will need to be updated if the internal calling logic gets # refactored, or else we'll be expanding the wrong variables. - + # Determine stack_depth depending on where run_line_magic() has been called stack_depth = _stack_depth - magic_arg_s = self.var_expand(line, stack_depth) + if getattr(fn, magic.MAGIC_NO_VAR_EXPAND_ATTR, False): + # magic has opted out of var_expand + magic_arg_s = line + else: + magic_arg_s = self.var_expand(line, stack_depth) # Put magic args in a list so we can call with f(*a) syntax args = [magic_arg_s] kwargs = {} @@ -2284,12 +2288,12 @@ class InteractiveShell(SingletonConfigurable): if getattr(fn, "needs_local_scope", False): kwargs['local_ns'] = sys._getframe(stack_depth).f_locals with self.builtin_trap: - result = fn(*args,**kwargs) + result = fn(*args, **kwargs) return result def run_cell_magic(self, magic_name, line, cell): """Execute the given cell magic. - + Parameters ---------- magic_name : str @@ -2318,7 +2322,11 @@ class InteractiveShell(SingletonConfigurable): # This will need to be updated if the internal calling logic gets # refactored, or else we'll be expanding the wrong variables. stack_depth = 2 - magic_arg_s = self.var_expand(line, stack_depth) + if getattr(fn, magic.MAGIC_NO_VAR_EXPAND_ATTR, False): + # magic has opted out of var_expand + magic_arg_s = line + else: + magic_arg_s = self.var_expand(line, stack_depth) with self.builtin_trap: result = fn(magic_arg_s, cell) return result diff --git a/IPython/core/magic.py b/IPython/core/magic.py index c387d4f..02af2f8 100644 --- a/IPython/core/magic.py +++ b/IPython/core/magic.py @@ -265,6 +265,25 @@ def _function_magic_marker(magic_kind): return magic_deco +MAGIC_NO_VAR_EXPAND_ATTR = '_ipython_magic_no_var_expand' + + +def no_var_expand(magic_func): + """Mark a magic function as not needing variable expansion + + By default, IPython interprets `{a}` or `$a` in the line passed to magics + as variables that should be interpolated from the interactive namespace + before passing the line to the magic function. + This is not always desirable, e.g. when the magic executes Python code + (%timeit, %time, etc.). + Decorate magics with `@no_var_expand` to opt-out of variable expansion. + + .. versionadded:: 7.2 + """ + setattr(magic_func, MAGIC_NO_VAR_EXPAND_ATTR, True) + return magic_func + + # Create the actual decorators for public use # These three are used to decorate methods in class definitions diff --git a/IPython/core/magics/execution.py b/IPython/core/magics/execution.py index b651a42..130c5f9 100644 --- a/IPython/core/magics/execution.py +++ b/IPython/core/magics/execution.py @@ -36,7 +36,8 @@ from IPython.core import page from IPython.core.error import UsageError from IPython.core.macro import Macro from IPython.core.magic import (Magics, magics_class, line_magic, cell_magic, - line_cell_magic, on_off, needs_local_scope) + line_cell_magic, on_off, needs_local_scope, + no_var_expand) from IPython.testing.skipdoctest import skip_doctest from IPython.utils.contexts import preserve_keys from IPython.utils.capture import capture_output @@ -184,6 +185,7 @@ python packages because of its non-free license. To use profiling, install the python-profiler package from non-free.""") @skip_doctest + @no_var_expand @line_cell_magic def prun(self, parameter_s='', cell=None): @@ -293,6 +295,11 @@ python-profiler package from non-free.""") You can read the complete documentation for the profile module with:: In [1]: import profile; profile.help() + + .. versionchanged:: 7.2 + User variables are no longer expanded, + the magic line is always left unmodified. + """ opts, arg_str = self.parse_options(parameter_s, 'D:l:rs:T:q', list_all=True, posix=False) @@ -422,6 +429,7 @@ python-profiler package from non-free.""") You can omit this in cell magic mode. """ ) + @no_var_expand @line_cell_magic def debug(self, line='', cell=None): """Activate the interactive debugger. @@ -442,6 +450,11 @@ python-profiler package from non-free.""") If you want IPython to automatically do this on every exception, see the %pdb magic for more details. + + .. versionchanged:: 7.2 + When running code, user variables are no longer expanded, + the magic line is always left unmodified. + """ args = magic_arguments.parse_argstring(self.debug, line) @@ -972,6 +985,7 @@ python-profiler package from non-free.""") print("Wall time: %10.2f s." % (twall1 - twall0)) @skip_doctest + @no_var_expand @line_cell_magic @needs_local_scope def timeit(self, line='', cell=None, local_ns=None): @@ -1017,6 +1031,9 @@ python-profiler package from non-free.""") -o: return a TimeitResult that can be stored in a variable to inspect the result in more details. + .. versionchanged:: 7.2 + User variables are no longer expanded, + the magic line is always left unmodified. Examples -------- @@ -1161,6 +1178,7 @@ python-profiler package from non-free.""") return timeit_result @skip_doctest + @no_var_expand @needs_local_scope @line_cell_magic def time(self,line='', cell=None, local_ns=None): @@ -1175,12 +1193,16 @@ python-profiler package from non-free.""") - In line mode you can time a single-line statement (though multiple ones can be chained with using semicolons). - - In cell mode, you can time the cell body (a directly + - In cell mode, you can time the cell body (a directly following statement raises an error). - This function provides very basic timing functionality. Use the timeit + This function provides very basic timing functionality. Use the timeit magic for more control over the measurement. + .. versionchanged:: 7.2 + User variables are no longer expanded, + the magic line is always left unmodified. + Examples -------- :: diff --git a/IPython/core/tests/test_magic.py b/IPython/core/tests/test_magic.py index 6973db0..310751e 100644 --- a/IPython/core/tests/test_magic.py +++ b/IPython/core/tests/test_magic.py @@ -1087,7 +1087,8 @@ def test_logging_magic_quiet_from_config(): lm.logstart(os.path.join(td, "quiet_from_config.log")) finally: _ip.logger.logstop() - + + def test_logging_magic_not_quiet(): _ip.config.LoggingMagics.quiet = False lm = logging.LoggingMagics(shell=_ip) @@ -1098,9 +1099,15 @@ def test_logging_magic_not_quiet(): finally: _ip.logger.logstop() -## + +def test_time_no_var_expand(): + _ip.user_ns['a'] = 5 + _ip.user_ns['b'] = [] + _ip.magic('time b.append("{a}")') + assert _ip.user_ns['b'] == ['{a}'] + + # this is slow, put at the end for local testing. -## def test_timeit_arguments(): "Test valid timeit arguments, should not cause SyntaxError (GH #1269)" if sys.version_info < (3,7):