pax_global_header00006660000000000000000000000064141600413330014505gustar00rootroot0000000000000052 comment=e566b58486c9ba344ecdc4d2af09a89147603edf pyqtconsole-1.2.2/000077500000000000000000000000001416004133300140675ustar00rootroot00000000000000pyqtconsole-1.2.2/.github/000077500000000000000000000000001416004133300154275ustar00rootroot00000000000000pyqtconsole-1.2.2/.github/workflows/000077500000000000000000000000001416004133300174645ustar00rootroot00000000000000pyqtconsole-1.2.2/.github/workflows/test.yml000066400000000000000000000021571416004133300211730ustar00rootroot00000000000000name: Tests on: push: pull_request: jobs: test: strategy: fail-fast: false matrix: python: - "3.7" - "3.8" - "3.9" - "3.10" os: - macos-latest - ubuntu-latest - windows-latest runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 with: python-version: ${{ matrix.python }} - run: pip install build - run: python -m build . - run: pip install dist/*.whl shell: bash - run: pip install pytest - run: pytest - uses: actions/upload-artifact@v2 with: name: dist ${{ matrix.os }} ${{ matrix.python }} path: dist/ deploy: needs: test if: startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v2 with: name: dist ubuntu-latest 3.10 path: dist/ - uses: pypa/gh-action-pypi-publish@master with: user: __token__ password: ${{ secrets.PYPI_TOKEN }} pyqtconsole-1.2.2/.gitignore000066400000000000000000000000761416004133300160620ustar00rootroot00000000000000*.py[cod] .svn* *~ MANIFEST *.egg *.egg-info .eggs dist build pyqtconsole-1.2.2/CHANGES.rst000066400000000000000000000025621416004133300156760ustar00rootroot00000000000000Changelog ~~~~~~~~~ v1.2.2 ------ Date: 18.10.2021 - fixed PyQt warning because of explicit integer type - fixed jedi autocomplete because of method rename v1.2.1 ------ Date: 17.03.2020 - fix accepting input with AltGr modifier on win10 (#53) v1.2.0 ------ Date: 17.03.2020 - add PySide2 compatibility - add Ctrl-U shortcut to clear the input buffer - use standard QtPy package to provide the compatibility layer - hide the cursor during the execution of a python command - mimic shell behaviour when using up and down key to go to end of history - fix crash when closing the interpreter window of the threaded example - disable excepthook on displaying exception - write '\n' before syntax errors for consistency Thanks to @roberthdevries and @irgolic for their contributions! v1.1.5 ------ Date: 25.11.2019 - fix TypeError in highlighter when called without formats v1.1.4 ------ Date: 21.11.2019 - fix AttributeError due to QueuedConnection on PyQt<5.11 (#23) - fix exception on import when started within spyder (#26) - fix gevent example to incorporate interoperability code for gevent/Qt (#28) - fix not waiting for empty line when entering code blocks before applying input (#30) - fix TypeError during compilation step on python 3.8 - allow user to override syntax highlighting color preferences (#29) note that this is provisional API - automate release process (#34) pyqtconsole-1.2.2/LICENSE000066400000000000000000000020611416004133300150730ustar00rootroot00000000000000MIT License Copyright (c) 2015 Marcus Oscarsson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pyqtconsole-1.2.2/MANIFEST.in000066400000000000000000000000401416004133300156170ustar00rootroot00000000000000recursive-include examples *.py pyqtconsole-1.2.2/README.rst000066400000000000000000000104061416004133300155570ustar00rootroot00000000000000pyqtconsole =========== |Version| |Python| |License| |Tests| pyqtconsole is a lightweight python console for Qt applications. It's made to be easy to embed in other Qt applications and comes with some examples that show how this can be done. The interpreter can run in a separate thread, in the UI main thread or in a gevent task. Installing ~~~~~~~~~~ Simply type:: pip install pyqtconsole Or to install a development version from local checkout, type:: pip install -e . Simple usage ~~~~~~~~~~~~ The following snippet shows how to create a console that will execute user input in a separate thread. Be aware that long running tasks will still block the main thread due to the GIL. See the ``examples`` directory for more examples. .. code-block:: python import sys from threading import Thread from PyQt5.QtWidgets import QApplication from pyqtconsole.console import PythonConsole app = QApplication([]) console = PythonConsole() console.show() console.eval_in_thread() sys.exit(app.exec_()) Embedding ~~~~~~~~~ * *Separate thread* - Runs the interpreter in a separate thread, see the example threaded.py_. Running the interpreter in a separate thread obviously limits the interaction with the Qt application. The parts of Qt that needs to be called from the main thread will not work properly, but is excellent way for having a 'plain' python console in your Qt app. * *main thread* - Runs the interpreter in the main thread, see the example inuithread.py_. Makes full interaction with Qt possible, lenghty operations will of course freeze the UI (as any lenghty operation that is called from the main thread). This is a great alternative for people who does not want to use the gevent based approach but still wants full interactivity with Qt. * *gevent* - Runs the interpreter in a gevent task, see the example `_gevent.py`_. Allows for full interactivity with Qt without special consideration (at least to some extent) for longer running processes. The best method if you want to use pyQtgraph, Matplotlib, PyMca or similar. Customizing syntax highlighting ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The coloring of the syntax highlighting can be customized by passing a ``formats`` dictionary to the ``PythonConsole`` constructer. This dictionary must be shaped as follows: .. code-block:: python import pyqtconsole.highlighter as hl console = PythonConsole(formats={ 'keyword': hl.format('blue', 'bold'), 'operator': hl.format('red'), 'brace': hl.format('darkGray'), 'defclass': hl.format('black', 'bold'), 'string': hl.format('magenta'), 'string2': hl.format('darkMagenta'), 'comment': hl.format('darkGreen', 'italic'), 'self': hl.format('black', 'italic'), 'numbers': hl.format('brown'), 'inprompt': hl.format('darkBlue', 'bold'), 'outprompt': hl.format('darkRed', 'bold'), }) All keys are optional and default to the value shown above if left unspecified. Credits ~~~~~~~ This module depends on QtPy which provides a compatibility layer for Qt4 and Qt5. The console is tested under both Qt4 and Qt5. .. _threaded.py: https://github.com/marcus-oscarsson/pyqtconsole/blob/master/examples/threaded.py .. _inuithread.py: https://github.com/marcus-oscarsson/pyqtconsole/blob/master/examples/inuithread.py .. _`_gevent.py`: https://github.com/marcus-oscarsson/pyqtconsole/blob/master/examples/_gevent.py .. _QtPy: https://github.com/spyder-ide/qtpy .. Badges: .. |Version| image:: https://img.shields.io/pypi/v/pyqtconsole.svg :target: https://pypi.org/project/pyqtconsole :alt: Latest Version .. |Python| image:: https://img.shields.io/pypi/pyversions/pyqtconsole.svg :target: https://pypi.org/project/pyqtconsole#files :alt: Python versions .. |License| image:: https://img.shields.io/pypi/l/pyqtconsole.svg :target: https://github.com/marcus-oscarsson/pyqtconsole/blob/master/LICENSE :alt: License: MIT .. |Tests| image:: https://github.com/pyqtconsole/pyqtconsole/workflows/Tests/badge.svg :target: https://github.com/pyqtconsole/pyqtconsole/actions?query=Tests :alt: Test status pyqtconsole-1.2.2/examples/000077500000000000000000000000001416004133300157055ustar00rootroot00000000000000pyqtconsole-1.2.2/examples/_gevent.py000066400000000000000000000024741416004133300177150ustar00rootroot00000000000000#! /usr/bin/env python # -*- coding: utf-8 -*- from gevent import monkey; monkey.patch_all() # noqa import gevent import sys from qtpy.QtCore import QTimer from qtpy.QtWidgets import QApplication from pyqtconsole.console import PythonConsole def greet(): print("hello world") class GEventProcessing: """Interoperability class between Qt/gevent that allows processing gevent tasks during Qt idle periods.""" def __init__(self, idle_period=0.010): # Limit the IDLE handler's frequency while still allow for gevent # to trigger a microthread anytime self._idle_period = idle_period # IDLE timer: on_idle is called whenever no Qt events left for # processing self._timer = QTimer() self._timer.timeout.connect(self.process_events) self._timer.start(0) def __enter__(self): pass def __exit__(self, *exc_info): self._timer.stop() def process_events(self): # Cooperative yield, allow gevent to monitor file handles via libevent gevent.sleep(self._idle_period) if __name__ == '__main__': app = QApplication([]) console = PythonConsole() console.push_local_ns('greet', greet) console.show() console.eval_executor(gevent.spawn) with GEventProcessing(): sys.exit(app.exec_()) pyqtconsole-1.2.2/examples/inuithread.py000066400000000000000000000007541416004133300204210ustar00rootroot00000000000000#! /usr/bin/env python # -*- coding: utf-8 -*- import sys from qtpy.QtWidgets import QApplication from pyqtconsole.console import PythonConsole from pyqtconsole.highlighter import format def greet(): print("hello world") if __name__ == '__main__': app = QApplication([]) console = PythonConsole(formats={ 'keyword': format('darkBlue', 'bold') }) console.push_local_ns('greet', greet) console.show() console.eval_queued() sys.exit(app.exec_()) pyqtconsole-1.2.2/examples/threaded.py000066400000000000000000000006051416004133300200400ustar00rootroot00000000000000#! /usr/bin/env python # -*- coding: utf-8 -*- import sys from qtpy.QtWidgets import QApplication from pyqtconsole.console import PythonConsole def greet(): print("hello world") if __name__ == '__main__': app = QApplication([]) console = PythonConsole() console.push_local_ns('greet', greet) console.show() console.eval_in_thread() sys.exit(app.exec_()) pyqtconsole-1.2.2/pyqtconsole/000077500000000000000000000000001416004133300164475ustar00rootroot00000000000000pyqtconsole-1.2.2/pyqtconsole/__init__.py000066400000000000000000000004211416004133300205550ustar00rootroot00000000000000# -*- coding: utf-8 -*- __version__ = '1.2.2' __description__ = \ 'Lightweight python console, easy to embed into Qt applications' __author__ = 'Marcus Oskarsson' __author_email__ = 'marcus.oscarsson@esrf.fr' __url__ = 'https://github.com/marcus-oscarsson/pyqtconsole' pyqtconsole-1.2.2/pyqtconsole/autocomplete.py000066400000000000000000000134371416004133300215320ustar00rootroot00000000000000# -*- coding: utf-8 -*- from qtpy.QtCore import Qt, QObject, QEvent from qtpy.QtWidgets import QCompleter from .text import columnize, long_substr class COMPLETE_MODE(object): DROPDOWN = 1 INLINE = 2 class AutoComplete(QObject): def __init__(self, parent): super(AutoComplete, self).__init__(parent) self.mode = COMPLETE_MODE.INLINE self.completer = None self._last_key = None parent.edit.installEventFilter(self) self.init_completion_list([]) def eventFilter(self, widget, event): if event.type() == QEvent.KeyPress: return bool(self.key_pressed_handler(event)) return False def key_pressed_handler(self, event): intercepted = False key = event.key() if key == Qt.Key_Tab: intercepted = self.handle_tab_key(event) elif key in (Qt.Key_Return, Qt.Key_Enter, Qt.Key_Space): intercepted = self.handle_complete_key(event) elif key == Qt.Key_Escape: intercepted = self.hide_completion_suggestions() self._last_key = key self.update_completion(key) return intercepted def handle_tab_key(self, event): if self.parent()._textCursor().hasSelection(): return False if self.mode == COMPLETE_MODE.DROPDOWN: if self.parent().input_buffer().strip(): if self.completing(): self.complete() else: self.trigger_complete() event.accept() return True elif self.mode == COMPLETE_MODE.INLINE: if self._last_key == Qt.Key_Tab: self.trigger_complete() event.accept() return True def handle_complete_key(self, event): if self.completing(): self.complete() event.accept() return True def init_completion_list(self, words): self.completer = QCompleter(words, self) self.completer.setCompletionPrefix(self.parent().input_buffer()) self.completer.setWidget(self.parent().edit) self.completer.setCaseSensitivity(Qt.CaseSensitive) self.completer.setModelSorting(QCompleter.CaseSensitivelySortedModel) if self.mode == COMPLETE_MODE.DROPDOWN: self.completer.setCompletionMode(QCompleter.PopupCompletion) self.completer.activated.connect(self.insert_completion) else: self.completer.setCompletionMode(QCompleter.InlineCompletion) def trigger_complete(self): _buffer = self.parent().input_buffer().strip() self.show_completion_suggestions(_buffer) def show_completion_suggestions(self, _buffer): words = self.parent().get_completions(_buffer) # No words to show, just return if len(words) == 0: return # Close any popups before creating a new one if self.completer.popup(): self.completer.popup().close() self.init_completion_list(words) leastcmn = long_substr(words) self.insert_completion(leastcmn) # If only one word to complete, just return and don't display options if len(words) == 1: return if self.mode == COMPLETE_MODE.DROPDOWN: cr = self.parent().edit.cursorRect() sbar_w = self.completer.popup().verticalScrollBar() popup_width = self.completer.popup().sizeHintForColumn(0) popup_width += sbar_w.sizeHint().width() cr.setWidth(popup_width) self.completer.complete(cr) elif self.mode == COMPLETE_MODE.INLINE: cl = columnize(words, colsep=' | ') self.parent()._insert_output_text( '\n\n' + cl + '\n', lf=True, keep_buffer=True) def hide_completion_suggestions(self): if self.completing(): self.completer.popup().close() return True def completing(self): if self.mode == COMPLETE_MODE.DROPDOWN: return (self.completer.popup() and self.completer.popup().isVisible()) else: return False def insert_completion(self, completion): _buffer = self.parent().input_buffer().strip() # Handling the . operator in object oriented languages so we don't # overwrite the . when we are inserting the completion. Its not the . # operator If the buffer starts with a . (dot), but something else # perhaps terminal specific so do nothing. if '.' in _buffer and _buffer[0] != '.': idx = _buffer.rfind('.') + 1 _buffer = _buffer[idx:] if self.mode == COMPLETE_MODE.DROPDOWN: self.parent().insert_input_text(completion[len(_buffer):]) elif self.mode == COMPLETE_MODE.INLINE: self.parent().clear_input_buffer() self.parent().insert_input_text(completion) words = self.parent().get_completions(completion) if len(words) == 1: self.parent().insert_input_text(' ') def update_completion(self, key): if self.completing(): _buffer = self.parent().input_buffer() if len(_buffer) > 1: self.show_completion_suggestions(_buffer) self.completer.setCurrentRow(0) model = self.completer.completionModel() self.completer.popup().setCurrentIndex(model.index(0, 0)) else: self.completer.popup().hide() def complete(self): if self.completing() and self.mode == COMPLETE_MODE.DROPDOWN: index = self.completer.popup().currentIndex() model = self.completer.completionModel() word = model.itemData(index)[0] self.insert_completion(word) self.completer.popup().hide() pyqtconsole-1.2.2/pyqtconsole/commandhistory.py000066400000000000000000000023311416004133300220600ustar00rootroot00000000000000# -*- coding: utf-8 -*- from qtpy.QtCore import QObject class CommandHistory(QObject): def __init__(self, parent): super(CommandHistory, self).__init__(parent) self._cmd_history = [] self._idx = 0 self._pending_input = '' def add(self, str_): if str_: self._cmd_history.append(str_) self._pending_input = '' self._idx = len(self._cmd_history) def inc(self): # index starts at 0 so + 1 to make sure that we are within the # limits of the list if self._cmd_history: self._idx = min(self._idx + 1, len(self._cmd_history)) self._insert_in_editor(self.current()) def dec(self, _input): if self._idx == len(self._cmd_history): self._pending_input = _input if len(self._cmd_history) and self._idx > 0: self._idx -= 1 self._insert_in_editor(self.current()) def current(self): if self._idx == len(self._cmd_history): return self._pending_input else: return self._cmd_history[self._idx] def _insert_in_editor(self, str_): self.parent().clear_input_buffer() self.parent().insert_input_text(str_) pyqtconsole-1.2.2/pyqtconsole/console.py000066400000000000000000000554621416004133300204770ustar00rootroot00000000000000# -*- coding: utf-8 -*- import threading import ctypes from abc import abstractmethod from qtpy.QtCore import Qt, QThread, Slot, QEvent from qtpy.QtWidgets import QPlainTextEdit, QApplication, QHBoxLayout, QFrame from qtpy.QtGui import QFontMetrics, QTextCursor, QClipboard from .interpreter import PythonInterpreter from .stream import Stream from .highlighter import PythonHighlighter, PromptHighlighter from .commandhistory import CommandHistory from .autocomplete import AutoComplete, COMPLETE_MODE from .prompt import PromptArea try: import jedi from jedi import settings settings.case_insensitive_completion = False except ImportError: jedi = None try: # PyQt >= 5.11 QueuedConnection = Qt.ConnectionType.QueuedConnection except AttributeError: # PyQt < 5.11 QueuedConnection = Qt.QueuedConnection class BaseConsole(QFrame): """Base class for implementing a GUI console.""" def __init__(self, parent=None, formats=None): super(BaseConsole, self).__init__(parent) self.edit = edit = InputArea() self.pbar = pbar = PromptArea( edit, self._get_prompt_text, PromptHighlighter(formats=formats)) layout = QHBoxLayout() layout.addWidget(pbar) layout.addWidget(edit) layout.setSpacing(0) layout.setContentsMargins(0, 0, 0, 0) self.setLayout(layout) self._prompt_doc = [''] self._prompt_pos = 0 self._output_inserted = False self._tab_chars = 4 * ' ' self._ctrl_d_exits = False self._copy_buffer = '' self._last_input = '' self._more = False self._current_line = 0 self._ps1 = 'IN [%s]: ' self._ps2 = '...: ' self._ps_out = 'OUT[%s]: ' self._ps = self._ps1 % self._current_line self.stdin = Stream() self.stdout = Stream() self.stdout.write_event.connect(self._stdout_data_handler) # show frame around both child widgets: self.setFrameStyle(edit.frameStyle()) edit.setFrameStyle(QFrame.NoFrame) font = edit.document().defaultFont() font.setFamily("Courier New") font_width = QFontMetrics(font).width('M') self.setFont(font) geometry = edit.geometry() geometry.setWidth(font_width*80+20) geometry.setHeight(font_width*40) edit.setGeometry(geometry) edit.resize(font_width*80+20, font_width*40) edit.setReadOnly(True) edit.setTextInteractionFlags( Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard) self.setFocusPolicy(Qt.NoFocus) pbar.setFocusPolicy(Qt.NoFocus) edit.setFocusPolicy(Qt.StrongFocus) edit.setFocus() edit.installEventFilter(self) self._key_event_handlers = self._get_key_event_handlers() self.command_history = CommandHistory(self) self.auto_complete = jedi and AutoComplete(self) self._show_ps() def setFont(self, font): """Set font (you should only use monospace!).""" self.edit.document().setDefaultFont(font) self.edit.setFont(font) super(BaseConsole, self).setFont(font) def eventFilter(self, edit, event): """Intercepts events from the input control.""" if event.type() == QEvent.KeyPress: return bool(self._filter_keyPressEvent(event)) elif event.type() == QEvent.MouseButtonPress: return bool(self._filter_mousePressEvent(event)) else: return False def _textCursor(self): return self.edit.textCursor() def _setTextCursor(self, cursor): self.edit.setTextCursor(cursor) def ensureCursorVisible(self): self.edit.ensureCursorVisible() def _update_ps(self, _more): # We need to show the more prompt of the input was incomplete # If the input is complete increase the input number and show # the in prompt if not _more: self._ps = self._ps1 % self._current_line else: self._ps = (len(self._ps) - len(self._ps2)) * ' ' + self._ps2 @Slot(bool, object) def _finish_command(self, executed, result): if result is not None: self._insert_output_text( repr(result), prompt=self._ps_out % self._current_line) self._insert_output_text('\n') if executed and self._last_input: self._current_line += 1 self._more = False self._show_cursor() self._update_ps(self._more) self._show_ps() def _show_ps(self): if self._output_inserted and not self._more: self._insert_output_text("\n") self._insert_prompt_text(self._ps) def _get_key_event_handlers(self): return { Qt.Key_Escape: self._handle_escape_key, Qt.Key_Return: self._handle_enter_key, Qt.Key_Enter: self._handle_enter_key, Qt.Key_Backspace: self._handle_backspace_key, Qt.Key_Delete: self._handle_delete_key, Qt.Key_Home: self._handle_home_key, Qt.Key_Tab: self._handle_tab_key, Qt.Key_Backtab: self._handle_backtab_key, Qt.Key_Up: self._handle_up_key, Qt.Key_Down: self._handle_down_key, Qt.Key_Left: self._handle_left_key, Qt.Key_D: self._handle_d_key, Qt.Key_C: self._handle_c_key, Qt.Key_V: self._handle_v_key, Qt.Key_U: self._handle_u_key, } def insertFromMimeData(self, mime_data): if mime_data and mime_data.hasText(): self.insert_input_text(mime_data.text()) def _filter_mousePressEvent(self, event): if event.button() == Qt.MiddleButton: clipboard = QApplication.clipboard() mime_data = clipboard.mimeData(QClipboard.Selection) self.insertFromMimeData(mime_data) return True def _filter_keyPressEvent(self, event): key = event.key() event.ignore() if self._executing(): # ignore all key presses while executing, except for Ctrl-C if event.modifiers() == Qt.ControlModifier and key == Qt.Key_C: self._handle_ctrl_c() return True handler = self._key_event_handlers.get(key) intercepted = handler and handler(event) # Assumes that Control+Key is a movement command, i.e. should not be # handled as text insertion. However, on win10 AltGr is reported as # Alt+Control which is why we handle this case like regular # keypresses, see #53: if not event.modifiers() & Qt.ControlModifier or \ event.modifiers() & Qt.AltModifier: self._keep_cursor_in_buffer() if not intercepted and event.text(): intercepted = True self.insert_input_text(event.text()) return intercepted def _handle_escape_key(self, event): return True def _handle_enter_key(self, event): if event.modifiers() & Qt.ShiftModifier: self.insert_input_text('\n') else: cursor = self._textCursor() cursor.movePosition(QTextCursor.End) self._setTextCursor(cursor) buffer = self.input_buffer() self._hide_cursor() self.insert_input_text('\n', show_ps=False) self.process_input(buffer) return True def _handle_backspace_key(self, event): self._keep_cursor_in_buffer() cursor = self._textCursor() offset = self.cursor_offset() if not cursor.hasSelection() and offset >= 1: tab = self._tab_chars buf = self._get_line_until_cursor() if event.modifiers() == Qt.ControlModifier: cursor.movePosition( QTextCursor.PreviousWord, QTextCursor.KeepAnchor, 1) self._keep_cursor_in_buffer() else: # delete spaces to previous tabstop boundary: tabstop = len(buf) % len(tab) == 0 num = len(tab) if tabstop and buf.endswith(tab) else 1 cursor.movePosition( QTextCursor.PreviousCharacter, QTextCursor.KeepAnchor, num) self._remove_selected_input(cursor) return True def _handle_delete_key(self, event): self._keep_cursor_in_buffer() cursor = self._textCursor() offset = self.cursor_offset() if not cursor.hasSelection() and offset < len(self.input_buffer()): tab = self._tab_chars left = self._get_line_until_cursor() right = self._get_line_after_cursor() if event.modifiers() == Qt.ControlModifier: cursor.movePosition( QTextCursor.NextWord, QTextCursor.KeepAnchor, 1) self._keep_cursor_in_buffer() else: # delete spaces to next tabstop boundary: tabstop = len(left) % len(tab) == 0 num = len(tab) if tabstop and right.startswith(tab) else 1 cursor.movePosition( QTextCursor.NextCharacter, QTextCursor.KeepAnchor, num) self._remove_selected_input(cursor) return True def _handle_tab_key(self, event): cursor = self._textCursor() if cursor.hasSelection(): self._setTextCursor(self._indent_selection(cursor)) else: # add spaces until next tabstop boundary: tab = self._tab_chars buf = self._get_line_until_cursor() num = len(tab) - len(buf) % len(tab) self.insert_input_text(tab[:num]) event.accept() return True def _handle_backtab_key(self, event): self._setTextCursor(self._indent_selection(self._textCursor(), False)) return True def _indent_selection(self, cursor, indent=True): buf = self.input_buffer() tab = self._tab_chars pos0 = cursor.selectionStart() - self._prompt_pos pos1 = cursor.selectionEnd() - self._prompt_pos line0 = buf[:pos0].count('\n') line1 = buf[:pos1].count('\n') lines = buf.split('\n') for i in range(line0, line1+1): # Although it at first seemed appealing to me to indent to the # next tab boundary, this leads to losing relative sub-tab # indentations and is therefore not desirable. We should therefore # always indent by a full tab: line = lines[i] if indent: lines[i] = tab + line else: lines[i] = line[:len(tab)].lstrip() + line[len(tab):] num = len(lines[i]) - len(line) pos0 += num if i == line0 else 0 pos1 += num self.clear_input_buffer() self.insert_input_text('\n'.join(lines)) cursor.setPosition(self._prompt_pos + pos0) cursor.setPosition(self._prompt_pos + pos1, QTextCursor.KeepAnchor) return cursor def _handle_home_key(self, event): select = event.modifiers() & Qt.ShiftModifier self._move_cursor(self._prompt_pos, select) return True def _handle_up_key(self, event): shift = event.modifiers() & Qt.ShiftModifier if shift or '\n' in self.input_buffer()[:self.cursor_offset()]: self._move_cursor(QTextCursor.Up, select=shift) else: self.command_history.dec(self.input_buffer()) return True def _handle_down_key(self, event): shift = event.modifiers() & Qt.ShiftModifier if shift or '\n' in self.input_buffer()[self.cursor_offset():]: self._move_cursor(QTextCursor.Down, select=shift) else: self.command_history.inc() return True def _handle_left_key(self, event): return self.cursor_offset() < 1 def _handle_d_key(self, event): if event.modifiers() == Qt.ControlModifier and not self.input_buffer(): if self._ctrl_d_exits: self.exit() else: self._insert_output_text( "\nCan't use CTRL-D to exit, you have to exit the " "application !\n") self._more = False self._update_ps(False) self._show_ps() return True def _handle_c_key(self, event): intercepted = False if event.modifiers() == Qt.ControlModifier: self._handle_ctrl_c() intercepted = True elif event.modifiers() == Qt.ControlModifier | Qt.ShiftModifier: self.edit.copy() intercepted = True return intercepted def _handle_u_key(self, event): if event.modifiers() == Qt.ControlModifier and self.input_buffer(): self.clear_input_buffer() return True return False def _handle_v_key(self, event): if event.modifiers() == Qt.ControlModifier or \ event.modifiers() == Qt.ControlModifier | Qt.ShiftModifier: clipboard = QApplication.clipboard() mime_data = clipboard.mimeData(QClipboard.Clipboard) self.insertFromMimeData(mime_data) return True return False def _hide_cursor(self): self.edit.setCursorWidth(0) def _show_cursor(self): self.edit.setCursorWidth(1) def _move_cursor(self, position, select=False): cursor = self._textCursor() mode = QTextCursor.KeepAnchor if select else QTextCursor.MoveAnchor if isinstance(position, QTextCursor.MoveOperation): cursor.movePosition(position, mode) else: cursor.setPosition(position, mode) self._setTextCursor(cursor) self._keep_cursor_in_buffer() def _keep_cursor_in_buffer(self): cursor = self._textCursor() if cursor.anchor() < self._prompt_pos: cursor.setPosition(self._prompt_pos) if cursor.position() < self._prompt_pos: cursor.setPosition(self._prompt_pos, QTextCursor.KeepAnchor) self._setTextCursor(cursor) self.ensureCursorVisible() def _insert_output_text(self, text, lf=False, keep_buffer=False, prompt=''): if keep_buffer: self._copy_buffer = self.input_buffer() cursor = self._textCursor() cursor.movePosition(QTextCursor.End) cursor.insertText(text) self._prompt_pos = cursor.position() self.ensureCursorVisible() self._insert_prompt_text(prompt + '\n' * text.count('\n')) self._output_inserted = True if lf: self.process_input('') def _update_prompt_pos(self): cursor = self._textCursor() cursor.movePosition(QTextCursor.End) self._prompt_pos = cursor.position() self._output_inserted = self._more def input_buffer(self): """Retrieve current input buffer in string form.""" return self.edit.toPlainText()[self._prompt_pos:] def cursor_offset(self): """Get current cursor index within input buffer.""" return self._textCursor().position() - self._prompt_pos def _get_line_until_cursor(self): """Get current line of input buffer, up to cursor position.""" return self.input_buffer()[:self.cursor_offset()].rsplit('\n', 1)[-1] def _get_line_after_cursor(self): """Get current line of input buffer, after cursor position.""" return self.input_buffer()[self.cursor_offset():].split('\n', 1)[0] def clear_input_buffer(self): """Clear input buffer.""" cursor = self._textCursor() cursor.setPosition(self._prompt_pos) cursor.movePosition(QTextCursor.End, QTextCursor.KeepAnchor) self._remove_selected_input(cursor) self._setTextCursor(cursor) def insert_input_text(self, text, show_ps=True): """Insert text into input buffer.""" self._keep_cursor_in_buffer() self.ensureCursorVisible() self._remove_selected_input(self._textCursor()) self._textCursor().insertText(text) if show_ps and '\n' in text: self._update_ps(True) for _ in range(text.count('\n')): # NOTE: need to insert in two steps, because this internally # uses setAlignment, which affects only the first line: self._insert_prompt_text('\n') self._insert_prompt_text(self._ps) elif '\n' in text: self._insert_prompt_text('\n' * text.count('\n')) def set_auto_complete_mode(self, mode): if self.auto_complete: self.auto_complete.mode = mode def process_input(self, source): """Handle a new source snippet confirmed by the user.""" self._last_input = source self._more = self._run_source(source) self._update_ps(self._more) if self._more: self._show_ps() self._show_cursor() else: self.command_history.add(source) self._update_prompt_pos() def _handle_ctrl_c(self): """Inject keyboard interrupt if code is being executed in a thread, else cancel the current prompt.""" # There is a race condition here, we should lock on the value of # executing() to avoid accidentally raising KeyboardInterrupt after # execution has finished. Deal with this later… if self._executing(): self._cancel() else: self._last_input = '' self.stdout.write('^C\n') self._output_inserted = False self._more = False self._update_ps(self._more) self._show_ps() def _stdout_data_handler(self, data): self._insert_output_text(data) if len(self._copy_buffer) > 0: self.insert_input_text(self._copy_buffer) self._copy_buffer = '' def _insert_prompt_text(self, text): lines = text.split('\n') self._prompt_doc[-1] += lines[0] self._prompt_doc += lines[1:] for line in self._prompt_doc[-len(lines):]: self.pbar.adjust_width(line) def _get_prompt_text(self, line_number): return self._prompt_doc[line_number] def _remove_selected_input(self, cursor): if not cursor.hasSelection(): return num_lines = cursor.selectedText().replace(u'\u2029', '\n').count('\n') cursor.removeSelectedText() if num_lines > 0: block = cursor.blockNumber() + 1 del self._prompt_doc[block:block+num_lines] def closeEvent(self, event): """Exit interpreter when we're closing.""" self.exit() event.accept() def _close(self): if self.window().isVisible(): self.window().close() def set_tab(self, chars): self._tab_chars = chars def ctrl_d_exits_console(self, b): self._ctrl_d_exits = b # Abstract @abstractmethod def exit(self): pass @abstractmethod def _executing(self): pass @abstractmethod def _cancel(self): pass @abstractmethod def _run_source(self, source): pass @abstractmethod def get_completions(self, line): return ['No completion support available'] class PythonConsole(BaseConsole): """Interactive python GUI console.""" def __init__(self, parent=None, locals=None, formats=None): super(PythonConsole, self).__init__(parent, formats=formats) self.highlighter = PythonHighlighter( self.edit.document(), formats=formats) self.interpreter = PythonInterpreter( self.stdin, self.stdout, locals=locals) self.interpreter.done_signal.connect(self._finish_command) self.interpreter.exit_signal.connect(self.exit) self.set_auto_complete_mode(COMPLETE_MODE.DROPDOWN) self._thread = None def _executing(self): return self.interpreter.executing() def _cancel(self): if self._thread: self._thread.inject_exception(KeyboardInterrupt) # wake up thread in case it is currently waiting on input: self.stdin.flush() def _run_source(self, source): return self.interpreter.runsource(source, symbol='multi') def exit(self): """Exit interpreter.""" if self._thread: self._thread.exit() self._thread.wait() self._thread = None self._close() def get_completions(self, line): """Get completions. Used by the ``autocomplete`` extension.""" script = jedi.Interpreter(line, [self.interpreter.locals]) try: comps = script.complete() except AttributeError: # Jedi < 0.16.0 named the method differently comps = script.completions() return [comp.name for comp in comps] def push_local_ns(self, name, value): """Set a variable in the local namespace.""" self.interpreter.locals[name] = value def eval_in_thread(self): """Start a thread in which code snippets will be executed.""" self._thread = Thread() self.interpreter.moveToThread(self._thread) self.interpreter.exec_signal.connect( self.interpreter.exec_, QueuedConnection) return self._thread def eval_queued(self): """Setup connections to execute code snippets in later mainloop iterations in the main thread.""" return self.interpreter.exec_signal.connect( self.interpreter.exec_, QueuedConnection) def eval_executor(self, spawn): """Exec snippets using the given executor function (e.g. ``gevent.spawn``).""" return self.interpreter.exec_signal.connect( lambda line: spawn(self.interpreter.exec_, line)) class Thread(QThread): """Thread that runs an event loop and exposes thread ID as ``.ident``.""" def __init__(self, parent=None): super(Thread, self).__init__(parent) self.ready = threading.Event() self.start() self.ready.wait() def run(self): """Run Qt event dispatcher within the thread.""" self.ident = threading.current_thread().ident self.ready.set() self.exec_() def inject_exception(self, value): """Raise exception in remote thread to stop execution of current commands (this only triggers once the thread executes any python bytecode).""" if self.ident != threading.current_thread().ident: ctypes.pythonapi.PyThreadState_SetAsyncExc( ctypes.c_long(self.ident), ctypes.py_object(value)) class InputArea(QPlainTextEdit): """Widget that is used for the input/output edit area of the console.""" def insertFromMimeData(self, mime_data): return self.parent().insertFromMimeData(mime_data) pyqtconsole-1.2.2/pyqtconsole/highlighter.py000066400000000000000000000137541416004133300213310ustar00rootroot00000000000000from qtpy.QtCore import QRegExp from qtpy.QtGui import (QColor, QTextCharFormat, QFont, QSyntaxHighlighter) import keyword def format(color, style=''): """Return a QTextCharFormat with the given attributes. """ _color = QColor() _color.setNamedColor(color) _format = QTextCharFormat() _format.setForeground(_color) if 'bold' in style: _format.setFontWeight(QFont.Bold) if 'italic' in style: _format.setFontItalic(True) return _format # Syntax styles that can be shared by all languages STYLES = { 'keyword': format('blue', 'bold'), 'operator': format('red'), 'brace': format('darkGray'), 'defclass': format('black', 'bold'), 'string': format('magenta'), 'string2': format('darkMagenta'), 'comment': format('darkGreen', 'italic'), 'self': format('black', 'italic'), 'numbers': format('brown'), 'inprompt': format('darkBlue', 'bold'), 'outprompt': format('darkRed', 'bold'), } class PromptHighlighter(object): def __init__(self, formats=None): self.styles = styles = dict(STYLES, **(formats or {})) self.rules = [ # Match the prompt incase of a console (QRegExp(r'IN[^\:]*'), 0, styles['inprompt']), (QRegExp(r'OUT[^\:]*'), 0, styles['outprompt']), # Numeric literals (QRegExp(r'\b[+-]?[0-9]+\b'), 0, styles['numbers']), ] def highlight(self, text): for expression, nth, format in self.rules: index = expression.indexIn(text, 0) while index >= 0: index = expression.pos(nth) length = len(expression.cap(nth)) yield (index, length, format) index = expression.indexIn(text, index + length) class PythonHighlighter(QSyntaxHighlighter): """Syntax highlighter for the Python language. """ # Python keywords keywords = keyword.kwlist def __init__(self, document, formats=None): QSyntaxHighlighter.__init__(self, document) self.styles = styles = dict(STYLES, **(formats or {})) # Multi-line strings (expression, flag, style) # FIXME: The triple-quotes in these two lines will mess up the # syntax highlighting from this point onward self.tri_single = (QRegExp("'''"), 1, styles['string2']) self.tri_double = (QRegExp('"""'), 2, styles['string2']) rules = [] # Keyword, operator, and brace rules rules += [(r'\b%s\b' % w, 0, styles['keyword']) for w in PythonHighlighter.keywords] # All other rules rules += [ # 'self' # (r'\bself\b', 0, STYLES['self']), # Double-quoted string, possibly containing escape sequences (r'"[^"\\]*(\\.[^"\\]*)*"', 0, styles['string']), # Single-quoted string, possibly containing escape sequences (r"'[^'\\]*(\\.[^'\\]*)*'", 0, styles['string']), # 'def' followed by an identifier (r'\bdef\b\s*(\w+)', 1, styles['defclass']), # 'class' followed by an identifier (r'\bclass\b\s*(\w+)', 1, styles['defclass']), # From '#' until a newline (r'#[^\n]*', 0, styles['comment']), # Numeric literals (r'\b[+-]?[0-9]+[lL]?\b', 0, styles['numbers']), (r'\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b', 0, styles['numbers']), (r'\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b', 0, styles['numbers']), ] # Build a QRegExp for each pattern self.rules = [(QRegExp(pat), index, fmt) for (pat, index, fmt) in rules] def highlightBlock(self, text): """Apply syntax highlighting to the given block of text. """ # Do other syntax formatting for expression, nth, format in self.rules: index = expression.indexIn(text, 0) while index >= 0: # We actually want the index of the nth match index = expression.pos(nth) length = len(expression.cap(nth)) self.setFormat(index, length, format) index = expression.indexIn(text, index + length) self.setCurrentBlockState(0) # Do multi-line strings in_multiline = self.match_multiline(text, *self.tri_single) if not in_multiline: in_multiline = self.match_multiline(text, *self.tri_double) def match_multiline(self, text, delimiter, in_state, style): """Do highlighting of multi-line strings. ``delimiter`` should be a ``QRegExp`` for triple-single-quotes or triple-double-quotes, and ``in_state`` should be a unique integer to represent the corresponding state changes when inside those strings. Returns True if we're still inside a multi-line string when this function is finished. """ # If inside triple-single quotes, start at 0 if self.previousBlockState() == in_state: start = 0 add = 0 # Otherwise, look for the delimiter on this line else: start = delimiter.indexIn(text) # Move past this match add = delimiter.matchedLength() # As long as there's a delimiter match on this line... while start >= 0: # Look for the ending delimiter end = delimiter.indexIn(text, start + add) # Ending delimiter on this line? if end >= add: length = end - start + add + delimiter.matchedLength() self.setCurrentBlockState(0) # No; multi-line string else: self.setCurrentBlockState(in_state) length = len(text) - start + add # Apply formatting self.setFormat(start, length, style) # Look for the next match start = delimiter.indexIn(text, start + length) # Return True if still inside a multi-line string, False otherwise return self.currentBlockState() == in_state pyqtconsole-1.2.2/pyqtconsole/interpreter.py000066400000000000000000000116431416004133300213710ustar00rootroot00000000000000# -*- coding: utf-8 -*- import sys import contextlib from functools import partial import ast from code import InteractiveInterpreter from qtpy.QtCore import QObject, Slot, Signal class PythonInterpreter(QObject, InteractiveInterpreter): exec_signal = Signal(object) done_signal = Signal(bool, object) exit_signal = Signal(object) def __init__(self, stdin, stdout, locals=None): QObject.__init__(self) InteractiveInterpreter.__init__(self, locals) self.locals['exit'] = Exit() self.stdin = stdin self.stdout = stdout self._executing = False self.compile = partial(compile_multi, self.compile) def executing(self): return self._executing def runcode(self, code): self.exec_signal.emit(code) @Slot(object) def exec_(self, codes): self._executing = True result = None # Redirect IO and disable excepthook, this is the only place were we # redirect IO, since we don't how IO is handled within the code we # are running. Same thing for the except hook, we don't know what the # user are doing in it. try: with redirected_io(self.stdout): for code, mode in codes: if mode == 'eval': result = eval(code, self.locals) else: exec(code, self.locals) except SystemExit as e: self.exit_signal.emit(e) except BaseException: self.showtraceback() finally: self._executing = False self.done_signal.emit(True, result) def write(self, data): self.stdout.write(data) def showtraceback(self): type_, value, tb = sys.exc_info() self.stdout.write('\n') if type_ == KeyboardInterrupt: self.stdout.write('KeyboardInterrupt\n') else: with disabled_excepthook(): InteractiveInterpreter.showtraceback(self) def showsyntaxerror(self, filename): self.stdout.write('\n') with disabled_excepthook(): InteractiveInterpreter.showsyntaxerror(self, filename) self.done_signal.emit(False, None) def compile_multi(compiler, source, filename, symbol): """If mode is 'multi', split code into individual toplevel expressions or statements. Returns a list of tuples ``(code, mode)``. """ if symbol != 'multi': return [(compiler(source, filename, symbol), symbol)] # First, check if the source compiles at all. This raises an exception if # there is a SyntaxError, or returns None if the code is incomplete: if compiler(source, filename, 'exec') is None: return None # Now split into individual 'single' units: module = ast.parse(source) # When entering a code block, the standard python interpreter waits for an # additional empty line to apply the input. We adhere to this convention, # checked by `compiler(..., 'single')`: if module.body: block_lineno = module.body[-1].lineno block_source = source[find_nth('\n' + source, '\n', block_lineno):] if compiler(block_source, filename, 'single') is None: return None return [ compile_single_node(node, filename) for node in module.body ] def compile_single_node(node, filename): """Compile a 'single' ast.node (expression or statement).""" mode = 'eval' if isinstance(node, ast.Expr) else 'exec' if mode == 'eval': root = ast.Expression(node.value) else: if sys.version_info >= (3, 8): root = ast.Module([node], type_ignores=[]) else: root = ast.Module([node]) return (compile(root, filename, mode), mode) def find_nth(string, char, n): """Find the n'th occurence of a character within a string.""" return [i for i, c in enumerate(string) if c == char][n-1] @contextlib.contextmanager def disabled_excepthook(): """Run code with the exception hook temporarily disabled.""" old_excepthook = sys.excepthook sys.excepthook = sys.__excepthook__ try: yield finally: # If the code we did run did change sys.excepthook, we leave it # unchanged. Otherwise, we reset it. if sys.excepthook is sys.__excepthook__: sys.excepthook = old_excepthook @contextlib.contextmanager def redirected_io(stdout): old_stdout = sys.stdout old_stderr = sys.stderr sys.stdout = stdout sys.stderr = stdout try: yield finally: if sys.stdout is stdout: sys.stdout = old_stdout if sys.stderr is stdout: sys.stderr = old_stderr # We use a custom exit function to avoid issues with environments such as # spyder, where `builtins.exit` is not available, see #26: class Exit: def __repr__(self): return "Type exit() to exit this console." def __call__(self, *args): raise SystemExit(*args) pyqtconsole-1.2.2/pyqtconsole/prompt.py000066400000000000000000000051601416004133300203440ustar00rootroot00000000000000from qtpy.QtCore import Qt, QRect from qtpy.QtWidgets import QWidget from qtpy.QtGui import QPainter class PromptArea(QWidget): """Widget that displays the prompts on the left of the input area.""" def __init__(self, edit, get_text, highlighter): super(PromptArea, self).__init__(edit) self.setFixedWidth(0) self.edit = edit self.get_text = get_text self.highlighter = highlighter edit.updateRequest.connect(self.updateContents) def paintEvent(self, event): edit = self.edit height = edit.fontMetrics().height() block = edit.firstVisibleBlock() count = block.blockNumber() painter = QPainter(self) painter.fillRect(event.rect(), edit.palette().base()) first = True while block.isValid(): count += 1 block_top = edit.blockBoundingGeometry(block).translated( edit.contentOffset()).top() if not block.isVisible() or block_top > event.rect().bottom(): break rect = QRect(0, int(block_top), self.width(), height) self.draw_block(painter, rect, block, first) first = False block = block.next() painter.end() super(PromptArea, self).paintEvent(event) def updateContents(self, rect, scroll): if scroll: self.scroll(0, scroll) else: self.update() def adjust_width(self, new_text): width = calc_text_width(self.edit, new_text) if width > self.width(): self.setFixedWidth(width) def draw_block(self, painter, rect, block, first): """Draw the info corresponding to a given block (text line) of the text document.""" pen = painter.pen() text = self.get_text(block.blockNumber()) default = self.edit.currentCharFormat() formats = [default] * len(text) painter.setFont(self.edit.font()) for index, length, format in self.highlighter.highlight(text): formats[index:index+length] = [format] * length for idx, (char, format) in enumerate(zip(text, formats)): rpos = len(text) - idx - 1 pen.setColor(format.foreground().color()) painter.setPen(pen) painter.drawText(rect, Qt.AlignRight, text[idx] + ' ' * rpos) def calc_text_width(widget, text): """Estimate the width that the given text would take within the widget.""" return (widget.fontMetrics().width(text) + widget.fontMetrics().width('M') + widget.contentsMargins().left() + widget.contentsMargins().right()) pyqtconsole-1.2.2/pyqtconsole/stream.py000066400000000000000000000047001416004133300203150ustar00rootroot00000000000000# -*- coding: utf-8 -*- from threading import Condition from qtpy.QtCore import QObject, Signal class Stream(QObject): write_event = Signal(str) flush_event = Signal(str) close_event = Signal() def __init__(self): super(Stream, self).__init__() self._line_cond = Condition() self._buffer = '' def _reset_buffer(self): data = self._buffer self._buffer = '' return data def _flush(self): with self._line_cond: data = self._reset_buffer() self._line_cond.notify() return data def readline(self, timeout=None): data = '' try: with self._line_cond: first_linesep = self._buffer.find('\n') # Is there already some lines in the buffer, write might have # been called before we read ! while first_linesep == -1: notfied = self._line_cond.wait(timeout) first_linesep = self._buffer.find('\n') # We had a timeout, break ! if not notfied: break # Check if there really is something in the buffer after # waiting for line_cond. There might have been a timeout, and # there is still no data available if first_linesep > -1: data = self._buffer[0:first_linesep+1] if len(self._buffer) > len(data): self._buffer = self._buffer[first_linesep+1:] else: self._buffer = '' # Tricky RuntimeError !, wait releases the lock and waits for notify # and then acquire the lock again !. There might be an exception, i.e # KeyboardInterupt which interrupts the wait. The cleanup of the with # statement then tries to release the lock which is not acquired, # causing a RuntimeError. puh ! If its the case just try again ! except RuntimeError: data = self.readline(timeout) return data def write(self, data): with self._line_cond: self._buffer += data if '\n' in self._buffer: self._line_cond.notify() self.write_event.emit(data) def flush(self): data = self._flush() self.flush_event.emit(data) return data def close(self): self.close_event.emit() pyqtconsole-1.2.2/pyqtconsole/text.py000066400000000000000000000201101416004133300177770ustar00rootroot00000000000000# -*- coding: utf-8 -*- def long_substr(data): substr = '' if len(data) > 1 and len(data[0]) > 0: for i in range(len(data[0])): for j in range(len(data[0])-i+1): if j > len(substr) and is_substr(data[0][i:i+j], data): substr = data[0][i:i+j] elif len(data) == 1: substr = data[0] return substr def is_substr(find, data): if len(data) < 1 and len(find) < 1: return False for i in range(len(data)): if find not in data[i]: return False return True default_opts = { 'arrange_array': False, # Check if file has changed since last time 'arrange_vertical': True, 'array_prefix': '', 'array_suffix': '', 'colfmt': None, 'colsep': ' ', 'displaywidth': 80, 'lineprefix': '', 'linesuffix': "\n", 'ljust': None, 'term_adjust': False } def get_option(key, options): return options.get(key, default_opts.get(key)) def columnize(array, displaywidth=80, colsep=' ', arrange_vertical=True, ljust=True, lineprefix='', opts={}): """Return a list of strings as a compact set of columns arranged horizontally or vertically. For example, for a line width of 4 characters (arranged vertically): ['1', '2,', '3', '4'] => '1 3\n2 4\n' or arranged horizontally: ['1', '2,', '3', '4'] => '1 2\n3 4\n' Each column is only as wide as necessary. By default, columns are separated by two spaces - one was not legible enough. Set "colsep" to adjust the string separate columns. Set `displaywidth' to set the line width. Normally, consecutive items go down from the top to bottom from the left-most column to the right-most. If "arrange_vertical" is set false, consecutive items will go across, left to right, top to bottom.""" if not isinstance(array, (list, tuple)): raise TypeError(( 'array needs to be an instance of a list or a tuple')) if len(opts.keys()) > 0: o = {key: get_option(key, opts) for key in default_opts} if o['arrange_array']: o.update({ 'array_prefix': '[', 'lineprefix': ' ', 'linesuffix': ",\n", 'array_suffix': "]\n", 'colsep': ', ', 'arrange_vertical': False, }) else: o = default_opts.copy() o.update({ 'displaywidth': displaywidth, 'colsep': colsep, 'arrange_vertical': arrange_vertical, 'ljust': ljust, 'lineprefix': lineprefix, }) # if o['ljust'] is None: # o['ljust'] = !(list.all?{|datum| datum.kind_of?(Numeric)}) if o['colfmt']: array = [(o['colfmt'] % i) for i in array] else: array = [str(i) for i in array] # Some degenerate cases size = len(array) if 0 == size: return "\n" elif size == 1: return '%s%s%s\n' % (o['array_prefix'], str(array[0]), o['array_suffix']) if o['displaywidth'] - len(o['lineprefix']) < 4: o['displaywidth'] = len(o['lineprefix']) + 4 else: o['displaywidth'] -= len(o['lineprefix']) o['displaywidth'] = max(4, o['displaywidth'] - len(o['lineprefix'])) if o['arrange_vertical']: def array_index(nrows, row, col): return nrows*col + row # Try every row count from 1 upwards for nrows in range(1, size): ncols = (size+nrows-1) // nrows colwidths = [] totwidth = -len(o['colsep']) for col in range(ncols): # get max column width for this column colwidth = 0 for row in range(nrows): i = array_index(nrows, row, col) if i >= size: break x = array[i] colwidth = max(colwidth, len(x)) colwidths.append(colwidth) totwidth += colwidth + len(o['colsep']) if totwidth > o['displaywidth']: break if totwidth <= o['displaywidth']: break # The smallest number of rows computed and the # max widths for each column has been obtained. # Now we just have to format each of the # rows. s = '' for row in range(nrows): texts = [] for col in range(ncols): i = row + nrows*col if i >= size: x = "" else: x = array[i] texts.append(x) while texts and not texts[-1]: del texts[-1] for col in range(len(texts)): if o['ljust']: texts[col] = texts[col].ljust(colwidths[col]) else: texts[col] = texts[col].rjust(colwidths[col]) s += "%s%s%s" % (o['lineprefix'], str(o['colsep'].join(texts)), o['linesuffix']) return s else: def array_index(ncols, row, col): return ncols*(row-1) + col # Try every column count from size downwards colwidths = [] for ncols in range(size, 0, -1): # Try every row count from 1 upwards min_rows = (size+ncols-1) // ncols nrows = min_rows - 1 while nrows < size: nrows += 1 rounded_size = nrows * ncols colwidths = [] totwidth = -len(o['colsep']) for col in range(ncols): # get max column width for this column colwidth = 0 for row in range(1, nrows+1): i = array_index(ncols, row, col) if i >= rounded_size: break elif i < size: x = array[i] colwidth = max(colwidth, len(x)) colwidths.append(colwidth) totwidth += colwidth + len(o['colsep']) if totwidth >= o['displaywidth']: break if totwidth <= o['displaywidth'] and i >= rounded_size-1: # Found the right nrows and ncols # print "right nrows and ncols" nrows = row break elif totwidth >= o['displaywidth']: # print "reduce ncols", ncols # Need to reduce ncols break if totwidth <= o['displaywidth'] and i >= rounded_size-1: break # The smallest number of rows computed and the # max widths for each column has been obtained. # Now we just have to format each of the # rows. s = '' if len(o['array_prefix']) != 0: prefix = o['array_prefix'] else: prefix = o['lineprefix'] for row in range(1, nrows+1): texts = [] for col in range(ncols): i = array_index(ncols, row, col) if i >= size: break else: x = array[i] texts.append(x) for col in range(len(texts)): if o['ljust']: texts[col] = texts[col].ljust(colwidths[col]) else: texts[col] = texts[col].rjust(colwidths[col]) s += "%s%s%s" % (prefix, str(o['colsep'].join(texts)), o['linesuffix']) prefix = o['lineprefix'] if o['arrange_array']: colsep = o['colsep'].rstrip() colsep_pos = -(len(colsep)+1) if s[colsep_pos:] == colsep + "\n": s = s[:colsep_pos] + o['array_suffix'] + "\n" else: s += o['array_suffix'] return s pyqtconsole-1.2.2/setup.cfg000066400000000000000000000020541416004133300157110ustar00rootroot00000000000000[metadata] name = pyqtconsole version = attr: pyqtconsole.__version__ description = Light weight python interpreter, easy to embed into Qt applications author = Marcus Oskarsson author_email = marcus.oscarsson@esrf.fr url = https://github.com/marcus-oscarsson/pyqtconsole license = MIT licens_file = LICENSE keywords = interactive interpreter console shell autocompletion jedi qt long_description = file: README.rst, CHANGES.rst classifiers = Environment :: X11 Applications :: Qt Intended Audience :: Developers Operating System :: OS Independent Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Topic :: Software Development :: Interpreters Topic :: Software Development :: User Interfaces long_description_content_type = text/x-rst [options] packages = pyqtconsole python_requires = >=2.7 zip_safe = true include_package_data = true install_requires = qtpy jedi [bdist_wheel] universal = true [flake8] max-line-length = 82 pyqtconsole-1.2.2/setup.py000066400000000000000000000000731416004133300156010ustar00rootroot00000000000000#!/usr/bin/env python from setuptools import setup setup() pyqtconsole-1.2.2/tests/000077500000000000000000000000001416004133300152315ustar00rootroot00000000000000pyqtconsole-1.2.2/tests/test_text.py000066400000000000000000000130601416004133300176260ustar00rootroot00000000000000import pytest from pyqtconsole.text import columnize def _strip(text): """Normalize expected strings to allow more readable definition.""" return text.lstrip('\n').rstrip(' ') def test_columnize_basic(): assert columnize([]) == '\n' assert columnize(["a", '2', "c"], 10, ', ') == 'a, 2, c\n' assert columnize(["oneitem"]) == 'oneitem\n' assert columnize(("one", "two", "three")) == 'one two three\n' assert columnize(list(range(4))) == '0 1 2 3\n' def test_columnize_array(): assert columnize(list(range(12)), opts={ 'displaywidth': 6, 'arrange_array': True}) == _strip(""" [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] """) assert columnize(list(range(12)), opts={ 'displaywidth': 10, 'arrange_array': True}) == _strip(""" [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] """) def test_columnize_horizontal_vs_vertical(): dat4 = list('0123') # use displaywidth 4 assert columnize(dat4, opts={ 'displaywidth': 4, 'arrange_vertical': False}) == _strip(""" 0 1 2 3 """) assert columnize(dat4, opts={ 'displaywidth': 4, 'arrange_vertical': True}) == _strip(""" 0 2 1 3 """) # use displaywidth 7: assert columnize(dat4, opts={ 'displaywidth': 7, 'arrange_vertical': False}) == _strip(""" 0 1 2 3 """) # FIXME: this looks like a bug to me: assert columnize(dat4, opts={ 'displaywidth': 7, 'arrange_vertical': True}) == _strip(""" 0 2 1 3 """) # longer dataset: dat100 = [str(i) for i in range(100)] assert columnize(dat100, opts={ 'displaywidth': 80, 'arrange_vertical': False}) == _strip(""" 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 """) assert columnize(dat100, opts={ 'displaywidth': 80, 'arrange_vertical': True}) == _strip(""" 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 1 6 11 16 21 26 31 36 41 46 51 56 61 66 71 76 81 86 91 96 2 7 12 17 22 27 32 37 42 47 52 57 62 67 72 77 82 87 92 97 3 8 13 18 23 28 33 38 43 48 53 58 63 68 73 78 83 88 93 98 4 9 14 19 24 29 34 39 44 49 54 59 64 69 74 79 84 89 94 99 """) def test_columnize_count27(): data = ( "one", "two", "three", "for", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eightteen", "nineteen", "twenty", "twentyone", "twentytwo", "twentythree", "twentyfour", "twentyfive", "twentysix", "twentyseven", ) # We use 'inline strings' to make sure the trailing space is obvious to # the reader and won't get lost due to automatic removal in some editors: assert columnize(data) == ( 'one five nine thirteen seventeen twentyone twentyfive \n' 'two six ten fourteen eightteen twentytwo twentysix \n' 'three seven eleven fifteen nineteen twentythree twentyseven\n' 'for eight twelve sixteen twenty twentyfour \n') assert columnize(data, arrange_vertical=False) == ( 'one two three for five six \n' 'seven eight nine ten eleven twelve \n' 'thirteen fourteen fifteen sixteen seventeen eightteen \n' 'nineteen twenty twentyone twentytwo twentythree twentyfour\n' 'twentyfive twentysix twentyseven\n') def test_columnize_count55(): data = [str(i) for i in range(55)] assert columnize(data, opts={ 'displaywidth': 39, 'arrange_array': True}) == _strip(""" [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54] """) assert columnize( data, displaywidth=39, ljust=False, colsep=', ', lineprefix=' ') == _strip(""" 0, 7, 14, 21, 28, 35, 42, 49 1, 8, 15, 22, 29, 36, 43, 50 2, 9, 16, 23, 30, 37, 44, 51 3, 10, 17, 24, 31, 38, 45, 52 4, 11, 18, 25, 32, 39, 46, 53 5, 12, 19, 26, 33, 40, 47, 54 6, 13, 20, 27, 34, 41, 48 """) assert columnize( data, displaywidth=39, ljust=False, arrange_vertical=False, colsep=', ') == _strip(""" 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 20, 21, 22, 23, 24, 25, 26, 27, 28, 29 30, 31, 32, 33, 34, 35, 36, 37, 38, 39 40, 41, 42, 43, 44, 45, 46, 47, 48, 49 50, 51, 52, 53, 54 """) assert columnize( data, displaywidth=39, ljust=False, arrange_vertical=False, colsep=', ', lineprefix=' ') == _strip(""" 0, 1, 2, 3, 4, 5, 6, 7 8, 9, 10, 11, 12, 13, 14, 15 16, 17, 18, 19, 20, 21, 22, 23 24, 25, 26, 27, 28, 29, 30, 31 32, 33, 34, 35, 36, 37, 38, 39 40, 41, 42, 43, 44, 45, 46, 47 48, 49, 50, 51, 52, 53, 54 """) def test_columnize_raises_typeerror(): with pytest.raises(TypeError): columnize(5)