Skip to content

Commit

Permalink
Merge branch 'TQDMQtConsole' of https://github.com/jsbautista/qtconsole
Browse files Browse the repository at this point in the history
… into TQDMQtConsole
  • Loading branch information
jsbautista committed Aug 20, 2024
2 parents dea62c7 + 530ba46 commit f8cb1a8
Show file tree
Hide file tree
Showing 10 changed files with 146 additions and 33 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/linux-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ jobs:
PYTHON_VERSION: ${{ matrix.PYTHON_VERSION }}
RUNNER_OS: 'ubuntu'
COVERALLS_REPO_TOKEN: XWVhJf2AsO7iouBLuCsh0pPhwHy81Uz1v
COVERALLS_SERVICE_NAME: 'github-actions'
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -76,7 +77,7 @@ jobs:
env:
QT_API: ${{ matrix.QT_LIB }}
PYTEST_QT_API: ${{ matrix.QT_LIB }}
- name: Upload coverage to Codecov
- name: Upload coverage to coveralls
if: matrix.PYTHON_VERSION == '3.8'
shell: bash -l {0}
run: coveralls
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ __pycache__
.coverage
.pytest_cache
.vscode
.spyproject
31 changes: 26 additions & 5 deletions qtconsole/ansi_code_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,31 @@ def _replace_special(self, match):
self.actions.append(ScrollAction('scroll', 'down', 'page', 1))
return ''

def _parse_ansi_color(self, color, intensity):
"""
Map an ANSI color code to color name or a RGB tuple.
Based on: https://gist.github.com/MightyPork/1d9bd3a3fd4eb1a661011560f6921b5b
"""
parsed_color = None
if color < 16:
# Adjust for intensity, if possible.
if intensity > 0 and color < 8:
color += 8
parsed_color = self.color_map.get(color, None)
elif (color > 231):
s = int((color - 232) * 10 + 8)
parsed_color = (s, s, s)
else:
n = color - 16
b = n % 6
g = (n - b) / 6 % 6
r = (n - b - g * 6) / 36 % 6
r = int(r * 40 + 55) if r else 0
g = int(g * 40 + 55) if g else 0
b = int(b * 40 + 55) if b else 0
parsed_color = (r, g, b)
return parsed_color


class QtAnsiCodeProcessor(AnsiCodeProcessor):
""" Translates ANSI escape codes into QTextCharFormats.
Expand Down Expand Up @@ -333,12 +358,8 @@ def get_color(self, color, intensity=0):
""" Returns a QColor for a given color code or rgb list, or None if one
cannot be constructed.
"""

if isinstance(color, int):
# Adjust for intensity, if possible.
if color < 8 and intensity > 0:
color += 8
constructor = self.color_map.get(color, None)
constructor = self._parse_ansi_color(color, intensity)
elif isinstance(color, (tuple, list)):
constructor = color
else:
Expand Down
82 changes: 59 additions & 23 deletions qtconsole/console_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,10 @@ def __init__(self, parent=None, **kw):
self._reading_callback = None
self._tab_width = 4

# Cursor position of where to insert text.
# Control characters allow this to move around on the current line.
self._insert_text_cursor = self._control.textCursor()

# List of strings pending to be appended as plain text in the widget.
# The text is not immediately inserted when available to not
# choke the Qt event loop with paint events for the widget in
Expand Down Expand Up @@ -695,6 +699,9 @@ def do_execute(self, source, complete, indent):
# effect when using a QTextEdit. I believe this is a Qt bug.
self._control.moveCursor(QtGui.QTextCursor.End)

# Advance where text is inserted
self._insert_text_cursor.movePosition(QtGui.QTextCursor.End)

def export_html(self):
""" Shows a dialog to export HTML/XML in various formats.
"""
Expand All @@ -712,6 +719,9 @@ def _finalize_input_request(self):
self._append_before_prompt_cursor.setPosition(
self._get_end_cursor().position())

self._insert_text_cursor.setPosition(
self._get_end_cursor().position())

# The maximum block count is only in effect during execution.
# This ensures that _prompt_pos does not become invalid due to
# text truncation.
Expand Down Expand Up @@ -841,12 +851,12 @@ def paste(self, mode=QtGui.QClipboard.Clipboard):

self._insert_plain_text_into_buffer(cursor, dedent(text))

def print_(self, printer = None):
def print_(self, printer=None):
""" Print the contents of the ConsoleWidget to the specified QPrinter.
"""
if (not printer):
if not printer:
printer = QtPrintSupport.QPrinter()
if(QtPrintSupport.QPrintDialog(printer).exec_() != QtPrintSupport.QPrintDialog.Accepted):
if QtPrintSupport.QPrintDialog(printer).exec_() != QtPrintSupport.QPrintDialog.Accepted:
return
self._control.print_(printer)

Expand Down Expand Up @@ -998,18 +1008,40 @@ def _append_custom(self, insert, input, before_prompt=False, *args, **kwargs):
current prompt, if there is one.
"""
# Determine where to insert the content.
cursor = self._control.textCursor()
cursor = self._insert_text_cursor
if before_prompt and (self._reading or not self._executing):
self._flush_pending_stream()
cursor._insert_mode=True
cursor.setPosition(self._append_before_prompt_pos)

