Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 38 additions & 10 deletions IPython/core/interactiveshell.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@
from logging import error
import IPython.core.hooks

from typing import List as ListType
from ast import AST

# NoOpContext is deprecated, but ipykernel imports it from here.
# See https://github.com/ipython/ipykernel/issues/157
from IPython.utils.contexts import NoOpContext
Expand All @@ -102,6 +105,13 @@ class ProvisionalWarning(DeprecationWarning):
"""
pass

if sys.version_info > (3,6):
_assign_nodes = (ast.AugAssign, ast.AnnAssign, ast.Assign)
_single_targets_nodes = (ast.AugAssign, ast.AnnAssign)
else:
_assign_nodes = (ast.AugAssign, ast.Assign )
_single_targets_nodes = (ast.AugAssign, )

#-----------------------------------------------------------------------------
# Globals
#-----------------------------------------------------------------------------
Expand Down Expand Up @@ -376,11 +386,12 @@ def _prompt_trait_changed(self, change):
"""
).tag(config=True)

ast_node_interactivity = Enum(['all', 'last', 'last_expr', 'none'],
ast_node_interactivity = Enum(['all', 'last', 'last_expr', 'none', 'last_expr_or_assign'],
default_value='last_expr',
help="""
'all', 'last', 'last_expr' or 'none', specifying which nodes should be
run interactively (displaying output from expressions)."""
'all', 'last', 'last_expr' or 'none', 'last_expr_or_assign' specifying
which nodes should be run interactively (displaying output from expressions).
"""
).tag(config=True)

# TODO: this part of prompt management should be moved to the frontends.
Expand Down Expand Up @@ -2749,7 +2760,7 @@ def transform_ast(self, node):
return node


def run_ast_nodes(self, nodelist, cell_name, interactivity='last_expr',
def run_ast_nodes(self, nodelist:ListType[AST], cell_name:str, interactivity='last_expr',
compiler=compile, result=None):
"""Run a sequence of AST nodes. The execution mode depends on the
interactivity parameter.
Expand All @@ -2762,11 +2773,13 @@ def run_ast_nodes(self, nodelist, cell_name, interactivity='last_expr',
Will be passed to the compiler as the filename of the cell. Typically
the value returned by ip.compile.cache(cell).
interactivity : str
'all', 'last', 'last_expr' or 'none', specifying which nodes should be
run interactively (displaying output from expressions). 'last_expr'
will run the last node interactively only if it is an expression (i.e.
expressions in loops or other blocks are not displayed. Other values
for this parameter will raise a ValueError.
'all', 'last', 'last_expr' , 'last_expr_or_assign' or 'none',
specifying which nodes should be run interactively (displaying output
from expressions). 'last_expr' will run the last node interactively
only if it is an expression (i.e. expressions in loops or other blocks
are not displayed) 'last_expr_or_assign' will run the last expression
or the last assignment. Other values for this parameter will raise a
ValueError.
compiler : callable
A function with the same interface as the built-in compile(), to turn
the AST nodes into code objects. Default is the built-in compile().
Expand All @@ -2781,6 +2794,21 @@ def run_ast_nodes(self, nodelist, cell_name, interactivity='last_expr',
if not nodelist:
return

if interactivity == 'last_expr_or_assign':
if isinstance(nodelist[-1], _assign_nodes):
asg = nodelist[-1]
if isinstance(asg, ast.Assign) and len(asg.targets) == 1:
target = asg.targets[0]
elif isinstance(asg, _single_targets_nodes):
target = asg.target
else:
target = None
if isinstance(target, ast.Name):
nnode = ast.Expr(ast.Name(target.id, ast.Load()))
ast.fix_missing_locations(nnode)
nodelist.append(nnode)
interactivity = 'last_expr'

if interactivity == 'last_expr':
if isinstance(nodelist[-1], ast.Expr):
interactivity = "last"
Expand Down Expand Up @@ -2924,7 +2952,7 @@ def enable_matplotlib(self, gui=None):
self.pylab_gui_select = gui
# Otherwise if they are different
elif gui != self.pylab_gui_select:
print ('Warning: Cannot change to a different GUI toolkit: %s.'
print('Warning: Cannot change to a different GUI toolkit: %s.'
' Using %s instead.' % (gui, self.pylab_gui_select))
gui, backend = pt.find_gui_and_backend(self.pylab_gui_select)

Expand Down
48 changes: 48 additions & 0 deletions IPython/core/tests/test_displayhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,51 @@ def test_underscore_no_overrite_builtins():
ip.run_cell('print(_)', store_history=True)
ip.run_cell('import builtins; del builtins._')


def test_interactivehooks_ast_modes():
"""
Test that ast nodes can be triggerd with different modes
"""
saved_mode = ip.ast_node_interactivity
ip.ast_node_interactivity = 'last_expr_or_assign'

try:
with AssertPrints('2'):
ip.run_cell('a = 1+1', store_history=True)

with AssertPrints('9'):
ip.run_cell('b = 1+8 # comment with a semicolon;', store_history=False)

with AssertPrints('7'):
ip.run_cell('c = 1+6\n#commented_out_function();', store_history=True)

ip.run_cell('d = 11', store_history=True)
with AssertPrints('12'):
ip.run_cell('d += 1', store_history=True)

with AssertNotPrints('42'):
ip.run_cell('(u,v) = (41+1, 43-1)')

finally:
ip.ast_node_interactivity = saved_mode

def test_interactivehooks_ast_modes_semi_supress():
"""
Test that ast nodes can be triggerd with different modes and supressed
by semicolon
"""
saved_mode = ip.ast_node_interactivity
ip.ast_node_interactivity = 'last_expr_or_assign'

try:
with AssertNotPrints('2'):
ip.run_cell('x = 1+1;', store_history=True)

with AssertNotPrints('7'):
ip.run_cell('y = 1+6; # comment with a semicolon', store_history=True)

with AssertNotPrints('9'):
ip.run_cell('z = 1+8;\n#commented_out_function()', store_history=True)

finally:
ip.ast_node_interactivity = saved_mode
22 changes: 22 additions & 0 deletions docs/source/whatsnew/pr/interactive_assignment.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
IPython can now trigger the display hook on last assignment of cells.
Up until 6.0 the following code wouldn't show the value of the assigned
variable::

In[1]: xyz = "something"
# nothing shown

You would have to actually make it the last statement::

In [2]: xyz = "something else"
... : xyz
Out[2]: "something else"

With the option ``InteractiveShell.ast_node_interactivity='last_expr_or_assign'``
you can now do::

In [2]: xyz = "something else"
Out[2]: "something else"

This option can be toggled at runtime with the ``%config`` magic, and will
trigger on assignment ``a = 1``, augmented assignment ``+=``, ``-=``, ``|=`` ...
as well as type annotated assignments: ``a:int = 2``.