Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PR: Make running a single line work on partial statements (IPython console) #21557

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
15 changes: 10 additions & 5 deletions spyder/plugins/editor/widgets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -512,10 +512,11 @@ def get_cell_list(self):
return []

def get_selection_as_executable_code(self, cursor=None):
"""Get selected text in a way that allows other plugins executed it."""
"""
Get selected text in a way that allows other plugins to execute it.
"""
ls = self.get_line_separator()

_indent = lambda line: len(line)-len(line.lstrip())
_indent = lambda line: len(line) - len(line.lstrip())

line_from, line_to = self.get_selection_bounds(cursor)
line_col_from, line_col_to = self.get_selection_start_end(cursor)
Expand All @@ -529,7 +530,7 @@ def get_selection_as_executable_code(self, cursor=None):
if len(lines) > 1:
# Multiline selection -> eventually fixing indentation
original_indent = _indent(self.get_text_line(line_from))
text = (" "*(original_indent-_indent(lines[0])))+text
text = (" " * (original_indent - _indent(lines[0]))) + text

# If there is a common indent to all lines, find it.
# Moving from bottom line to top line ensures that blank
Expand All @@ -538,7 +539,7 @@ def get_selection_as_executable_code(self, cursor=None):
min_indent = 999
current_indent = 0
lines = text.split(ls)
for i in range(len(lines)-1, -1, -1):
for i in range(len(lines) - 1, -1, -1):
line = lines[i]
if line.strip():
current_indent = _indent(line)
Expand Down Expand Up @@ -577,18 +578,22 @@ def get_cell_as_executable_code(self, cursor=None):
"""Return cell contents as executable code."""
if cursor is None:
cursor = self.textCursor()

ls = self.get_line_separator()
cursor, __ = self.select_current_cell(cursor)
line_from, __ = self.get_selection_bounds(cursor)

# Get the block for the first cell line
start = cursor.selectionStart()
block = self.document().findBlock(start)
if not is_cell_header(block) and start > 0:
block = self.document().findBlock(start - 1)

# Get text
text, off_pos, col_pos = self.get_selection_as_executable_code(cursor)
if text is not None:
text = ls * line_from + text

return text, block, off_pos, col_pos

def select_current_cell(self, cursor=None):
Expand Down
30 changes: 13 additions & 17 deletions spyder/plugins/editor/widgets/editorstack/editorstack.py
Original file line number Diff line number Diff line change
Expand Up @@ -2731,7 +2731,7 @@ def format_document_or_selection(self, index=None):

# ------ Run
def _get_lines_cursor(self, direction):
""" Select and return all lines from cursor in given direction"""
"""Select and return all lines from cursor in a given direction."""
editor = self.get_current_editor()
finfo = self.get_current_finfo()
enc = finfo.encoding
Expand Down Expand Up @@ -2766,28 +2766,20 @@ def get_from_current_line(self):
return self._get_lines_cursor(direction='down')

def get_selection(self):
"""
Get selected text or current line in console.

If some text is selected, then execute that text in console.

If no text is selected, then execute current line, unless current line
is empty. Then, advance cursor to next line. If cursor is on last line
and that line is not empty, then add a new blank line and move the
cursor there. If cursor is on last line and that line is empty, then do
not move cursor.
"""
"""Get selected text or current line in the editor."""
editor = self.get_current_editor()
encoding = self.get_current_finfo().encoding

# Get selection
selection = editor.get_selection_as_executable_code()
if selection:
text, off_pos, line_col_pos = selection
return text, off_pos, line_col_pos, encoding

# Get current line if no selection
line_col_from, line_col_to = editor.get_current_line_bounds()
line_off_from, line_off_to = editor.get_current_line_offsets()
line = editor.get_current_line()
text = line.lstrip()
text = editor.get_current_line()