# Jump to before prompt, if there is one
if cursor.position() >= self._append_before_prompt_pos \
and self._append_before_prompt_pos != self._get_end_pos():
cursor.setPosition(self._append_before_prompt_pos)

# If we appending on the same line as the prompt, use insert mode
# If so, the character at self._append_before_prompt_pos will not be a newline
cursor.movePosition(QtGui.QTextCursor.Right,
QtGui.QTextCursor.KeepAnchor)
if cursor.selection().toPlainText() != '\n':
cursor._insert_mode = True
cursor.movePosition(QtGui.QTextCursor.Left)
else:
# Insert at current printing point
# If cursor is before prompt jump to end, but only if there
# is a prompt (before_prompt_pos != end)
if cursor.position() <= self._append_before_prompt_pos \
and self._append_before_prompt_pos != self._get_end_pos():
cursor.movePosition(QtGui.QTextCursor.End)

if insert != self._insert_plain_text:
self._flush_pending_stream()
cursor.movePosition(QtGui.QTextCursor.End)

# Perform the insertion.
result = insert(cursor, input, *args, **kwargs)

# Remove insert mode tag
if hasattr(cursor, '_insert_mode'):
del cursor._insert_mode

return result

def _append_block(self, block_format=None, before_prompt=False):
Expand Down Expand Up @@ -1045,7 +1077,7 @@ def _clear_temporary_buffer(self):
# Select and remove all text below the input buffer.
cursor = self._get_prompt_cursor()
prompt = self._continuation_prompt.lstrip()
if(self._temp_buffer_filled):
if self._temp_buffer_filled:
self._temp_buffer_filled = False
while cursor.movePosition(QtGui.QTextCursor.NextBlock):
temp_cursor = QtGui.QTextCursor(cursor)
Expand Down Expand Up @@ -1657,24 +1689,23 @@ def _event_filter_page_keypress(self, event):
return False

