diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index d0d4796..e38c84d 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -199,6 +199,13 @@ class InteractiveShell(SingletonConfigurable): """An enhanced, interactive shell for Python.""" _instance = None + + ast_transformers = List([], config=True, help= + """ + A list of ast.NodeTransformer subclass instances, which will be applied + to user input before code is run. + """ + ) autocall = Enum((0,1,2), default_value=0, config=True, help= """ @@ -2630,6 +2637,8 @@ class InteractiveShell(SingletonConfigurable): self.execution_count += 1 return None + code_ast = self.transform_ast(code_ast) + interactivity = "none" if silent else self.ast_node_interactivity self.run_ast_nodes(code_ast.body, cell_name, interactivity=interactivity) @@ -2662,6 +2671,31 @@ class InteractiveShell(SingletonConfigurable): self.history_manager.store_output(self.execution_count) # Each cell is a *single* input, regardless of how many lines it has self.execution_count += 1 + + def transform_ast(self, node): + """Apply the AST transformations from self.ast_transformers + + Parameters + ---------- + node : ast.Node + The root node to be transformed. Typically called with the ast.Module + produced by parsing user input. + + Returns + ------- + An ast.Node corresponding to the node it was called with. Note that it + may also modify the passed object, so don't rely on references to the + original AST. + """ + for transformer in self.ast_transformers: + try: + node = transformer.visit(node) + except Exception: + warn("AST transformer %r threw an error. It will be unregistered.") + self.ast_transformers.remove(transformer) + + return node + def run_ast_nodes(self, nodelist, cell_name, interactivity='last_expr'): """Run a sequence of AST nodes. The execution mode depends on the diff --git a/IPython/core/tests/test_interactiveshell.py b/IPython/core/tests/test_interactiveshell.py index fe01c40..a318b60 100644 --- a/IPython/core/tests/test_interactiveshell.py +++ b/IPython/core/tests/test_interactiveshell.py @@ -20,6 +20,7 @@ Authors # Imports #----------------------------------------------------------------------------- # stdlib +import ast import os import shutil import sys @@ -414,6 +415,28 @@ class TestModules(unittest.TestCase, tt.TempFileMixin): out = "False\nFalse\nFalse\n" tt.ipexec_validate(self.fname, out) +class Negator(ast.NodeTransformer): + """Negates all number literals in an AST.""" + def visit_Num(self, node): + node.n = -node.n + return node + +class TestAstTransform(unittest.TestCase): + def setUp(self): + self.negator = Negator() + ip.ast_transformers.append(self.negator) + + def tearDown(self): + ip.ast_transformers.remove(self.negator) + + def test_run_cell(self): + with tt.AssertPrints('-34'): + ip.run_cell('print (12 + 22)') + + # A named reference to a number shouldn't be transformed. + ip.user_ns['n'] = 55 + with tt.AssertNotPrints('-55'): + ip.run_cell('print (n)') def test__IPYTHON__(): # This shouldn't raise a NameError, that's all