import sys import unittest from flaky import flaky import pytest from qtpy import QtCore, QtGui, QtWidgets from qtpy.QtTest import QTest from qtconsole.console_widget import ConsoleWidget from qtconsole.qtconsoleapp import JupyterQtConsoleApp from . import no_display if sys.version[0] == '2': # Python 2 from IPython.core.inputsplitter import InputSplitter as TransformerManager else: from IPython.core.inputtransformer2 import TransformerManager SHELL_TIMEOUT = 20000 @pytest.fixture def qtconsole(qtbot): """Qtconsole fixture.""" # Create a console console = JupyterQtConsoleApp() console.initialize(argv=[]) qtbot.addWidget(console.window) console.window.confirm_exit = False console.window.show() return console @flaky(max_runs=3) @pytest.mark.parametrize( "debug", [True, False]) def test_scroll(qtconsole, qtbot, debug): """ Make sure the scrolling works. """ window = qtconsole.window shell = window.active_frontend control = shell._control scroll_bar = control.verticalScrollBar() # Wait until the console is fully up qtbot.waitUntil(lambda: shell._prompt_html is not None, timeout=SHELL_TIMEOUT) assert scroll_bar.value() == 0 # Define a function with loads of output # Check the outputs are working as well code = ["import time", "def print_numbers():", " for i in range(1000):", " print(i)", " time.sleep(.01)"] for line in code: qtbot.keyClicks(control, line) qtbot.keyClick(control, QtCore.Qt.Key_Enter) with qtbot.waitSignal(shell.executed): qtbot.keyClick(control, QtCore.Qt.Key_Enter, modifier=QtCore.Qt.ShiftModifier) def run_line(line, block=True): qtbot.keyClicks(control, line) if block: with qtbot.waitSignal(shell.executed): qtbot.keyClick(control, QtCore.Qt.Key_Enter, modifier=QtCore.Qt.ShiftModifier) else: qtbot.keyClick(control, QtCore.Qt.Key_Enter, modifier=QtCore.Qt.ShiftModifier) if debug: # Enter debug run_line('%debug print()', block=False) qtbot.keyClick(control, QtCore.Qt.Key_Enter) # redefine run_line def run_line(line, block=True): qtbot.keyClicks(control, '!' + line) qtbot.keyClick(control, QtCore.Qt.Key_Enter, modifier=QtCore.Qt.ShiftModifier) if block: qtbot.waitUntil( lambda: control.toPlainText().strip( ).split()[-1] == "ipdb>") prev_position = scroll_bar.value() # Create a bunch of inputs for i in range(20): run_line('a = 1') assert scroll_bar.value() > prev_position # Put the scroll bar higher and check it doesn't move prev_position = scroll_bar.value() + scroll_bar.pageStep() // 2 scroll_bar.setValue(prev_position) for i in range(2): run_line('a') assert scroll_bar.value() == prev_position # add more input and check it moved for i in range(10): run_line('a') assert scroll_bar.value() > prev_position prev_position = scroll_bar.value() # Run the printing function run_line('print_numbers()', block=False) qtbot.wait(1000) # Check everything advances assert scroll_bar.value() > prev_position # move up prev_position = scroll_bar.value() - scroll_bar.pageStep() scroll_bar.setValue(prev_position) qtbot.wait(1000) # Check position stayed the same assert scroll_bar.value() == prev_position # reset position prev_position = scroll_bar.maximum() - (scroll_bar.pageStep() * 8) // 10 scroll_bar.setValue(prev_position) qtbot.wait(1000) assert scroll_bar.value() > prev_position @flaky(max_runs=3) def test_input(qtconsole, qtbot): """ Test input function """ window = qtconsole.window shell = window.active_frontend control = shell._control # Wait until the console is fully up qtbot.waitUntil(lambda: shell._prompt_html is not None, timeout=SHELL_TIMEOUT) with qtbot.waitSignal(shell.executed): shell.execute("import time") if sys.version[0] == '2': input_function = 'raw_input' else: input_function = 'input' shell.execute("print(" + input_function + "('name: ')); time.sleep(3)") qtbot.waitUntil(lambda: control.toPlainText().split()[-1] == 'name:') qtbot.keyClicks(control, 'test') qtbot.keyClick(control, QtCore.Qt.Key_Enter) qtbot.waitUntil(lambda: not shell._reading) qtbot.keyClick(control, 'z', modifier=QtCore.Qt.ControlModifier) for i in range(10): qtbot.keyClick(control, QtCore.Qt.Key_Backspace) qtbot.waitUntil(lambda: shell._prompt_html is not None, timeout=SHELL_TIMEOUT) assert 'name: test\ntest' in control.toPlainText() @flaky(max_runs=3) def test_debug(qtconsole, qtbot): """ Make sure the cursor works while debugging It might not because the console is "_executing" """ window = qtconsole.window shell = window.active_frontend control = shell._control # Wait until the console is fully up qtbot.waitUntil(lambda: shell._prompt_html is not None, timeout=SHELL_TIMEOUT) # Enter execution code = "%debug range(1)" qtbot.keyClicks(control, code) qtbot.keyClick(control, QtCore.Qt.Key_Enter, modifier=QtCore.Qt.ShiftModifier) qtbot.waitUntil( lambda: control.toPlainText().strip().split()[-1] == "ipdb>", timeout=SHELL_TIMEOUT) # We should be able to move the cursor while debugging qtbot.keyClicks(control, "abd") qtbot.wait(100) qtbot.keyClick(control, QtCore.Qt.Key_Left) qtbot.keyClick(control, 'c') qtbot.wait(100) assert control.toPlainText().strip().split()[-1] == "abcd" @pytest.mark.skipif(no_display, reason="Doesn't work without a display") class TestConsoleWidget(unittest.TestCase): @classmethod def setUpClass(cls): """ Create the application for the test case. """ cls._app = QtWidgets.QApplication.instance() if cls._app is None: cls._app = QtWidgets.QApplication([]) cls._app.setQuitOnLastWindowClosed(False) @classmethod def tearDownClass(cls): """ Exit the application. """ QtWidgets.QApplication.quit() def assert_text_equal(self, cursor, text): cursor.select(cursor.Document) selection = cursor.selectedText() self.assertEqual(selection, text) def test_special_characters(self): """ Are special characters displayed correctly? """ w = ConsoleWidget() cursor = w._get_prompt_cursor() test_inputs = ['xyz\b\b=\n', 'foo\b\nbar\n', 'foo\b\nbar\r\n', 'abc\rxyz\b\b='] expected_outputs = [u'x=z\u2029', u'foo\u2029bar\u2029', u'foo\u2029bar\u2029', 'x=z'] for i, text in enumerate(test_inputs): w._insert_plain_text(cursor, text) self.assert_text_equal(cursor, expected_outputs[i]) # clear all the text cursor.insertText('') def test_link_handling(self): noKeys = QtCore.Qt noButton = QtCore.Qt.MouseButton(0) noButtons = QtCore.Qt.MouseButtons(0) noModifiers = QtCore.Qt.KeyboardModifiers(0) MouseMove = QtCore.QEvent.MouseMove QMouseEvent = QtGui.QMouseEvent w = ConsoleWidget() cursor = w._get_prompt_cursor() w._insert_html(cursor, 'written in') obj = w._control tip = QtWidgets.QToolTip self.assertEqual(tip.text(), u'') # should be somewhere else elsewhereEvent = QMouseEvent(MouseMove, QtCore.QPoint(50,50), noButton, noButtons, noModifiers) w.eventFilter(obj, elsewhereEvent) self.assertEqual(tip.isVisible(), False) self.assertEqual(tip.text(), u'') # should be over text overTextEvent = QMouseEvent(MouseMove, QtCore.QPoint(1,5), noButton, noButtons, noModifiers) w.eventFilter(obj, overTextEvent) self.assertEqual(tip.isVisible(), True) self.assertEqual(tip.text(), "http://python.org") # should still be over text stillOverTextEvent = QMouseEvent(MouseMove, QtCore.QPoint(1,5), noButton, noButtons, noModifiers) w.eventFilter(obj, stillOverTextEvent) self.assertEqual(tip.isVisible(), True) self.assertEqual(tip.text(), "http://python.org") def test_width_height(self): # width()/height() QWidget properties should not be overridden. w = ConsoleWidget() self.assertEqual(w.width(), QtWidgets.QWidget.width(w)) self.assertEqual(w.height(), QtWidgets.QWidget.height(w)) def test_prompt_cursors(self): """Test the cursors that keep track of where the prompt begins and ends""" w = ConsoleWidget() w._prompt = 'prompt>' doc = w._control.document() # Fill up the QTextEdit area with the maximum number of blocks doc.setMaximumBlockCount(10) for _ in range(9): w._append_plain_text('line\n') # Draw the prompt, this should cause the first lines to be deleted w._show_prompt() self.assertEqual(doc.blockCount(), 10) # _prompt_pos should be at the end of the document self.assertEqual(w._prompt_pos, w._get_end_pos()) # _append_before_prompt_pos should be at the beginning of the prompt self.assertEqual(w._append_before_prompt_pos, w._prompt_pos - len(w._prompt)) # insert some more text without drawing a new prompt w._append_plain_text('line\n') self.assertEqual(w._prompt_pos, w._get_end_pos() - len('line\n')) self.assertEqual(w._append_before_prompt_pos, w._prompt_pos - len(w._prompt)) # redraw the prompt w._show_prompt() self.assertEqual(w._prompt_pos, w._get_end_pos()) self.assertEqual(w._append_before_prompt_pos, w._prompt_pos - len(w._prompt)) # insert some text before the prompt w._append_plain_text('line', before_prompt=True) self.assertEqual(w._prompt_pos, w._get_end_pos()) self.assertEqual(w._append_before_prompt_pos, w._prompt_pos - len(w._prompt)) def test_select_all(self): w = ConsoleWidget() w._append_plain_text('Header\n') w._prompt = 'prompt>' w._show_prompt() control = w._control app = QtWidgets.QApplication.instance() cursor = w._get_cursor() w._insert_plain_text_into_buffer(cursor, "if:\n pass") cursor.clearSelection() control.setTextCursor(cursor) # "select all" action selects cell first w.select_all_smart() QTest.keyClick(control, QtCore.Qt.Key_C, QtCore.Qt.ControlModifier) copied = app.clipboard().text() self.assertEqual(copied, 'if:\n> pass') # # "select all" action triggered a second time selects whole document w.select_all_smart() QTest.keyClick(control, QtCore.Qt.Key_C, QtCore.Qt.ControlModifier) copied = app.clipboard().text() self.assertEqual(copied, 'Header\nprompt>if:\n> pass') def test_keypresses(self): """Test the event handling code for keypresses.""" w = ConsoleWidget() w._append_plain_text('Header\n') w._prompt = 'prompt>' w._show_prompt() app = QtWidgets.QApplication.instance() control = w._control # Test setting the input buffer w._set_input_buffer('test input') self.assertEqual(w._get_input_buffer(), 'test input') # Ctrl+K kills input until EOL w._set_input_buffer('test input') c = control.textCursor() c.setPosition(c.position() - 3) control.setTextCursor(c) QTest.keyClick(control, QtCore.Qt.Key_K, QtCore.Qt.ControlModifier) self.assertEqual(w._get_input_buffer(), 'test in') # Ctrl+V pastes w._set_input_buffer('test input ') app.clipboard().setText('pasted text') QTest.keyClick(control, QtCore.Qt.Key_V, QtCore.Qt.ControlModifier) self.assertEqual(w._get_input_buffer(), 'test input pasted text') self.assertEqual(control.document().blockCount(), 2) # Paste should strip indentation w._set_input_buffer('test input ') app.clipboard().setText(' pasted text') QTest.keyClick(control, QtCore.Qt.Key_V, QtCore.Qt.ControlModifier) self.assertEqual(w._get_input_buffer(), 'test input pasted text') self.assertEqual(control.document().blockCount(), 2) # Multiline paste, should also show continuation marks w._set_input_buffer('test input ') app.clipboard().setText('line1\nline2\nline3') QTest.keyClick(control, QtCore.Qt.Key_V, QtCore.Qt.ControlModifier) self.assertEqual(w._get_input_buffer(), 'test input line1\nline2\nline3') self.assertEqual(control.document().blockCount(), 4) self.assertEqual(control.document().findBlockByNumber(1).text(), 'prompt>test input line1') self.assertEqual(control.document().findBlockByNumber(2).text(), '> line2') self.assertEqual(control.document().findBlockByNumber(3).text(), '> line3') # Multiline paste should strip indentation intelligently # in the case where pasted text has leading whitespace on first line # and we're pasting into indented position w._set_input_buffer(' ') app.clipboard().setText(' If 1:\n pass') QTest.keyClick(control, QtCore.Qt.Key_V, QtCore.Qt.ControlModifier) self.assertEqual(w._get_input_buffer(), ' If 1:\n pass') # Ctrl+Backspace should intelligently remove the last word w._set_input_buffer("foo = ['foo', 'foo', 'foo', \n" " 'bar', 'bar', 'bar']") QTest.keyClick(control, QtCore.Qt.Key_Backspace, QtCore.Qt.ControlModifier) self.assertEqual(w._get_input_buffer(), ("foo = ['foo', 'foo', 'foo', \n" " 'bar', 'bar', '")) QTest.keyClick(control, QtCore.Qt.Key_Backspace, QtCore.Qt.ControlModifier) QTest.keyClick(control, QtCore.Qt.Key_Backspace, QtCore.Qt.ControlModifier) self.assertEqual(w._get_input_buffer(), ("foo = ['foo', 'foo', 'foo', \n" " '")) QTest.keyClick(control, QtCore.Qt.Key_Backspace, QtCore.Qt.ControlModifier) self.assertEqual(w._get_input_buffer(), ("foo = ['foo', 'foo', 'foo', \n" "")) QTest.keyClick(control, QtCore.Qt.Key_Backspace, QtCore.Qt.ControlModifier) self.assertEqual(w._get_input_buffer(), "foo = ['foo', 'foo', 'foo',") # Ctrl+Delete should intelligently remove the next word w._set_input_buffer("foo = ['foo', 'foo', 'foo', \n" " 'bar', 'bar', 'bar']") c = control.textCursor() c.setPosition(35) control.setTextCursor(c) QTest.keyClick(control, QtCore.Qt.Key_Delete, QtCore.Qt.ControlModifier) self.assertEqual(w._get_input_buffer(), ("foo = ['foo', 'foo', ', \n" " 'bar', 'bar', 'bar']")) QTest.keyClick(control, QtCore.Qt.Key_Delete, QtCore.Qt.ControlModifier) self.assertEqual(w._get_input_buffer(), ("foo = ['foo', 'foo', \n" " 'bar', 'bar', 'bar']")) QTest.keyClick(control, QtCore.Qt.Key_Delete, QtCore.Qt.ControlModifier) self.assertEqual(w._get_input_buffer(), "foo = ['foo', 'foo', 'bar', 'bar', 'bar']") w._set_input_buffer("foo = ['foo', 'foo', 'foo', \n" " 'bar', 'bar', 'bar']") c = control.textCursor() c.setPosition(48) control.setTextCursor(c) QTest.keyClick(control, QtCore.Qt.Key_Delete, QtCore.Qt.ControlModifier) self.assertEqual(w._get_input_buffer(), ("foo = ['foo', 'foo', 'foo', \n" "'bar', 'bar', 'bar']")) # Left and right keys should respect the continuation prompt w._set_input_buffer("line 1\n" "line 2\n" "line 3") c = control.textCursor() c.setPosition(20) # End of line 1 control.setTextCursor(c) QTest.keyClick(control, QtCore.Qt.Key_Right) # Cursor should have moved after the continuation prompt self.assertEqual(control.textCursor().position(), 23) QTest.keyClick(control, QtCore.Qt.Key_Left) # Cursor should have moved to the end of the previous line self.assertEqual(control.textCursor().position(), 20) # TODO: many more keybindings def test_indent(self): """Test the event handling code for indent/dedent keypresses .""" w = ConsoleWidget() w._append_plain_text('Header\n') w._prompt = 'prompt>' w._show_prompt() control = w._control # TAB with multiline selection should block-indent w._set_input_buffer("") c = control.textCursor() pos=c.position() w._set_input_buffer("If 1:\n pass") c.setPosition(pos, QtGui.QTextCursor.KeepAnchor) control.setTextCursor(c) QTest.keyClick(control, QtCore.Qt.Key_Tab) self.assertEqual(w._get_input_buffer()," If 1:\n pass") # TAB with multiline selection, should block-indent to next multiple # of 4 spaces, if first line has 0 < indent < 4 w._set_input_buffer("") c = control.textCursor() pos=c.position() w._set_input_buffer(" If 2:\n pass") c.setPosition(pos, QtGui.QTextCursor.KeepAnchor) control.setTextCursor(c) QTest.keyClick(control, QtCore.Qt.Key_Tab) self.assertEqual(w._get_input_buffer()," If 2:\n pass") # Shift-TAB with multiline selection should block-dedent w._set_input_buffer("") c = control.textCursor() pos=c.position() w._set_input_buffer(" If 3:\n pass") c.setPosition(pos, QtGui.QTextCursor.KeepAnchor) control.setTextCursor(c) QTest.keyClick(control, QtCore.Qt.Key_Backtab) self.assertEqual(w._get_input_buffer(),"If 3:\n pass") def test_complete(self): class TestKernelClient(object): def is_complete(self, source): calls.append(source) return msg_id w = ConsoleWidget() cursor = w._get_prompt_cursor() w._execute = lambda *args: calls.append(args) w.kernel_client = TestKernelClient() msg_id = object() calls = [] # test incomplete statement (no _execute called, but indent added) w.execute("thing", interactive=True) self.assertEqual(calls, ["thing"]) calls = [] w._handle_is_complete_reply( dict(parent_header=dict(msg_id=msg_id), content=dict(status="incomplete", indent="!!!"))) self.assert_text_equal(cursor, u"thing\u2029> !!!") self.assertEqual(calls, []) # test complete statement (_execute called) msg_id = object() w.execute("else", interactive=True) self.assertEqual(calls, ["else"]) calls = [] w._handle_is_complete_reply( dict(parent_header=dict(msg_id=msg_id), content=dict(status="complete", indent="###"))) self.assertEqual(calls, [("else", False)]) calls = [] self.assert_text_equal(cursor, u"thing\u2029> !!!else\u2029") # test missing answer from is_complete msg_id = object() w.execute("done", interactive=True) self.assertEqual(calls, ["done"]) calls = [] self.assert_text_equal(cursor, u"thing\u2029> !!!else\u2029") w._trigger_is_complete_callback() self.assert_text_equal(cursor, u"thing\u2029> !!!else\u2029\u2029> ") # assert that late answer isn't destroying anything w._handle_is_complete_reply( dict(parent_header=dict(msg_id=msg_id), content=dict(status="complete", indent="###"))) self.assertEqual(calls, []) def test_complete_python(self): """Test that is_complete is working correctly for Python.""" # Kernel client to test the responses of is_complete class TestIPyKernelClient(object): def is_complete(self, source): tm = TransformerManager() check_complete = tm.check_complete(source) responses.append(check_complete) # Initialize widget responses = [] w = ConsoleWidget() w._append_plain_text('Header\n') w._prompt = 'prompt>' w._show_prompt() w.kernel_client = TestIPyKernelClient() # Execute incomplete statement inside a block code = '\n'.join(["if True:", " a = 1"]) w._set_input_buffer(code) w.execute(interactive=True) assert responses == [('incomplete', 4)] # Execute complete statement inside a block responses = [] code = '\n'.join(["if True:", " a = 1\n\n"]) w._set_input_buffer(code) w.execute(interactive=True) assert responses == [('complete', None)]