return (
text, (line_off_from, line_off_to),
Expand All @@ -2807,17 +2799,21 @@ def advance_line(self):
editor.move_cursor_to_next('line', 'down')

def get_current_cell(self):
"""Get current cell attributes."""
"""Get current cell."""
text, block, off_pos, line_col_pos = (
self.get_current_editor().get_cell_as_executable_code())
encoding = self.get_current_finfo().encoding
name = cell_name(block)
return text, off_pos, line_col_pos, name, encoding

def advance_cell(self, reverse=False):
"""Advance to the next cell.
"""
Advance to the next cell.

reverse = True --> go to previous cell.
Parameters
----------
reverse: bool, optional
If True, go to the previous cell.
"""
if not reverse:
move_func = self.get_current_editor().go_to_next_cell
Expand Down
11 changes: 9 additions & 2 deletions spyder/plugins/ipythonconsole/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -817,9 +817,16 @@ def execute_code(self, lines, current_client=True, clear_variables=False):
clear_variables=clear_variables)

def run_selection(self, lines):
"""Execute selected lines in the current console."""
"""
Execute selected lines in the current console.

Parameters
----------
lines : str
Code lines to run.
"""
self.sig_unmaximize_plugin_requested.emit()
self.get_widget().execute_code(lines)
self.get_widget().execute_code(lines, check_line_by_line=True)

# ---- For working directory and path management
def set_current_client_working_directory(self, directory):
Expand Down
117 changes: 111 additions & 6 deletions spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import re
import shutil
import sys
from textwrap import dedent
from textwrap import dedent, indent

# Third party imports
from ipykernel._version import __version__ as ipykernel_version
Expand Down Expand Up @@ -2151,11 +2151,6 @@ def test_run_script(ipyconsole, qtbot, tmp_path):
not is_anaconda(), reason="Only works with Anaconda")
def test_show_spyder_kernels_error_on_restart(ipyconsole, qtbot):
"""Test that we show Spyder-kernels error message on restarts."""
# Wait until the window is fully up
shell = ipyconsole.get_current_shellwidget()
qtbot.waitUntil(
lambda: shell._prompt_html is not None, timeout=SHELL_TIMEOUT)

# Point to an interpreter without Spyder-kernels
ipyconsole.set_conf('default', False, section='main_interpreter')
pyexec = get_list_conda_envs()['conda: base'][0]
Expand Down Expand Up @@ -2187,5 +2182,115 @@ def test_show_spyder_kernels_error_on_restart(ipyconsole, qtbot):
assert not main_widget.show_time_action.isEnabled()


def test_line_by_line_execution(ipyconsole, qtbot):
"""Check that we can run multiline statements line by line."""
shell = ipyconsole.get_current_shellwidget()
control = shell._control

# Check that running code line by line works as expected
code = dedent("""
for i in range(2):
for j in range(2):
print(i, j)
print('foo')

""")

with qtbot.waitSignal(shell.executed):
for line in code.splitlines()[1:]:
ipyconsole.run_selection(line)

assert '0 1\nfoo' in control.toPlainText()

with qtbot.waitSignal(shell.sig_prompt_ready):
shell.clear_console()

# Check that running different complete statements executes them
# immediately and don't show the continuation prompt
with qtbot.waitSignal(shell.executed):
ipyconsole.run_selection('a = 10')

assert shell.get_value('a') == 10
assert '...' not in control.toPlainText()

with qtbot.waitSignal(shell.sig_prompt_ready):
shell.clear_console()

with qtbot.waitSignal(shell.executed):
ipyconsole.run_selection("print('foo')")

assert 'foo' == control.toPlainText().splitlines()[-3]
assert '...' not in control.toPlainText()

with qtbot.waitSignal(shell.sig_prompt_ready):
shell.clear_console()

with qtbot.waitSignal(shell.executed):
ipyconsole.run_selection("import math")

assert 'error' not in control.toPlainText().lower()
assert '...' not in control.toPlainText()

with qtbot.waitSignal(shell.sig_prompt_ready):
shell.clear_console()

# Check that running indented code works as expected
code1 = indent(code, ' ' * 4)

with qtbot.waitSignal(shell.executed):
for line in code1.splitlines()[1:]:
ipyconsole.run_selection(line)

assert '0 1\nfoo' in control.toPlainText()

with qtbot.waitSignal(shell.sig_prompt_ready):
shell.clear_console()

# Check that trying to run lines with less indentation than the initial
# block with which we started runs what's in the buffer.
code2 = dedent("""
for i in range(2):
print('foo')
for j in range(2):
if j > 0:
print(j)
else:
print('bar')
print('baz')
""")

with qtbot.waitSignal(shell.executed):
for line in code2.splitlines()[3:]:
ipyconsole.run_selection(line)

assert 'foo' not in control.toPlainText()
assert 'bar\n1' in control.toPlainText()
assert 'baz' not in control.toPlainText()

with qtbot.waitSignal(shell.sig_prompt_ready):
shell.clear_console()

# Check that we can execute complex multiline assignments
code3 = dedent("""
d = {
'a': {
'b': 1,
'c': 2
},
'd': {
'e': 3,
'f': 4
}
}

""")

with qtbot.waitSignal(shell.executed):
for line in code3.splitlines()[1:]:
ipyconsole.run_selection(line)

assert shell.get_value('d')


if __name__ == "__main__":
pytest.main()
13 changes: 10 additions & 3 deletions spyder/plugins/ipythonconsole/widgets/main_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -2157,16 +2157,16 @@ def update_active_project_path(self, active_project_path):

# ---- For execution
def execute_code(self, lines, current_client=True, clear_variables=False,
shellwidget=None):
shellwidget=None, check_line_by_line=False):
"""Execute code instructions."""
if current_client:
sw = self.get_current_shellwidget()
else:
sw = shellwidget

if sw is not None:
if not current_client:
# Clear console and reset namespace for
# dedicated clients.
# Clear console and reset namespace for dedicated clients.
# See spyder-ide/spyder#5748.
try:
sw.sig_prompt_ready.disconnect()
Expand All @@ -2177,6 +2177,13 @@ def execute_code(self, lines, current_client=True, clear_variables=False,
elif current_client and clear_variables:
sw.reset_namespace(warning=False)

# If the user is trying to execute code line by line, we need to
# call a special method to do it.
# Fixes spyder-ide/spyder#4431.
if check_line_by_line and len(lines.splitlines()) <= 1:
sw.execute_line_by_line(lines)
return

# Needed to handle an error when kernel_client is none.
# See spyder-ide/spyder#6308.
try:
Expand Down
Loading