123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610 |
- 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, '<a href="http://python.org">written in</a>')
- 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)]
|