def _on_flush_pending_stream_timer(self):
""" Flush the pending stream output and change the
prompt position appropriately.
""" Flush pending text into the widget on console timer trigger.
"""
cursor = self._control.textCursor()
cursor.movePosition(QtGui.QTextCursor.End)
self._flush_pending_stream()
cursor.movePosition(QtGui.QTextCursor.End)

def _flush_pending_stream(self):
""" Flush out pending text into the widget. """
""" Flush pending text into the widget. Only applies to text that is pending
when the console is in the running state. Text printed when console is
not running is shown immediately, and does not wait to be flushed.
"""
text = self._pending_insert_text
self._pending_insert_text = []
buffer_size = self._control.document().maximumBlockCount()
if buffer_size > 0:
text = self._get_last_lines_from_list(text, buffer_size)
text = ''.join(text)
t = time.time()
self._insert_plain_text(self._get_end_cursor(), text, flush=True)
self._insert_plain_text(self._insert_text_cursor, text, flush=True)
# Set the flush interval to equal the maximum time to update text.
self._pending_text_flush_interval.setInterval(
int(max(100, (time.time() - t) * 1000))
Expand Down Expand Up @@ -2093,12 +2124,12 @@ def _insert_plain_text(self, cursor, text, flush=False):

if (self._executing and not flush and
self._pending_text_flush_interval.isActive() and
cursor.position() == self._get_end_pos()):
cursor.position() == self._insert_text_cursor.position()):
# Queue the text to insert in case it is being inserted at end
self._pending_insert_text.append(text)
if buffer_size > 0:
self._pending_insert_text = self._get_last_lines_from_list(
self._pending_insert_text, buffer_size)
self._pending_insert_text, buffer_size)
return

if self._executing and not self._pending_text_flush_interval.isActive():
Expand All @@ -2123,7 +2154,7 @@ def _insert_plain_text(self, cursor, text, flush=False):
cursor.select(QtGui.QTextCursor.Document)
remove = True
if act.area == 'line':
if act.erase_to == 'all':
if act.erase_to == 'all':
cursor.select(QtGui.QTextCursor.LineUnderCursor)
remove = True
elif act.erase_to == 'start':
Expand All @@ -2137,7 +2168,7 @@ def _insert_plain_text(self, cursor, text, flush=False):
QtGui.QTextCursor.EndOfLine,
QtGui.QTextCursor.KeepAnchor)
remove = True
if remove:
if remove:
nspace=cursor.selectionEnd()-cursor.selectionStart() if fill else 0
cursor.removeSelectedText()
if nspace>0: cursor.insertText(' '*nspace) # replace text by space, to keep cursor position as specified
Expand Down Expand Up @@ -2191,15 +2222,17 @@ def _insert_plain_text(self, cursor, text, flush=False):
# simulate replacement mode
if substring is not None:
format = self._ansi_processor.get_format()
if not (hasattr(cursor,'_insert_mode') and cursor._insert_mode):

# Note that using _insert_mode means the \r ANSI sequence will not swallow characters.
if not (hasattr(cursor, '_insert_mode') and cursor._insert_mode):
pos = cursor.position()
cursor2 = QtGui.QTextCursor(cursor) # self._get_line_end_pos() is the previous line, don't use it
cursor2.movePosition(QtGui.QTextCursor.EndOfLine)
remain = cursor2.position() - pos # number of characters until end of line
n=len(substring)
swallow = min(n, remain) # number of character to swallow
cursor.setPosition(pos+swallow,QtGui.QTextCursor.KeepAnchor)
cursor.insertText(substring,format)
cursor.setPosition(pos + swallow, QtGui.QTextCursor.KeepAnchor)
cursor.insertText(substring, format)
else:
cursor.insertText(text)
cursor.endEditBlock()
Expand Down Expand Up @@ -2416,7 +2449,7 @@ def _readline(self, prompt='', callback=None, password=False):

self._reading = True
if password:
self._show_prompt('Warning: QtConsole does not support password mode, '\
self._show_prompt('Warning: QtConsole does not support password mode, '
'the text you type will be visible.', newline=True)

if 'ipdb' not in prompt.lower():
Expand Down Expand Up @@ -2548,6 +2581,9 @@ def _show_prompt(self, prompt=None, html=False, newline=True,
if move_forward:
self._append_before_prompt_cursor.setPosition(
self._append_before_prompt_cursor.position() + 1)
else:
# cursor position was 0, set before prompt cursor
self._append_before_prompt_cursor.setPosition(0)
self._prompt_started()

#------ Signal handlers ----------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion qtconsole/frontend_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -728,7 +728,7 @@ def restart_kernel(self, message, now=False):

def append_stream(self, text):
"""Appends text to the output stream."""
self._append_plain_text(text, before_prompt=True)
self._append_plain_text(text, before_prompt = True)

def flush_clearoutput(self):
"""If a clearoutput is pending, execute it."""
Expand Down
14 changes: 13 additions & 1 deletion qtconsole/mainwindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,7 @@ def set_syntax_style(self, syntax_style):
colors='nocolor'
elif styles.dark_style(syntax_style):
colors='linux'

else:
colors='lightbg'
self.active_frontend.syntax_style = syntax_style
Expand All @@ -809,7 +810,18 @@ def set_syntax_style(self, syntax_style):
self.active_frontend._syntax_style_changed()
self.active_frontend._style_sheet_changed()
self.active_frontend.reset(clear=True)
self.active_frontend._execute("%colors linux", True)
self.active_frontend._execute(
f"""
from IPython.core.ultratb import VerboseTB
if getattr(VerboseTB, 'tb_highlight_style', None) is not None:
VerboseTB.tb_highlight_style = '{syntax_style}'
elif getattr(VerboseTB, '_tb_highlight_style', None) is not None:
VerboseTB._tb_highlight_style = '{syntax_style}'
else:
get_ipython().run_line_magic('colors', '{colors}')
""",
True)


def close_active_frontend(self):
self.close_tab(self.active_frontend)
Expand Down
38 changes: 38 additions & 0 deletions qtconsole/tests/test_00_console_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,44 @@ def test_erase_in_line(self):
# clear all the text
cursor.insertText('')

def test_print_carriage_return(self):
""" Test that overwriting the current line works as intended,
before and after the cursor prompt.
"""
w = ConsoleWidget()

# Show a prompt
w._prompt = "prompt>"
w._prompt_sep = "\n"

w._show_prompt()
self.assert_text_equal(w._get_cursor(), '\u2029prompt>')

test_inputs = ['Hello\n', 'World\r',
'*' * 10, '\r',
'0', '1', '2', '3', '4',
'5', '6', '7', '8', '9',
'\r\n']

for text in test_inputs:
w._append_plain_text(text, before_prompt=True)
w._flush_pending_stream() # emulate text being flushed

self.assert_text_equal(w._get_cursor(),
"Hello\u20290123456789\u2029\u2029prompt>")

# Print after prompt
w._executing = True
test_inputs = ['\nF', 'o', 'o',
'\r', 'Bar', '\n']

for text in test_inputs:
w._append_plain_text(text, before_prompt=False)
w._flush_pending_stream() # emulate text being flushed

self.assert_text_equal(w._get_cursor(),
"Hello\u20290123456789\u2029\u2029prompt>\u2029Bar\u2029")

def test_link_handling(self):
noButton = QtCore.Qt.NoButton
noButtons = QtCore.Qt.NoButton
Expand Down
6 changes: 6 additions & 0 deletions qtconsole/tests/test_jupyter_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,15 @@ def test_stylesheet_changed(self):
# By default, the background is light. White text is rendered as black
self.assertEqual(w._ansi_processor.get_color(15).name(), '#000000')

# Color code 40
self.assertEqual(w._ansi_processor.get_color(40).name(), '#00d700')

# Change to a dark colorscheme. White text is rendered as white
w.syntax_style = 'monokai'
self.assertEqual(w._ansi_processor.get_color(15).name(), '#ffffff')

# Color code 40 with monokai
self.assertEqual(w._ansi_processor.get_color(40).name(), '#00d700')

@pytest.mark.skipif(not sys.platform.startswith('linux'),
reason="Works only on Linux")
Expand Down
1 change: 0 additions & 1 deletion requirements/environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ dependencies:
- jupyter_client
- pygments
- ipykernel
- pyzmq >=17.1

# For testing
- coveralls
Expand Down
1 change: 0 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@
'pygments',
'ipykernel>=4.1', # not a real dependency, but require the reference kernel
'qtpy>=2.4.0',
'pyzmq>=17.1',
'packaging'
],
extras_require = {
Expand Down

0 comments on commit f8cb1a8

Please sign in to comment.