diff --git a/qtconsole/comms.py b/qtconsole/comms.py index 74f8ce5c..7280e60d 100644 --- a/qtconsole/comms.py +++ b/qtconsole/comms.py @@ -11,12 +11,12 @@ from traitlets.config import LoggingConfigurable -from ipython_genutils.importstring import import_item import uuid from qtpy import QtCore -from qtconsole.util import MetaQObjectHasTraits, SuperQObject + +from qtconsole.util import MetaQObjectHasTraits, SuperQObject, import_item class CommManager(MetaQObjectHasTraits( diff --git a/qtconsole/completion_html.py b/qtconsole/completion_html.py index 62391112..8881b8a4 100644 --- a/qtconsole/completion_html.py +++ b/qtconsole/completion_html.py @@ -3,10 +3,11 @@ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. -import ipython_genutils.text as text from qtpy import QtCore, QtGui, QtWidgets +from .util import compute_item_matrix + #-------------------------------------------------------------------------- # Return an HTML table with selected item in a special class #-------------------------------------------------------------------------- @@ -311,9 +312,8 @@ def show_items(self, cursor, items, prefix_length=0): width = self._text_edit.document().textWidth() char_width = self._console_widget._get_font_width() displaywidth = int(max(10, (width / char_width) - 1)) - items_m, ci = text.compute_item_matrix(items, empty=' ', - displaywidth=displaywidth) - self._sliding_interval = SlidingInterval(len(items_m)-1, width=self._rows) + items_m, ci = compute_item_matrix(items, empty=" ", displaywidth=displaywidth) + self._sliding_interval = SlidingInterval(len(items_m) - 1, width=self._rows) self._items = items_m self._size = (ci['rows_numbers'], ci['columns_numbers']) diff --git a/qtconsole/completion_plain.py b/qtconsole/completion_plain.py index 6596c907..367ceacd 100644 --- a/qtconsole/completion_plain.py +++ b/qtconsole/completion_plain.py @@ -4,7 +4,8 @@ # Distributed under the terms of the Modified BSD License. from qtpy import QtCore, QtGui, QtWidgets -import ipython_genutils.text as text + +from .util import columnize class CompletionPlain(QtWidgets.QWidget): @@ -53,7 +54,7 @@ def show_items(self, cursor, items, prefix_length=0): if not items : return self.cancel_completion() - strng = text.columnize(items) + strng = columnize(items) # Move cursor to start of the prefix to replace it # when a item is selected cursor.movePosition(QtGui.QTextCursor.Left, n=prefix_length) diff --git a/qtconsole/console_widget.py b/qtconsole/console_widget.py index 447be581..62d20377 100644 --- a/qtconsole/console_widget.py +++ b/qtconsole/console_widget.py @@ -19,7 +19,6 @@ from qtconsole.rich_text import HtmlExporter from qtconsole.util import MetaQObjectHasTraits, get_font, superQ -from ipython_genutils.text import columnize from traitlets.config.configurable import LoggingConfigurable from traitlets import Bool, Enum, Integer, Unicode @@ -28,6 +27,7 @@ from .completion_html import CompletionHtml from .completion_plain import CompletionPlain from .kill_ring import QtKillRing +from .util import columnize def is_letter_or_number(char): @@ -1680,28 +1680,6 @@ def _flush_pending_stream(self): int(max(100, (time.time() - t) * 1000)) ) - def _format_as_columns(self, items, separator=' '): - """ Transform a list of strings into a single string with columns. - - Parameters - ---------- - items : sequence of strings - The strings to process. - - separator : str, optional [default is two spaces] - The string that separates columns. - - Returns - ------- - The formatted string. - """ - # Calculate the number of characters available. - width = self._control.document().textWidth() - char_width = self._get_font_width() - displaywidth = max(10, (width / char_width) - 1) - - return columnize(items, separator, displaywidth) - def _get_cursor(self): """ Get a cursor at the current insert position. """ diff --git a/qtconsole/frontend_widget.py b/qtconsole/frontend_widget.py index d7d1134d..9cc96a59 100644 --- a/qtconsole/frontend_widget.py +++ b/qtconsole/frontend_widget.py @@ -9,7 +9,6 @@ import re from qtpy import QtCore, QtGui, QtWidgets -from ipython_genutils.importstring import import_item from qtconsole.base_frontend_mixin import BaseFrontendMixin from traitlets import Any, Bool, Instance, Unicode, DottedObjectName, default @@ -17,6 +16,7 @@ from .call_tip_widget import CallTipWidget from .history_console_widget import HistoryConsoleWidget from .pygments_highlighter import PygmentsHighlighter +from .util import import_item class FrontendHighlighter(PygmentsHighlighter): diff --git a/qtconsole/rich_jupyter_widget.py b/qtconsole/rich_jupyter_widget.py index 885033d8..879b3cfa 100644 --- a/qtconsole/rich_jupyter_widget.py +++ b/qtconsole/rich_jupyter_widget.py @@ -8,7 +8,6 @@ from qtpy import QtCore, QtGui, QtWidgets -from ipython_genutils.path import ensure_dir_exists from traitlets import Bool from pygments.util import ClassNotFound @@ -22,6 +21,23 @@ latex_to_png = None +def _ensure_dir_exists(path, mode=0o755): + """ensure that a directory exists + + If it doesn't exists, try to create it and protect against a race condition + if another process is doing the same. + + The default permissions are 755, which differ from os.makedirs default of 777. + """ + if not os.path.exists(path): + try: + os.makedirs(path, mode=mode) + except OSError as e: + if e.errno != errno.EEXIST: + raise + elif not os.path.isdir(path): + raise IOError("%r exists but is not a directory" % path) + class LatexError(Exception): """Exception for Latex errors""" @@ -310,7 +326,7 @@ def _get_image_tag(self, match, path = None, format = "png"): return "Couldn't find image %s" % match.group("name") if path is not None: - ensure_dir_exists(path) + _ensure_dir_exists(path) relpath = os.path.basename(path) if image.save("%s/qt_img%s.%s" % (path, match.group("name"), format), "PNG"): diff --git a/qtconsole/util.py b/qtconsole/util.py index 7c1a116d..c3827a4a 100644 --- a/qtconsole/util.py +++ b/qtconsole/util.py @@ -106,3 +106,157 @@ def get_font(family, fallback=None): if fallback is not None and font_info.family() != family: font = QtGui.QFont(fallback) return font + + +# ----------------------------------------------------------------------------- +# Vendored from ipython_genutils +# ----------------------------------------------------------------------------- +def _chunks(l, n): + """Yield successive n-sized chunks from l.""" + for i in range(0, len(l), n): + yield l[i : i + n] + + +def _find_optimal(rlist, *, separator_size, displaywidth): + """Calculate optimal info to columnize a list of strings""" + for nrow in range(1, len(rlist) + 1): + chk = list(map(max, _chunks(rlist, nrow))) + sumlength = sum(chk) + ncols = len(chk) + if sumlength + separator_size * (ncols - 1) <= displaywidth: + break + + return { + "columns_numbers": ncols, + "rows_numbers": nrow, + "columns_width": chk, + } + + +def _get_or_default(mylist, i, *, default): + """return list item number, or default if don't exist""" + if i >= len(mylist): + return default + else: + return mylist[i] + + +def compute_item_matrix(items, empty=None, *, separator_size=2, displaywith=80): + """Returns a nested list, and info to columnize items + + Parameters + ---------- + items + list of strings to columnize + empty : (default None) + Default value to fill list if needed + separator_size : int (default=2) + How much characters will be used as a separation between each column. + displaywidth : int (default=80) + The width of the area onto which the columns should enter + + Returns + ------- + + strings_matrix + + nested list of strings, the outer most list contains as many list as + rows, the innermost lists have each as many element as column. If the + total number of elements in `items` does not equal the product of + rows*columns, the last element of some lists are filled with `None`. + + dict_info + Some info to make columnize easier: + + columns_numbers + number of columns + rows_numbers + number of rows + columns_width + list of width of each columns + + Examples + -------- + :: + + In [1]: l = ['aaa','b','cc','d','eeeee','f','g','h','i','j','k','l'] + ...: compute_item_matrix(l,displaywidth=12) + Out[1]: + ([['aaa', 'f', 'k'], + ['b', 'g', 'l'], + ['cc', 'h', None], + ['d', 'i', None], + ['eeeee', 'j', None]], + {'columns_numbers': 3, + 'columns_width': [5, 1, 1], + 'rows_numbers': 5}) + """ + info = _find_optimal( + [len(it) for it in items], separator_size=separator_size, displaywidth=displaywidth + ) + nrow, ncol = info["rows_numbers"], info["columns_numbers"] + return ( + [ + [_get_or_default(items, c * nrow + i, default=empty) for c in range(ncol)] + for i in range(nrow) + ], + info, + ) + + +def columnize(items): + """Transform a list of strings into a single string with columns. + + Parameters + ---------- + items : sequence of strings + The strings to process. + + Returns + ------- + The formatted string. + """ + separator = " " + displaywidth = 80 + if not items: + return "\n" + matrix, info = compute_item_matrix( + items, separator_size=len(separator), displaywidth=displaywidth + ) + fmatrix = [filter(None, x) for x in matrix] + sjoin = lambda x: separator.join( + [y.ljust(w, " ") for y, w in zip(x, info["columns_width"])] + ) + return "\n".join(map(sjoin, fmatrix)) + "\n" + + +def import_item(name): + """Import and return ``bar`` given the string ``foo.bar``. + + Calling ``bar = import_item("foo.bar")`` is the functional equivalent of + executing the code ``from foo import bar``. + + Parameters + ---------- + name : string + The fully qualified name of the module/package being imported. + + Returns + ------- + mod : module object + The module that was imported. + """ + parts = name.rsplit(".", 1) + + if len(parts) == 2: + # called with 'foo.bar....' + package, obj = parts + module = __import__(package, fromlist=[obj]) + try: + pak = getattr(module, obj) + except AttributeError: + raise ImportError("No module named %s" % obj) + return pak + else: + # called with un-dotted string + return __import__(parts[0]) diff --git a/requirements/environment.yml b/requirements/environment.yml index 17a3b8aa..a3d7a5dc 100644 --- a/requirements/environment.yml +++ b/requirements/environment.yml @@ -5,7 +5,6 @@ dependencies: - pyqt - qtpy >=2.0.1 - traitlets -- ipython_genutils - jupyter_core - jupyter_client - pygments diff --git a/setup.py b/setup.py index aa6a1a8e..59f5f0db 100644 --- a/setup.py +++ b/setup.py @@ -67,7 +67,6 @@ python_requires = '>= 3.7', install_requires = [ 'traitlets!=5.2.1,!=5.2.2', - 'ipython_genutils', 'jupyter_core', 'jupyter_client>=4.1', 'pygments',