diff options
55 files changed, 639 insertions, 288 deletions
diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..57420f2 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,5 @@ +[run] +omit = + .tox/* + setup.py + diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 0000000..af0bd27 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1,3 @@ +service_name: travis-pro +repo_token: lNMvPpeiiKc20j7oGjScYtGKUfVseah6S + diff --git a/.travis.yml b/.travis.yml index d19ac11..3fa964e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,26 +1,26 @@ language: python -python: - - "2.6" - - "2.7_with_system_site_packages" - - "3.2_with_system_site_packages" - - "3.3" - - "3.4" - - "3.5" - - "pypy" +matrix: + include: + - python: 2.7 + env: TOXENV=py27 + - python: 3.3 + env: TOXENV=py33 + - python: 3.4 + env: TOXENV=py34 + - python: 3.5 + env: TOXENV=py35 + - python: 3.6 + env: TOXENV=py36 + - python: pypy + env: TOXENV=pypy before_install: - "sudo apt-get update" - "sudo apt-get install python-gi python3-gi" install: - - "pip install setuptools" - - if [[ $TRAVIS_PYTHON_VERSION == 2.6 ]]; then pip install "twisted<=15.4"; fi - - "[ $TRAVIS_PYTHON_VERSION == '3.2_with_system_site_packages' ] && pip install 'tornado<=4.3.0' || pip install twisted tornado" + - "pip install tox coverage coveralls" script: - - "DEPS=\"$TRAVIS_PYTHON_VERSION `python bin/deps.py`\"; echo $DEPS" - - "[ \"$DEPS\" == '2.6 tornado twisted' -o - \"$DEPS\" == '2.7_with_system_site_packages pygobject tornado twisted' -o - \"$DEPS\" == '3.2_with_system_site_packages pygobject tornado' -o - \"$DEPS\" == '3.3 tornado twisted' -o - \"$DEPS\" == '3.4 tornado twisted' -o - \"$DEPS\" == '3.5 tornado twisted' -o - \"$DEPS\" == 'pypy tornado twisted' ]" - - "python setup.py test" + - "tox" + - "coverage report" +after_success: + - "coveralls" + diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..e10d7ac --- /dev/null +++ b/ISSUE_TEMPLATE.md @@ -0,0 +1,14 @@ +##### Description: +[...] + +##### Affected versions (if applicable) +- [ ] `master` branch (specify commit) +- [ ] Latest stable version from `pypi` +- [ ] Other (specify source) + +##### Steps to reproduce (if applicable) +[...] + +##### Expected/actual outcome +[...] + diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..d2b1df3 --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,11 @@ +##### Checklist +- [ ] I've ensured that similar functionality has not already been implemented +- [ ] I've ensured that similar functionality has not earlier been proposed and declined +- [ ] I've branched off the `master` or `python-dual-support` branch +- [ ] I've merged fresh upstream into my branch recently +- [ ] I've ran `tox` successfully in local environment +- [ ] I've included docstrings and/or documentation and/or examples for my code (if this is a new feature) + +##### Description: +*(P. S. If this pull requests fixes an existing issue, please specify which one.)* + @@ -2,10 +2,19 @@ :alt: build status :target: https://travis-ci.org/urwid/urwid/ +.. image:: https://coveralls.io/repos/github/urwid/urwid/badge.svg + :alt: build coverage + :target: https://coveralls.io/github/urwid/urwid + `Development version documentation <http://urwid.readthedocs.org/en/latest/>`_ +**Urwid is looking for new maintainers, please open an issue here to volunteer!** + .. content-start +About +===== + Urwid is a console user interface library for Python. It includes many features useful for text console application developers including: @@ -22,3 +31,18 @@ It includes many features useful for text console application developers includi Home Page: http://urwid.org/ + +Testing +======= + +To run tests locally, install & run `tox`. You must have +appropriate Python versions installed to run `tox` for +each of them. + +To test code in all Python versions: + +.. code:: bash + + tox # Test all versions specified in tox.ini: + tox -e py36 # Test Python 3.6 only + tox -e py27,py36,pypy # Test Python 2.7, Python 3.6 & pypy diff --git a/docs/changelog.rst b/docs/changelog.rst index 676a5d3..b79ee44 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -279,7 +279,7 @@ Urwid 1.0.1 * Fix for a LineBox border __init__() parameters - * Fix input input of UTF-8 in tour.py example by converting captions + * Fix input of UTF-8 in tour.py example by converting captions to unicode * Fix tutorial examples' use of TextCanvas and switch to using @@ -307,7 +307,7 @@ Urwid 1.0.0 * New experimental Terminal widget with xterm emulation and terminal.py example program (by aszlig) - * Edit widget now supports a mask (for passwords), has a + * Edit widget now supports a mask (for passwords), has an insert_text_result() method for full-field validation and normalizes input text to Unicode or bytes based on the caption type used @@ -566,7 +566,7 @@ Urwid 0.9.8 without blocking. * The Columns, Pile and ListBox widgets now choose their first - selectable child widget as the focus widget by defaut. + selectable child widget as the focus widget by default. * New ListWalker base class for list walker classes. @@ -593,7 +593,7 @@ Urwid 0.9.8 * The raw_display module now uses an alternate buffer so that the original screen can be restored on exit. The old behaviour is - available by seting the alternate_buffer parameter of start() or + available by setting the alternate_buffer parameter of start() or run_wrapper() to False. * Many internal string processing functions have been rewritten in C to @@ -611,7 +611,7 @@ Urwid 0.9.7.2 * Fixed a UTF-8 input bug. - * Added a clear() function to the the display modules to force the + * Added a clear() function to the display modules to force the screen to be repainted on the next draw_screen() call. @@ -1048,7 +1048,7 @@ Urwid 0.8.6 register_palette() and register_palette_entry() now accept "default" as foreground and/or background. If the terminal's default attributes - cannot be detected black on light gray will be used to accomodate + cannot be detected black on light gray will be used to accommodate terminals with always-black cursors. "default" is now the default for text with no attributes. This means diff --git a/docs/manual/canvascache.rst b/docs/manual/canvascache.rst index bb5f094..a2d0bd4 100644 --- a/docs/manual/canvascache.rst +++ b/docs/manual/canvascache.rst @@ -53,7 +53,7 @@ will remain in the cache, and others will be garbage collected. Future Work =========== -A updating method that invalidates regions of the display without redrawing +An updating method that invalidates regions of the display without redrawing parent widgets would be more efficient for the common case of a single change on the screen that does not affect the screen layout. Send an email to the mailing list if you're interested in helping with this or other display diff --git a/docs/manual/displayattributes.rst b/docs/manual/displayattributes.rst index 44b7753..4f800f6 100644 --- a/docs/manual/displayattributes.rst +++ b/docs/manual/displayattributes.rst @@ -159,6 +159,21 @@ Foreground and Background Settings - YES - standout - widely supported + * - :ref:`italics <bold-underline-standout>` + - YES + - YES + - NO + - widely supported + * - :ref:`blink <bold-underline-standout>` + - YES/NO + - NO + - NO + - some support + * - :ref:`strikethrough <bold-underline-standout>` + - YES + - NO + - NO + - some supported * - :ref:`"bright" background colors <bright-background>` - YES - urxvt @@ -239,9 +254,12 @@ Bold, Underline, Standout * ``'bold'`` * ``'underline'`` * ``'standout'`` +* ``'blink'`` +* ``'italics'`` +* ``'strikethrough'`` These settings may be tagged on to foreground colors using commas, eg: ``'light -gray,underline,bold'`` +gray,underline,bold,strikethrough'`` For monochrome mode combinations of these are the only values that may be used. diff --git a/docs/manual/overview.rst b/docs/manual/overview.rst index 9360147..a9e953b 100644 --- a/docs/manual/overview.rst +++ b/docs/manual/overview.rst @@ -35,7 +35,7 @@ you can also write your own text layout classes. Urwid supports a range of common :ref:`display attributes <display-attributes>`, including 256-color foreground and background settings, -bold, underline and standount settings for displaying text. Not all of these +bold, underline and standout settings for displaying text. Not all of these are supported by all terminals, so Urwid helps you write applications that support different color modes depending on what the user's terminal supports and what they choose to enable. diff --git a/docs/manual/widgets.rst b/docs/manual/widgets.rst index 44fe7d3..9fabb97 100644 --- a/docs/manual/widgets.rst +++ b/docs/manual/widgets.rst @@ -150,7 +150,7 @@ return ``None``. container.focus_position is a read/write property that provides access to the position of the -container's widget in focus. This will often be a integer value but may be +container's widget in focus. This will often be an integer value but may be any object. :class:`Columns`, :class:`Pile`, :class:`GridFlow`, :class:`Overlay` and :class:`ListBox` with a :class:`SimpleListWalker` or :class:`SimpleFocusListWalker` @@ -237,7 +237,7 @@ and proceeding along each child widget until reaching a leaf (non-container) widget. Note that the list does not contain the topmost container widget -(i.e, on which this method is called), but does include the +(i.e., on which this method is called), but does include the lowest leaf widget. :: diff --git a/docs/tutorial/index.rst b/docs/tutorial/index.rst index 1a84bb6..b4a6553 100644 --- a/docs/tutorial/index.rst +++ b/docs/tutorial/index.rst @@ -151,7 +151,7 @@ a widget before the correct one has been created. assigning to its :attr:`MainLoop.widget` property. * :ref:`decoration-widgets` like :class:`AttrMap` have an - ``original_widget`` property that we can assign to to change the widget they wrap. + ``original_widget`` property that we can assign to change the widget they wrap. * :class:`Divider` widgets are used to create blank lines, colored with :class:`AttrMap`. @@ -317,9 +317,9 @@ return to previous menus by pressing *ESC*. :linenos: * *menu_button()* returns an :class:`AttrMap`-decorated :class:`Button` - and attaches a *callback* to the the its ``'click'`` signal. This function is + and attaches a *callback* to its ``'click'`` signal. This function is used for both sub-menus and final selection buttons. -* *sub_menu()* creates a menu button and a closure that will open the the +* *sub_menu()* creates a menu button and a closure that will open the menu when that button is clicked. Notice that :ref:`text markup <text-markup>` is used to add ``'...'`` to the end of the *caption* passed to *menu_button()*. diff --git a/examples/browse.py b/examples/browse.py index d5a5f16..ab689c0 100755 --- a/examples/browse.py +++ b/examples/browse.py @@ -31,6 +31,8 @@ Features: - outputs a quoted list of files and directories "selected" on exit """ +from __future__ import print_function + import itertools import re import os @@ -184,7 +186,7 @@ class DirectoryNode(urwid.ParentNode): dirs.append(a) else: files.append(a) - except OSError, e: + except OSError as e: depth = self.get_depth() + 1 self._children[None] = ErrorNode(self, parent=self, key=None, depth=depth) @@ -274,7 +276,7 @@ class DirectoryBrowser: # on exit, write the flagged filenames to the console names = [escape_filename_sh(x) for x in get_flagged_names()] - print " ".join(names) + print(" ".join(names)) def unhandled_input(self, k): # update display of focus directory diff --git a/examples/calc.py b/examples/calc.py index 2ac324a..e56be4a 100755 --- a/examples/calc.py +++ b/examples/calc.py @@ -31,6 +31,8 @@ Features: - outputs commands that may be used to recreate expression on exit """ +from __future__ import print_function + import urwid import urwid.raw_display import urwid.web_display @@ -60,7 +62,7 @@ OPERATORS = { COLUMN_KEYS = list( "?ABCDEF" ) # these lists are used to determine when to display errors -EDIT_KEYS = OPERATORS.keys() + COLUMN_KEYS + ['backspace','delete'] +EDIT_KEYS = list(OPERATORS.keys()) + COLUMN_KEYS + ['backspace','delete'] MOVEMENT_KEYS = ['up','down','left','right','page up','page down'] # Event text @@ -144,7 +146,7 @@ class Cell: if self.child is not None: return self.child.get_result() else: - return long("0"+self.edit.edit_text) + return int("0"+self.edit.edit_text) def get_result(self): """Return the numeric result of this cell's operation.""" @@ -153,7 +155,7 @@ class Cell: return self.get_value() if self.result.text == "": return None - return long(self.result.text) + return int(self.result.text) def set_result(self, result): """Set the numeric result for this cell.""" @@ -212,7 +214,7 @@ class ParentEdit(urwid.Edit): if key == "backspace": raise ColumnDeleteEvent(self.letter, from_parent=True) elif key in list("0123456789"): - raise CalcEvent, E_invalid_in_parent_cell + raise CalcEvent(E_invalid_in_parent_cell) else: return key @@ -344,7 +346,7 @@ class CellColumn( urwid.WidgetWrap ): # cell above is parent if edit.edit_text: # ..and current not empty, no good - raise CalcEvent, E_cant_combine + raise CalcEvent(E_cant_combine) above_pos = 0 else: # above is normal number cell @@ -366,13 +368,13 @@ class CellColumn( urwid.WidgetWrap ): below = self.walker.get_cell(i+1) if cell.child is not None: # this cell is a parent - raise CalcEvent, E_cant_combine + raise CalcEvent(E_cant_combine) if below is None: # nothing below return key if below.child is not None: # cell below is a parent - raise CalcEvent, E_cant_combine + raise CalcEvent(E_cant_combine) edit = self.walker.get_cell(i).edit edit.set_edit_text( edit.edit_text + @@ -453,9 +455,9 @@ class CellColumn( urwid.WidgetWrap ): cell = self.walker.get_cell(i) if cell.child is not None: - raise CalcEvent, E_new_col_cell_not_empty + raise CalcEvent(E_new_col_cell_not_empty) if cell.edit.edit_text: - raise CalcEvent, E_new_col_cell_not_empty + raise CalcEvent(E_new_col_cell_not_empty) child = CellColumn( letter ) cell.become_parent( child, letter ) @@ -579,9 +581,9 @@ class CalcDisplay: # on exit write the formula and the result to the console expression, result = self.get_expression_result() - print "Paste this expression into a new Column Calculator session to continue editing:" - print expression - print "Result:", result + print( "Paste this expression into a new Column Calculator session to continue editing:") + print( expression) + print( "Result:", result) def input_filter(self, input, raw_input): if 'q' in input or 'Q' in input: @@ -593,7 +595,7 @@ class CalcDisplay: self.wrap_keypress(k) self.event = None self.view.footer = None - except CalcEvent, e: + except CalcEvent as e: # display any message self.event = e self.view.footer = e.widget() @@ -607,7 +609,7 @@ class CalcDisplay: try: key = self.keypress(key) - except ColumnDeleteEvent, e: + except ColumnDeleteEvent as e: if e.letter == COLUMN_KEYS[1]: # cannot delete the first column, ignore key return @@ -619,7 +621,7 @@ class CalcDisplay: raise e self.delete_column(e.letter) - except UpdateParentEvent, e: + except UpdateParentEvent as e: self.update_parent_columns() return @@ -628,10 +630,10 @@ class CalcDisplay: if self.columns.get_focus_column() == 0: if key not in ('up','down','page up','page down'): - raise CalcEvent, E_invalid_in_help_col + raise CalcEvent(E_invalid_in_help_col) if key not in EDIT_KEYS and key not in MOVEMENT_KEYS: - raise CalcEvent, E_invalid_key % key.upper() + raise CalcEvent(E_invalid_key % key.upper()) def keypress(self, key): """Handle a keystroke.""" @@ -642,13 +644,13 @@ class CalcDisplay: # column switch i = COLUMN_KEYS.index(key.upper()) if i >= len( self.col_list ): - raise CalcEvent, E_no_such_column % key.upper() + raise CalcEvent(E_no_such_column % key.upper()) self.columns.set_focus_column( i ) return elif key == "(": # open a new column if len( self.col_list ) >= len(COLUMN_KEYS): - raise CalcEvent, E_no_more_columns + raise CalcEvent(E_no_more_columns) i = self.columns.get_focus_column() if i == 0: # makes no sense in help column @@ -672,7 +674,7 @@ class CalcDisplay: parent, pcol = self.get_parent( col ) if parent is None: # column has no parent - raise CalcEvent, E_no_parent_column + raise CalcEvent(E_no_parent_column) new_i = self.col_list.index( pcol ) self.columns.set_focus_column( new_i ) diff --git a/examples/dialog.py b/examples/dialog.py index 7f3a4d5..1328e79 100755 --- a/examples/dialog.py +++ b/examples/dialog.py @@ -100,7 +100,7 @@ class DialogDisplay: self.loop = urwid.MainLoop(self.view, self.palette) try: self.loop.run() - except DialogExit, e: + except DialogExit as e: return self.on_exit( e.args[0] ) def on_exit(self, exitcode): @@ -230,12 +230,12 @@ class MenuItem(urwid.Text): def keypress(self,size,key): if key == "enter": self.state = True - raise DialogExit, 0 + raise DialogExit(0) return key def mouse_event(self,size,event,button,col,row,focus): if event=='mouse release': self.state = True - raise DialogExit, 0 + raise DialogExit(0) return False def get_state(self): return self.state diff --git a/examples/fib.py b/examples/fib.py index e3262b4..ad6acc5 100755 --- a/examples/fib.py +++ b/examples/fib.py @@ -35,7 +35,7 @@ class FibonacciWalker(urwid.ListWalker): positions returned are (value at position-1, value at position) tuples. """ def __init__(self): - self.focus = (0L,1L) + self.focus = (0,1) self.numeric_layout = NumericLayout() def _get_at_pos(self, pos): diff --git a/examples/graph.py b/examples/graph.py index c21c9a9..536fa00 100755 --- a/examples/graph.py +++ b/examples/graph.py @@ -48,7 +48,7 @@ class GraphModel: data_max_value = 100 def __init__(self): - data = [ ('Saw', range(0,100,2)*2), + data = [ ('Saw', list(range(0,100,2))*2), ('Square', [0]*30 + [100]*30), ('Sine 1', [sin100(x) for x in range(100)] ), ('Sine 2', [(sin100(x) + sin100(x*2))/2 diff --git a/examples/subproc.py b/examples/subproc.py index 64eb072..4c7e918 100755 --- a/examples/subproc.py +++ b/examples/subproc.py @@ -21,7 +21,7 @@ def exit_on_enter(key): loop = urwid.MainLoop(frame_widget, unhandled_input=exit_on_enter) def received_output(data): - output_widget.set_text(output_widget.text + data) + output_widget.set_text(output_widget.text + data.decode('utf8')) write_fd = loop.watch_pipe(received_output) proc = subprocess.Popen( diff --git a/examples/subproc2.py b/examples/subproc2.py index 79c73b2..c40a647 100644 --- a/examples/subproc2.py +++ b/examples/subproc2.py @@ -1,8 +1,15 @@ # this is part of the subproc.py example +from __future__ import print_function + import sys +try: + range = xrange +except NameError: + pass + num = int(sys.argv[1]) -for c in xrange(1,10000000): +for c in range(1,10000000): if num % c == 0: - print "factor:", c + print("factor:", c) diff --git a/examples/twisted_serve_ssh.py b/examples/twisted_serve_ssh.py index aad63f9..c1a0004 100644 --- a/examples/twisted_serve_ssh.py +++ b/examples/twisted_serve_ssh.py @@ -31,6 +31,8 @@ Portions Copyright: 2010, Ian Ward <ian@excess.org> Licence: LGPL <http://opensource.org/licenses/lgpl-2.1.php> """ +from __future__ import print_function + import os import urwid @@ -198,7 +200,7 @@ class TwistedScreen(Screen): """ return self.terminalProtocol.width, self.terminalProtocol.height - def draw_screen(self, (maxcol, maxrow), r ): + def draw_screen(self, maxres, r ): """Render a canvas to the terminal. The canvas contains all the information required to render the Urwid @@ -206,6 +208,7 @@ class TwistedScreen(Screen): tuples. This very simple implementation iterates each row and simply writes it out. """ + (maxcol, maxrow) = maxres #self.terminal.eraseDisplay() lasta = None for i, row in enumerate(r.content()): @@ -409,9 +412,10 @@ class UrwidTerminalSession(TerminalSession): IConchUser(self.original), self.height, self.width) - def windowChanged(self, (h, w, x, y)): + def windowChanged(self, dimensions): """Called when the window size has changed. """ + (h, w, x, y) = dimensions self.chained_protocol.terminalProtocol.terminalSize(h, w) @@ -70,6 +70,7 @@ setup_d = { "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: PyPy", ], } @@ -78,9 +79,6 @@ if have_setuptools: setup_d['zip_safe'] = False setup_d['test_suite'] = 'urwid.tests' -if PYTHON3: - setup_d['use_2to3'] = True - if __name__ == "__main__": try: setup(**setup_d) @@ -0,0 +1,24 @@ +[tox] +envlist = + py27 + py33 + py34 + py35 + py36 + pypy + +[testenv] +usedevelop = true +deps = + setuptools + tornado + coverage + py27: twisted==16.6.0 + py33: twisted + py34: twisted + py35: twisted + py36: twisted + pypy: twisted +commands = + coverage run ./setup.py test + diff --git a/urwid/__init__.py b/urwid/__init__.py index bc5170e..32484de 100644 --- a/urwid/__init__.py +++ b/urwid/__init__.py @@ -20,6 +20,8 @@ # # Urwid web site: http://excess.org/urwid/ +from __future__ import division, print_function + from urwid.version import VERSION, __version__ from urwid.widget import (FLOW, BOX, FIXED, LEFT, RIGHT, CENTER, TOP, MIDDLE, BOTTOM, SPACE, ANY, CLIP, PACK, GIVEN, RELATIVE, RELATIVE_100, WEIGHT, diff --git a/urwid/canvas.py b/urwid/canvas.py index 4a51d3e..207ee21 100644 --- a/urwid/canvas.py +++ b/urwid/canvas.py @@ -19,6 +19,8 @@ # # Urwid web site: http://excess.org/urwid/ +from __future__ import division, print_function + import weakref from urwid.util import rle_len, rle_append_modify, rle_join_modify, rle_product, \ @@ -828,7 +830,7 @@ def shard_body_row(sbody): row = [] for done_rows, content_iter, cview in sbody: if content_iter: - row.extend(content_iter.next()) + row.extend(next(content_iter)) else: # need to skip this unchanged canvas if row and type(row[-1]) == int: @@ -867,10 +869,10 @@ def shards_delta(shards, other_shards): done = other_done = 0 for num_rows, cviews in shards: if other_num_rows is None: - other_num_rows, other_cviews = other_shards_iter.next() + other_num_rows, other_cviews = next(other_shards_iter) while other_done < done: other_done += other_num_rows - other_num_rows, other_cviews = other_shards_iter.next() + other_num_rows, other_cviews = next(other_shards_iter) if other_done > done: yield (num_rows, cviews) done += num_rows @@ -889,10 +891,10 @@ def shard_cviews_delta(cviews, other_cviews): cols = other_cols = 0 for cv in cviews: if other_cv is None: - other_cv = other_cviews_iter.next() + other_cv = next(other_cviews_iter) while other_cols < cols: other_cols += other_cv[2] - other_cv = other_cviews_iter.next() + other_cv = next(other_cviews_iter) if other_cols > cols: yield cv cols += cv[2] @@ -926,7 +928,7 @@ def shard_body(cviews, shard_tail, create_iter=True, iter_default=None): for col_gap, done_rows, content_iter, tail_cview in shard_tail: while col_gap: try: - cview = cviews_iter.next() + cview = next(cviews_iter) except StopIteration: raise CanvasError("cviews do not fill gaps in" " shard_tail!") @@ -1057,7 +1059,7 @@ def shards_join(shard_lists): All shards lists must have the same number of rows. """ shards_iters = [iter(sl) for sl in shard_lists] - shards_current = [i.next() for i in shards_iters] + shards_current = [next(i) for i in shards_iters] new_shards = [] while True: @@ -1078,7 +1080,7 @@ def shards_join(shard_lists): for i in range(len(shards_current)): if shards_current[i][0] > 0: continue - shards_current[i] = shards_iters[i].next() + shards_current[i] = next(shards_iters[i]) except StopIteration: break return new_shards diff --git a/urwid/command_map.py b/urwid/command_map.py index 15633f8..e6965f7 100644 --- a/urwid/command_map.py +++ b/urwid/command_map.py @@ -19,6 +19,8 @@ # # Urwid web site: http://excess.org/urwid/ +from __future__ import division, print_function + REDRAW_SCREEN = 'redraw screen' CURSOR_UP = 'cursor up' CURSOR_DOWN = 'cursor down' diff --git a/urwid/compat.py b/urwid/compat.py index 686d703..de0bf7a 100644 --- a/urwid/compat.py +++ b/urwid/compat.py @@ -20,6 +20,8 @@ # # Urwid web site: http://excess.org/urwid/ +from __future__ import division, print_function + import sys try: # python 2.4 and 2.5 compat @@ -39,10 +41,34 @@ if PYTHON3: chr2 = lambda x: bytes([x]) B = lambda x: x.encode('iso8859-1') bytes3 = bytes + text_type = str + xrange = range + text_types = (str,) else: ord2 = ord chr2 = chr B = lambda x: x bytes3 = lambda x: bytes().join([chr(c) for c in x]) + text_type = unicode + xrange = xrange + text_types = (str, unicode) + + +def with_metaclass(meta, *bases): + """ + Create a base class with a metaclass. + Taken from "six" library (https://pythonhosted.org/six/). + """ + # This requires a bit of explanation: the basic idea is to make a dummy + # metaclass for one level of class instantiation that replaces itself with + # the actual metaclass. + class metaclass(type): + + def __new__(cls, name, this_bases, d): + return meta(name, bases, d) + @classmethod + def __prepare__(cls, name, this_bases): + return meta.__prepare__(name, bases) + return type.__new__(metaclass, 'temporary_class', (), {}) diff --git a/urwid/container.py b/urwid/container.py index fdd5da4..c2cc71f 100755 --- a/urwid/container.py +++ b/urwid/container.py @@ -19,7 +19,10 @@ # # Urwid web site: http://excess.org/urwid/ +from __future__ import division, print_function + from itertools import chain, repeat +from urwid.compat import xrange from urwid.util import is_mouse_press from urwid.widget import (Widget, Divider, FLOW, FIXED, PACK, BOX, WidgetWrap, @@ -92,7 +95,7 @@ class WidgetContainerMixin(object): (non-container) widget. Note that the list does not contain the topmost container widget - (i.e, on which this method is called), but does include the + (i.e., on which this method is called), but does include the lowest leaf widget. """ out = [] @@ -1589,9 +1592,9 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): return key if self._command_map[key] == 'cursor up': - candidates = range(i-1, -1, -1) # count backwards to 0 + candidates = list(range(i-1, -1, -1)) # count backwards to 0 else: # self._command_map[key] == 'cursor down' - candidates = range(i+1, len(self.contents)) + candidates = list(range(i+1, len(self.contents))) if not item_rows: item_rows = self.get_item_rows(size, focus=True) @@ -1607,9 +1610,9 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): rows = item_rows[j] if self._command_map[key] == 'cursor up': - rowlist = range(rows-1, -1, -1) + rowlist = list(range(rows-1, -1, -1)) else: # self._command_map[key] == 'cursor down' - rowlist = range(rows) + rowlist = list(range(rows)) for row in rowlist: tsize = self.get_item_size(size, j, True, item_rows) if self.focus_item.move_cursor_to_coords( @@ -1718,7 +1721,7 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): is an int (``'pack'``, *widget*) call :meth:`pack() <Widget.pack>` to calculate the width of this column - (``'weight'``, *weight*, *widget*)` + (``'weight'``, *weight*, *widget*) give this column a relative *weight* (number) to calculate its width from the screen columns remaining @@ -2272,9 +2275,9 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): return key if self._command_map[key] == 'cursor left': - candidates = range(i-1, -1, -1) # count backwards to 0 + candidates = list(range(i-1, -1, -1)) # count backwards to 0 else: # key == 'right' - candidates = range(i+1, len(self.contents)) + candidates = list(range(i+1, len(self.contents))) for j in candidates: if not self.contents[j][0].selectable(): diff --git a/urwid/curses_display.py b/urwid/curses_display.py index 441042e..0aaa2f1 100755 --- a/urwid/curses_display.py +++ b/urwid/curses_display.py @@ -19,6 +19,8 @@ # # Urwid web site: http://excess.org/urwid/ +from __future__ import division, print_function + """ Curses-based UI implementation """ @@ -30,7 +32,7 @@ from urwid import escape from urwid.display_common import BaseScreen, RealTerminal, AttrSpec, \ UNPRINTABLE_TRANS_TABLE -from urwid.compat import bytes, PYTHON3 +from urwid.compat import bytes, PYTHON3, text_type, xrange KEY_RESIZE = 410 # curses.KEY_RESIZE (sometimes not defined) KEY_MOUSE = 409 # curses.KEY_MOUSE @@ -481,10 +483,12 @@ class Screen(BaseScreen, RealTerminal): self.s.attrset(attr) - def draw_screen(self, (cols, rows), r ): + def draw_screen(self, size, r ): """Paint screen with rendered canvas.""" assert self._started + cols, rows = size + assert r.rows() == rows, "canvas size and passed size don't match" y = -1 @@ -558,7 +562,7 @@ class Screen(BaseScreen, RealTerminal): class _test: def __init__(self): self.ui = Screen() - self.l = _curses_colours.keys() + self.l = list(_curses_colours.keys()) self.l.sort() for c in self.l: self.ui.register_palette( [ @@ -602,7 +606,7 @@ class _test: t = "" a = [] for k in keys: - if type(k) == unicode: k = k.encode("utf-8") + if type(k) == text_type: k = k.encode("utf-8") t += "'"+k + "' " a += [(None,1), ('yellow on dark blue',len(k)), (None,2)] diff --git a/urwid/decoration.py b/urwid/decoration.py index 731eb91..9c18028 100755 --- a/urwid/decoration.py +++ b/urwid/decoration.py @@ -19,6 +19,7 @@ # # Urwid web site: http://excess.org/urwid/ +from __future__ import division, print_function from urwid.util import int_scale from urwid.widget import (Widget, WidgetError, @@ -129,14 +130,14 @@ class AttrMap(delegate_to_widget_mixin('_original_widget'), WidgetDecoration): <AttrMap selectable flow widget <Edit selectable flow widget '' edit_pos=0> attr_map={None: 'notfocus'} focus_map={None: 'focus'}> >>> size = (5,) >>> am = AttrMap(Text(u"hi"), 'greeting', 'fgreet') - >>> am.render(size, focus=False).content().next() # ... = b in Python 3 + >>> next(am.render(size, focus=False).content()) # ... = b in Python 3 [('greeting', None, ...'hi ')] - >>> am.render(size, focus=True).content().next() + >>> next(am.render(size, focus=True).content()) [('fgreet', None, ...'hi ')] >>> am2 = AttrMap(Text(('word', u"hi")), {'word':'greeting', None:'bg'}) >>> am2 <AttrMap flow widget <Text flow widget 'hi'> attr_map={'word': 'greeting', None: 'bg'}> - >>> am2.render(size).content().next() + >>> next(am2.render(size).content()) [('greeting', None, ...'hi'), ('bg', None, ...' ')] """ self.__super.__init__(w) @@ -247,9 +248,9 @@ class AttrWrap(AttrMap): <AttrWrap selectable flow widget <Edit selectable flow widget '' edit_pos=0> attr='notfocus' focus_attr='focus'> >>> size = (5,) >>> aw = AttrWrap(Text(u"hi"), 'greeting', 'fgreet') - >>> aw.render(size, focus=False).content().next() + >>> next(aw.render(size, focus=False).content()) [('greeting', None, ...'hi ')] - >>> aw.render(size, focus=True).content().next() + >>> next(aw.render(size, focus=True).content()) [('fgreet', None, ...'hi ')] """ self.__super.__init__(w, attr, focus_attr) @@ -460,7 +461,7 @@ class Padding(WidgetDecoration): >>> size = (7,) >>> def pr(w): ... for t in w.render(size).text: - ... print "|%s|" % (t.decode('ascii'),) + ... print("|%s|" % (t.decode('ascii'),)) >>> pr(Padding(Text(u"Head"), ('relative', 20), 'pack')) | Head | >>> pr(Padding(Divider(u"-"), left=2, right=1)) @@ -731,14 +732,14 @@ class Filler(WidgetDecoration): if isinstance(height, tuple): if height[0] == 'fixed top': if not isinstance(valign, tuple) or valign[0] != 'fixed bottom': - raise FillerError("fixed bottom height may only be used " - "with fixed top valign") + raise FillerError("fixed top height may only be used " + "with fixed bottom valign") top = height[1] height = RELATIVE_100 elif height[0] == 'fixed bottom': if not isinstance(valign, tuple) or valign[0] != 'fixed top': - raise FillerError("fixed top height may only be used " - "with fixed bottom valign") + raise FillerError("fixed bottom height may only be used " + "with fixed top valign") bottom = height[1] height = RELATIVE_100 if isinstance(valign, tuple): diff --git a/urwid/display_common.py b/urwid/display_common.py index 867691d..e447682 100755 --- a/urwid/display_common.py +++ b/urwid/display_common.py @@ -18,6 +18,8 @@ # # Urwid web site: http://excess.org/urwid/ +from __future__ import division, print_function + import os import sys @@ -28,10 +30,10 @@ except ImportError: from urwid.util import StoppingContext, int_scale from urwid import signals -from urwid.compat import B, bytes3 +from urwid.compat import B, bytes3, xrange, with_metaclass # for replacing unprintable bytes with '?' -UNPRINTABLE_TRANS_TABLE = B("?") * 32 + bytes3(range(32,256)) +UNPRINTABLE_TRANS_TABLE = B("?") * 32 + bytes3(list(xrange(32,256))) # signals sent by BaseScreen @@ -91,8 +93,9 @@ _UNDERLINE = 0x04000000 _BOLD = 0x08000000 _BLINK = 0x10000000 _ITALICS = 0x20000000 +_STRIKETHROUGH = 0x40000000 _FG_MASK = (_FG_COLOR_MASK | _FG_BASIC_COLOR | _FG_HIGH_COLOR | - _STANDOUT | _UNDERLINE | _BLINK | _BOLD | _ITALICS) + _STANDOUT | _UNDERLINE | _BLINK | _BOLD | _ITALICS | _STRIKETHROUGH) _BG_MASK = _BG_COLOR_MASK | _BG_BASIC_COLOR | _BG_HIGH_COLOR DEFAULT = 'default' @@ -138,6 +141,7 @@ _ATTRIBUTES = { 'underline': _UNDERLINE, 'blink': _BLINK, 'standout': _STANDOUT, + 'strikethrough': _STRIKETHROUGH, } def _value_lookup_table(values, size): @@ -452,7 +456,8 @@ class AttrSpec(object): 'h8' (color number 8), 'h255' (color number 255) Setting: - 'bold', 'italics', 'underline', 'blink', 'standout' + 'bold', 'italics', 'underline', 'blink', 'standout', + 'strikethrough' Some terminals use 'bold' for bright colors. Most terminals ignore the 'blink' setting. If the color is not given then @@ -507,6 +512,7 @@ class AttrSpec(object): underline = property(lambda s: s._value & _UNDERLINE != 0) blink = property(lambda s: s._value & _BLINK != 0) standout = property(lambda s: s._value & _STANDOUT != 0) + strikethrough = property(lambda s: s._value & _STRIKETHROUGH != 0) def _colors(self): """ @@ -548,7 +554,7 @@ class AttrSpec(object): return (self._foreground_color() + ',bold' * self.bold + ',italics' * self.italics + ',standout' * self.standout + ',blink' * self.blink + - ',underline' * self.underline) + ',underline' * self.underline + ',strikethrough' * self.strikethrough) def _set_foreground(self, foreground): color = None @@ -715,11 +721,10 @@ class RealTerminal(object): class ScreenError(Exception): pass -class BaseScreen(object): +class BaseScreen(with_metaclass(signals.MetaSignals, object)): """ Base class for Screen classes (raw_display.Screen, .. etc) """ - __metaclass__ = signals.MetaSignals signals = [UPDATE_PALETTE_ENTRY, INPUT_DESCRIPTORS_CHANGED] def __init__(self): @@ -814,7 +819,7 @@ class BaseScreen(object): 'light magenta', 'light cyan', 'white' Settings: - 'bold', 'underline', 'blink', 'standout' + 'bold', 'underline', 'blink', 'standout', 'strikethrough' Some terminals use 'bold' for bright colors. Most terminals ignore the 'blink' setting. If the color is not given then diff --git a/urwid/escape.py b/urwid/escape.py index 683466c..b047fb0 100644 --- a/urwid/escape.py +++ b/urwid/escape.py @@ -20,6 +20,8 @@ # # Urwid web site: http://excess.org/urwid/ +from __future__ import division, print_function + """ Terminal Escape Sequences for input and display """ diff --git a/urwid/font.py b/urwid/font.py index bf0c2b1..e7bfe6e 100755 --- a/urwid/font.py +++ b/urwid/font.py @@ -20,9 +20,12 @@ # # Urwid web site: http://excess.org/urwid/ +from __future__ import division, print_function + from urwid.escape import SAFE_ASCII_DEC_SPECIAL_RE from urwid.util import apply_target_encoding, str_util from urwid.canvas import TextCanvas +from urwid.compat import text_type def separate_glyphs(gdata, height): @@ -96,9 +99,16 @@ class Font(object): self.char = {} self.canvas = {} self.utf8_required = False - for gdata in self.data: + data = [self._to_text(block) for block in self.data] + for gdata in data: self.add_glyphs(gdata) + @staticmethod + def _to_text(obj, encoding='utf-8', errors='strict'): + if isinstance(obj, text_type): + return obj + elif isinstance(obj, bytes): + return obj.decode(encoding, errors) def add_glyphs(self, gdata): d, utf8_required = separate_glyphs(gdata, self.height) @@ -106,7 +116,7 @@ class Font(object): self.utf8_required |= utf8_required def characters(self): - l = self.char.keys() + l = list(self.char.keys()) l.sort() return "".join(l) @@ -147,7 +157,7 @@ class Thin3x3Font(Font): ┌─┐ ┐ ┌─┐┌─┐ ┐┌─ ┌─ ┌─┐┌─┐┌─┐ │ │ │ │ ┌─┘ ─┤└─┼└─┐├─┐ ┼├─┤└─┤ │ └─┘ ┴ └─ └─┘ ┴ ─┘└─┘ ┴└─┘ ─┘ . -""", ur""" +""", r""" "###$$$%%%'*++,--.///:;==???[[\\\]]^__` " ┼┼┌┼┐O /' /.. _┌─┐┌ \ ┐^ ` ┼┼└┼┐ / * ┼ ─ / ., _ ┌┘│ \ │ @@ -179,7 +189,7 @@ class HalfBlock5x4Font(Font): ▀█▀█▀ ▀▄█▄ █ ▀▄▀ ▐▌ ▐▌ ▄▄█▄▄ ▄▄█▄▄ ▄▄▄▄ █ ▀ ▀ ▀█▀█▀ ▄ █ █ ▐▌▄ █ ▀▄▌▐▌ ▐▌ ▄▀▄ █ ▐▌ ▀ ▄▀ ▀ ▀ ▀▀▀ ▀ ▀ ▀▀ ▀ ▀ ▄▀ ▀ ▀ -''', ur""" +''', r""" <<<<<=====>>>>>?????@@@@@@[[[[\\\\]]]]^^^^____```{{{{||}}}}~~~~''´´´ ▄▀ ▀▄ ▄▀▀▄ ▄▀▀▀▄ █▀▀ ▐▌ ▀▀█ ▄▀▄ ▀▄ ▄▀ █ ▀▄ ▄ █ ▄▀ ▄▀ ▀▀▀▀ ▀▄ ▄▀ █ █▀█ █ █ █ ▄▀ █ ▀▄ ▐▐▌▌ @@ -258,7 +268,7 @@ class Thin6x6Font(Font): │ │ │ │ │ │ │ │ │ │ │ │ │ └───┘ ┴ └─── └───┘ ┴ ───┘ └───┘ ┴ └───┘ ───┘ -""", ur''' +""", r''' !! """######$$$$$$%%%%%%&&&&&&((()))******++++++ │ ││ ┌ ┌ ┌─┼─┐ ┌┐ / ┌─┐ / \ │ ─┼─┼─ │ │ └┘ / │ │ │ │ \ / │ @@ -266,7 +276,7 @@ class Thin6x6Font(Font): │ ─┼─┼─ │ │ / ┌┐ │ \, │ │ / \ │ . ┘ ┘ └─┼─┘ / └┘ └───\ \ / -''', ur""" +''', r""" ,,-----..//////::;;<<<<=====>>>>??????@@@@@@ / ┌───┐ ┌───┐ / . . / ──── \ │ │┌──┤ @@ -274,7 +284,7 @@ class Thin6x6Font(Font): / . , \ ──── / │ │└──┘ , . / \ / . └───┘ -""", ur""" +""", r""" [[\\\\\\]]^^^____``{{||}}~~~~~~ ┌ \ ┐ /\ \ ┌ │ ┐ │ \ │ │ │ │ ┌─┐ @@ -363,7 +373,7 @@ class HalfBlock7x7Font(Font): █▌ ▀ ▀█▌ ▐█▀ ▐█ ▀▀▀ ▐█ ▐█ ▐█ █▌ ▀███▀ ▀ -""", ur""" +""", r""" [[[[\\\\\]]]]^^^^^^^_____```{{{{{|||}}}}}~~~~~~~´´´ ▐██▌▐█ ▐██▌ ▐█▌ ▐█ █▌▐█ ▐█ █▌ ▐█ █▌ █▌ ▐█ █▌ █▌ █▌ ▐█ ▐█ ▄▄ ▐█ @@ -433,18 +443,18 @@ add_font("Half Block 7x7",HalfBlock7x7Font) if __name__ == "__main__": l = get_all_fonts() all_ascii = "".join([chr(x) for x in range(32, 127)]) - print "Available Fonts: (U) = UTF-8 required" - print "----------------" + print("Available Fonts: (U) = UTF-8 required") + print("----------------") for n,cls in l: f = cls() u = "" if f.utf8_required: u = "(U)" - print ("%-20s %3s " % (n,u)), + print(("%-20s %3s " % (n,u)), end=' ') c = f.characters() if c == all_ascii: - print "Full ASCII" + print("Full ASCII") elif c.startswith(all_ascii): - print "Full ASCII + " + c[len(all_ascii):] + print("Full ASCII + " + c[len(all_ascii):]) else: - print "Characters: " + c + print("Characters: " + c) diff --git a/urwid/graphics.py b/urwid/graphics.py index cd03d33..25cd2dd 100755 --- a/urwid/graphics.py +++ b/urwid/graphics.py @@ -20,6 +20,9 @@ # # Urwid web site: http://excess.org/urwid/ +from __future__ import division, print_function + +from urwid.compat import with_metaclass from urwid.util import decompose_tagmarkup, get_encoding_mode from urwid.canvas import CompositeCanvas, CanvasJoin, TextCanvas, \ CanvasCombine, SolidCanvas @@ -96,7 +99,7 @@ class BigText(Widget): class LineBox(WidgetDecoration, WidgetWrap): - def __init__(self, original_widget, title="", + def __init__(self, original_widget, title="", title_align="center", tlcorner=u'┌', tline=u'─', lline=u'│', trcorner=u'┐', blcorner=u'└', rline=u'│', bline=u'─', brcorner=u'┘'): @@ -106,6 +109,9 @@ class LineBox(WidgetDecoration, WidgetWrap): Use 'title' to set an initial title text with will be centered on top of the box. + Use `title_align` to align the title to the 'left', 'right', or 'center'. + The default is 'center'. + You can also override the widgets used for the lines/corners: tline: top line bline: bottom line @@ -116,37 +122,77 @@ class LineBox(WidgetDecoration, WidgetWrap): blcorner: bottom left corner brcorner: bottom right corner + If empty string is specified for one of the lines/corners, then no + character will be output there. This allows for seamless use of + adjoining LineBoxes. """ - tline, bline = Divider(tline), Divider(bline) - lline, rline = SolidFill(lline), SolidFill(rline) + if tline: + tline = Divider(tline) + if bline: + bline = Divider(bline) + if lline: + lline = SolidFill(lline) + if rline: + rline = SolidFill(rline) tlcorner, trcorner = Text(tlcorner), Text(trcorner) blcorner, brcorner = Text(blcorner), Text(brcorner) + if not tline and title: + raise ValueError('Cannot have a title when tline is empty string') + self.title_widget = Text(self.format_title(title)) - self.tline_widget = Columns([ - tline, - ('flow', self.title_widget), - tline, - ]) - - top = Columns([ - ('fixed', 1, tlcorner), - self.tline_widget, - ('fixed', 1, trcorner) - ]) - - middle = Columns([ - ('fixed', 1, lline), - original_widget, - ('fixed', 1, rline), - ], box_columns=[0, 2], focus_column=1) - - bottom = Columns([ - ('fixed', 1, blcorner), bline, ('fixed', 1, brcorner) - ]) - - pile = Pile([('flow', top), middle, ('flow', bottom)], focus_item=1) + + if tline: + if title_align not in ('left', 'center', 'right'): + raise ValueError('title_align must be one of "left", "right", or "center"') + if title_align == 'left': + tline_widgets = [('flow', self.title_widget), tline] + else: + tline_widgets = [tline, ('flow', self.title_widget)] + if title_align == 'center': + tline_widgets.append(tline) + self.tline_widget = Columns(tline_widgets) + top = Columns([ + ('fixed', 1, tlcorner), + self.tline_widget, + ('fixed', 1, trcorner) + ]) + + else: + self.tline_widget = None + top = None + + middle_widgets = [] + if lline: + middle_widgets.append(('fixed', 1, lline)) + else: + # Note: We need to define a fixed first widget (even if it's 0 width) so that the other + # widgets have something to anchor onto + middle_widgets.append(('fixed', 0, SolidFill(u""))) + middle_widgets.append(original_widget) + focus_col = len(middle_widgets) - 1 + if rline: + middle_widgets.append(('fixed', 1, rline)) + + middle = Columns(middle_widgets, + box_columns=[0, 2], focus_column=focus_col) + + if bline: + bottom = Columns([ + ('fixed', 1, blcorner), bline, ('fixed', 1, brcorner) + ]) + else: + bottom = None + + pile_widgets = [] + if top: + pile_widgets.append(('flow', top)) + pile_widgets.append(middle) + focus_pos = len(pile_widgets) - 1 + if bottom: + pile_widgets.append(('flow', bottom)) + pile = Pile(pile_widgets, focus_item=focus_pos) WidgetDecoration.__init__(self, original_widget) WidgetWrap.__init__(self, pile) @@ -158,6 +204,8 @@ class LineBox(WidgetDecoration, WidgetWrap): return "" def set_title(self, text): + if not self.title_widget: + raise ValueError('Cannot set title when tline is unset') self.title_widget.set_text(self.format_title(text)) self.tline_widget._invalidate() @@ -192,9 +240,7 @@ def nocache_bargraph_get_data(self, get_data_fn): class BarGraphError(Exception): pass -class BarGraph(Widget): - __metaclass__ = BarGraphMeta - +class BarGraph(with_metaclass(BarGraphMeta, Widget)): _sizing = frozenset([BOX]) ignore_focus = True @@ -481,7 +527,8 @@ class BarGraph(Widget): o = [] r = 0 # row remainder - def seg_combine((bt1, w1), (bt2, w2)): + def seg_combine(a, b): + (bt1, w1), (bt2, w2) = a, b if (bt1, w1) == (bt2, w2): return (bt1, w1), None, None wmin = min(w1, w2) @@ -811,6 +858,28 @@ class ProgressBar(Widget): foreground of satt corresponds to the normal part and the background corresponds to the complete part. If satt is ``None`` then no smoothing will be done. + + >>> pb = ProgressBar('a', 'b') + >>> pb + <ProgressBar flow widget> + >>> print(pb.get_text()) + 0 % + >>> pb.set_completion(34.42) + >>> print(pb.get_text()) + 34 % + >>> class CustomProgressBar(ProgressBar): + ... def get_text(self): + ... return u'Foobar' + >>> cpb = CustomProgressBar('a', 'b') + >>> print(cpb.get_text()) + Foobar + >>> for x in range(101): + ... cpb.set_completion(x) + ... s = cpb.render((10, )) + >>> cpb2 = CustomProgressBar('a', 'b', satt='c') + >>> for x in range(101): + ... cpb2.set_completion(x) + ... s = cpb2.render((10, )) """ self.normal = normal self.complete = complete @@ -840,6 +909,7 @@ class ProgressBar(Widget): def get_text(self): """ Return the progress bar percentage text. + You can override this method to display custom text. """ percent = min(100, max(0, int(self.current * 100 / self.done))) return str(percent) + " %" @@ -853,7 +923,12 @@ class ProgressBar(Widget): c = txt.render((maxcol,)) cf = float(self.current) * maxcol / self.done - ccol = int(cf) + ccol_dirty = int(cf) + ccol = len(c._text[0][:ccol_dirty].decode( + 'utf-8', 'ignore' + ).encode( + 'utf-8' + )) cs = 0 if self.satt is not None: cs = int((cf - ccol) * 8) @@ -909,3 +984,10 @@ class PythonLogo(Widget): """ fixed_size(size) return self._canvas + +def _test(): + import doctest + doctest.testmod() + +if __name__=='__main__': + _test() diff --git a/urwid/highlight.css b/urwid/highlight.css new file mode 100644 index 0000000..097663d --- /dev/null +++ b/urwid/highlight.css @@ -0,0 +1,19 @@ +/* Style definition file generated by highlight 3.41, http://www.andre-simon.de/ */ +/* highlight theme: Kwrite Editor */ +body.hl { background-color:#e0eaee; } +pre.hl { color:#000000; background-color:#e0eaee; font-size:10pt; font-family:'Courier New',monospace;} +.hl.num { color:#b07e00; } +.hl.esc { color:#ff00ff; } +.hl.str { color:#bf0303; } +.hl.pps { color:#818100; } +.hl.slc { color:#838183; font-style:italic; } +.hl.com { color:#838183; font-style:italic; } +.hl.ppc { color:#008200; } +.hl.opt { color:#000000; } +.hl.ipl { color:#0057ae; } +.hl.lin { color:#555555; } +.hl.kwa { color:#000000; font-weight:bold; } +.hl.kwb { color:#0057ae; } +.hl.kwc { color:#000000; font-weight:bold; } +.hl.kwd { color:#010181; } + diff --git a/urwid/html_fragment.py b/urwid/html_fragment.py index 3db1fd9..5df9273 100755 --- a/urwid/html_fragment.py +++ b/urwid/html_fragment.py @@ -19,6 +19,8 @@ # # Urwid web site: http://excess.org/urwid/ +from __future__ import division, print_function + """ HTML PRE-based UI implementation """ @@ -76,13 +78,15 @@ class HtmlGenerator(BaseScreen): def reset_default_terminal_palette(self, *args): pass - def draw_screen(self, (cols, rows), r ): + def draw_screen(self, size, r ): """Create an html fragment from the render object. Append it to HtmlGenerator.fragments list. """ # collect output in l l = [] + cols, rows = size + assert r.rows() == rows if r.cursor is not None: @@ -134,7 +138,7 @@ class HtmlGenerator(BaseScreen): def get_cols_rows(self): """Return the next screen size in HtmlGenerator.sizes.""" if not self.sizes: - raise HtmlGeneratorSimulationError, "Ran out of screen sizes to return!" + raise HtmlGeneratorSimulationError("Ran out of screen sizes to return!") return self.sizes.pop(0) def get_input(self, raw_keys=False): @@ -219,7 +223,7 @@ def screenshot_init( sizes, keys ): assert type(row) == int assert row>0 and col>0 except (AssertionError, ValueError): - raise Exception, "sizes must be in the form [ (col1,row1), (col2,row2), ...]" + raise Exception("sizes must be in the form [ (col1,row1), (col2,row2), ...]") try: for l in keys: @@ -227,11 +231,11 @@ def screenshot_init( sizes, keys ): for k in l: assert type(k) == str except (AssertionError, ValueError): - raise Exception, "keys must be in the form [ [keyA1, keyA2, ..], [keyB1, ..], ...]" + raise Exception("keys must be in the form [ [keyA1, keyA2, ..], [keyB1, ..], ...]") - import curses_display + from . import curses_display curses_display.Screen = HtmlGenerator - import raw_display + from . import raw_display raw_display.Screen = HtmlGenerator HtmlGenerator.sizes = sizes diff --git a/urwid/lcd_display.py b/urwid/lcd_display.py index 4f62173..e189d9a 100644 --- a/urwid/lcd_display.py +++ b/urwid/lcd_display.py @@ -20,8 +20,9 @@ # # Urwid web site: http://excess.org/urwid/ +from __future__ import division, print_function -from display_common import BaseScreen +from .display_common import BaseScreen import time @@ -39,7 +40,7 @@ class LCDScreen(BaseScreen): def reset_default_terminal_palette(self, *args): pass - def draw_screen(self, (cols, rows), r ): + def draw_screen(self, size, r ): pass def clear(self): diff --git a/urwid/listbox.py b/urwid/listbox.py index 72ce2d5..802b1d6 100644 --- a/urwid/listbox.py +++ b/urwid/listbox.py @@ -19,6 +19,9 @@ # # Urwid web site: http://excess.org/urwid/ +from __future__ import division, print_function + +from urwid.compat import xrange, with_metaclass from urwid.util import is_mouse_press from urwid.canvas import SolidCanvas, CanvasCombine from urwid.widget import Widget, nocache_widget_render_instance, BOX, GIVEN @@ -28,14 +31,12 @@ from urwid.signals import connect_signal from urwid.monitored_list import MonitoredList, MonitoredFocusList from urwid.container import WidgetContainerMixin from urwid.command_map import (CURSOR_UP, CURSOR_DOWN, - CURSOR_PAGE_UP, CURSOR_PAGE_DOWN) + CURSOR_PAGE_UP, CURSOR_PAGE_DOWN, CURSOR_MAX_LEFT, CURSOR_MAX_RIGHT) class ListWalkerError(Exception): pass -class ListWalker(object): - __metaclass__ = signals.MetaSignals - +class ListWalker(with_metaclass(signals.MetaSignals, object)): signals = ["modified"] def _modified(self): @@ -141,7 +142,7 @@ class SimpleListWalker(MonitoredList, ListWalker): this list walker to be updated. """ if not getattr(contents, '__getitem__', None): - raise ListWalkerError, "SimpleListWalker expecting list like object, got: %r"%(contents,) + raise ListWalkerError("SimpleListWalker expecting list like object, got: %r"%(contents,)) MonitoredList.__init__(self, contents) self.focus = 0 @@ -175,7 +176,7 @@ class SimpleListWalker(MonitoredList, ListWalker): if position < 0 or position >= len(self): raise ValueError except (TypeError, ValueError): - raise IndexError, "No widget at position %s" % (position,) + raise IndexError("No widget at position %s" % (position,)) self.focus = position self._modified() @@ -235,6 +236,7 @@ class SimpleFocusListWalker(ListWalker, MonitoredFocusList): def set_focus(self, position): """Set focus position.""" self.focus = position + self._modified() def next_position(self, position): """ @@ -313,7 +315,7 @@ class ListBox(Widget, WidgetContainerMixin): if getattr(body, 'get_focus', None): self._body = body else: - self._body = PollingListWalker(body) + self._body = SimpleListWalker(body) self._invalidate() body = property(_get_body, _set_body, doc=""" @@ -480,17 +482,17 @@ class ListBox(Widget, WidgetContainerMixin): for widget,w_pos,w_rows in fill_above: canvas = widget.render((maxcol,)) if w_rows != canvas.rows(): - raise ListBoxError, "Widget %r at position %r within listbox calculated %d rows but rendered %d!"% (widget,w_pos,w_rows, canvas.rows()) + raise ListBoxError("Widget %r at position %r within listbox calculated %d rows but rendered %d!"% (widget,w_pos,w_rows, canvas.rows())) rows += w_rows combinelist.append((canvas, w_pos, False)) focus_canvas = focus_widget.render((maxcol,), focus=focus) if focus_canvas.rows() != focus_rows: - raise ListBoxError, "Focus Widget %r at position %r within listbox calculated %d rows but rendered %d!"% (focus_widget,focus_pos,focus_rows, focus_canvas.rows()) + raise ListBoxError("Focus Widget %r at position %r within listbox calculated %d rows but rendered %d!"% (focus_widget,focus_pos,focus_rows, focus_canvas.rows())) c_cursor = focus_canvas.cursor if cursor != c_cursor: - raise ListBoxError, "Focus Widget %r at position %r within listbox calculated cursor coords %r but rendered cursor coords %r!" %(focus_widget,focus_pos,cursor,c_cursor) + raise ListBoxError("Focus Widget %r at position %r within listbox calculated cursor coords %r but rendered cursor coords %r!" %(focus_widget,focus_pos,cursor,c_cursor)) rows += focus_rows combinelist.append((focus_canvas, focus_pos, True)) @@ -498,7 +500,7 @@ class ListBox(Widget, WidgetContainerMixin): for widget,w_pos,w_rows in fill_below: canvas = widget.render((maxcol,)) if w_rows != canvas.rows(): - raise ListBoxError, "Widget %r at position %r within listbox calculated %d rows but rendered %d!"% (widget,w_pos,w_rows, canvas.rows()) + raise ListBoxError("Widget %r at position %r within listbox calculated %d rows but rendered %d!"% (widget,w_pos,w_rows, canvas.rows())) rows += w_rows combinelist.append((canvas, w_pos, False)) @@ -512,13 +514,13 @@ class ListBox(Widget, WidgetContainerMixin): rows -= trim_bottom if rows > maxrow: - raise ListBoxError, "Listbox contents too long! Probably urwid's fault (please report): %r" % ((top,middle,bottom),) + raise ListBoxError("Listbox contents too long! Probably urwid's fault (please report): %r" % ((top,middle,bottom),)) if rows < maxrow: bottom_pos = focus_pos if fill_below: bottom_pos = fill_below[-1][1] if trim_bottom != 0 or self._body.get_next(bottom_pos) != (None,None): - raise ListBoxError, "Listbox contents too short! Probably urwid's fault (please report): %r" % ((top,middle,bottom),) + raise ListBoxError("Listbox contents too short! Probably urwid's fault (please report): %r" % ((top,middle,bottom),)) final_canvas.pad_trim_top_bottom(0, maxrow - rows) return final_canvas @@ -602,7 +604,7 @@ class ListBox(Widget, WidgetContainerMixin): """ w, pos = self._body.get_focus() if w is None: - raise IndexError, "No focus_position, ListBox is empty" + raise IndexError("No focus_position, ListBox is empty") return pos focus_position = property(_get_focus_position, set_focus, doc=""" the position of child widget in focus. The valid values for this @@ -784,14 +786,14 @@ class ListBox(Widget, WidgetContainerMixin): if offset_inset >= 0: if offset_inset >= maxrow: - raise ListBoxError, "Invalid offset_inset: %r, only %r rows in list box"% (offset_inset, maxrow) + raise ListBoxError("Invalid offset_inset: %r, only %r rows in list box"% (offset_inset, maxrow)) self.offset_rows = offset_inset self.inset_fraction = (0,1) else: target, _ignore = self._body.get_focus() tgt_rows = target.rows( (maxcol,), True ) if offset_inset + tgt_rows <= 0: - raise ListBoxError, "Invalid offset_inset: %r, only %r rows in target!" %(offset_inset, tgt_rows) + raise ListBoxError("Invalid offset_inset: %r, only %r rows in target!" %(offset_inset, tgt_rows)) self.offset_rows = 0 self.inset_fraction = (-offset_inset,tgt_rows) self._invalidate() @@ -888,7 +890,7 @@ class ListBox(Widget, WidgetContainerMixin): self.inset_fraction = (0,1) else: if offset_inset + tgt_rows <= 0: - raise ListBoxError, "Invalid offset_inset: %s, only %s rows in target!" %(offset_inset, tgt_rows) + raise ListBoxError("Invalid offset_inset: %s, only %s rows in target!" %(offset_inset, tgt_rows)) self.offset_rows = 0 self.inset_fraction = (-offset_inset,tgt_rows) @@ -916,7 +918,7 @@ class ListBox(Widget, WidgetContainerMixin): # start from preferred row and move back to closest edge (pref_col, pref_row) = cursor_coords if pref_row < 0 or pref_row >= tgt_rows: - raise ListBoxError, "cursor_coords row outside valid range for target. pref_row:%r target_rows:%r"%(pref_row,tgt_rows) + raise ListBoxError("cursor_coords row outside valid range for target. pref_row:%r target_rows:%r"%(pref_row,tgt_rows)) if coming_from=='above': attempt_rows = range( pref_row, -1, -1 ) @@ -939,10 +941,10 @@ class ListBox(Widget, WidgetContainerMixin): if offset_rows == 0: inum, iden = self.inset_fraction if inum < 0 or iden < 0 or inum >= iden: - raise ListBoxError, "Invalid inset_fraction: %r"%(self.inset_fraction,) + raise ListBoxError("Invalid inset_fraction: %r"%(self.inset_fraction,)) inset_rows = focus_rows * inum // iden if inset_rows and inset_rows >= focus_rows: - raise ListBoxError, "urwid inset_fraction error (please report)" + raise ListBoxError("urwid inset_fraction error (please report)") return offset_rows, inset_rows @@ -975,15 +977,14 @@ class ListBox(Widget, WidgetContainerMixin): def keypress(self, size, key): """Move selection through the list elements scrolling when - necessary. 'up' and 'down' are first passed to widget in focus - in case that widget can handle them. 'page up' and 'page down' - are always handled by the ListBox. + necessary. Keystrokes are first passed to widget in focus + in case that widget can handle them. Keystrokes handled by this widget are: 'up' up one line (or widget) 'down' down one line (or widget) - 'page up' move cursor up one listbox length - 'page down' move cursor down one listbox length + 'page up' move cursor up one listbox length (or widget) + 'page down' move cursor down one listbox length (or widget) """ (maxcol, maxrow) = size @@ -994,12 +995,11 @@ class ListBox(Widget, WidgetContainerMixin): if focus_widget is None: # empty listbox, can't do anything return key - if self._command_map[key] not in [CURSOR_PAGE_UP, CURSOR_PAGE_DOWN]: - if focus_widget.selectable(): - key = focus_widget.keypress((maxcol,),key) + if focus_widget.selectable(): + key = focus_widget.keypress((maxcol,),key) if key is None: self.make_cursor_visible((maxcol,maxrow)) - return + return None def actual_key(unhandled): if unhandled: @@ -1018,8 +1018,23 @@ class ListBox(Widget, WidgetContainerMixin): if self._command_map[key] == CURSOR_PAGE_DOWN: return actual_key(self._keypress_page_down((maxcol, maxrow))) + if self._command_map[key] == CURSOR_MAX_LEFT: + return actual_key(self._keypress_max_left()) + + if self._command_map[key] == CURSOR_MAX_RIGHT: + return actual_key(self._keypress_max_right()) + return key + def _keypress_max_left(self): + self.focus_position = next(iter(self.body.positions())) + self.set_focus_valign('top') + return True + + def _keypress_max_right(self): + self.focus_position = next(iter(self.body.positions(reverse=True))) + self.set_focus_valign('bottom') + return True def _keypress_up(self, size): (maxcol, maxrow) = size @@ -1258,8 +1273,8 @@ class ListBox(Widget, WidgetContainerMixin): # choose the topmost selectable and (newly) visible widget # search within snap_rows then visible region - search_order = ( range( snap_region_start, len(t)) - + range( snap_region_start-1, -1, -1 ) ) + search_order = (list(xrange(snap_region_start, len(t))) + + list(xrange(snap_region_start-1, -1, -1))) #assert 0, repr((t, search_order)) bad_choices = [] cut_off_selectable_chosen = 0 @@ -1443,8 +1458,8 @@ class ListBox(Widget, WidgetContainerMixin): # choose the bottommost selectable and (newly) visible widget # search within snap_rows then visible region - search_order = ( range( snap_region_start, len(t)) - + range( snap_region_start-1, -1, -1 ) ) + search_order = (list(xrange(snap_region_start, len(t))) + + list(xrange(snap_region_start-1, -1, -1))) #assert 0, repr((t, search_order)) bad_choices = [] cut_off_selectable_chosen = 0 diff --git a/urwid/main_loop.py b/urwid/main_loop.py index 28577b2..442c27d 100755 --- a/urwid/main_loop.py +++ b/urwid/main_loop.py @@ -21,12 +21,14 @@ # # Urwid web site: http://excess.org/urwid/ +from __future__ import division, print_function import time import heapq import select import os from functools import wraps +from itertools import count from weakref import WeakKeyDictionary try: @@ -442,7 +444,7 @@ class MainLoop(object): sec = next_alarm[0] - time.time() if sec > 0: break - tm, callback = next_alarm + tm, tie_break, callback = next_alarm callback() if self.event_loop._alarms: @@ -589,6 +591,7 @@ class SelectEventLoop(object): self._watch_files = {} self._idle_handle = 0 self._idle_callbacks = {} + self._tie_break = count() def alarm(self, seconds, callback): """ @@ -601,8 +604,9 @@ class SelectEventLoop(object): callback -- function to call from event loop """ tm = time.time() + seconds - heapq.heappush(self._alarms, (tm, callback)) - return (tm, callback) + handle = (tm, next(self._tie_break), callback) + heapq.heappush(self._alarms, handle) + return handle def remove_alarm(self, handle): """ @@ -691,7 +695,7 @@ class SelectEventLoop(object): """ A single iteration of the event loop """ - fds = self._watch_files.keys() + fds = list(self._watch_files.keys()) if self._alarms or self._did_something: if self._alarms: tm = self._alarms[0][0] @@ -711,7 +715,7 @@ class SelectEventLoop(object): self._did_something = False elif tm is not None: # must have been a timeout - tm, alarm_callback = self._alarms.pop(0) + tm, tie_break, alarm_callback = heapq.heappop(self._alarms) alarm_callback() self._did_something = True @@ -848,7 +852,7 @@ class GLibEventLoop(object): # An exception caused us to exit, raise it now exc_info = self._exc_info self._exc_info = None - raise exc_info[0], exc_info[1], exc_info[2] + raise exc_info[0](exc_info[1]).with_traceback(exc_info[2]) def handle_exit(self,f): """ @@ -1183,7 +1187,7 @@ class TwistedEventLoop(object): # An exception caused us to exit, raise it now exc_info = self._exc_info self._exc_info = None - raise exc_info[0], exc_info[1], exc_info[2] + raise exc_info[0](exc_info[1]).with_traceback(exc_info[2]) def handle_exit(self, f, enable_idle=True): """ @@ -1203,7 +1207,7 @@ class TwistedEventLoop(object): self.reactor.stop() except: import sys - print sys.exc_info() + print(sys.exc_info()) self._exc_info = sys.exc_info() if self.manage_reactor: self.reactor.crash() @@ -1325,7 +1329,7 @@ class AsyncioEventLoop(object): self._loop.set_exception_handler(self._exception_handler) self._loop.run_forever() if self._exc_info: - raise self._exc_info[0], self._exc_info[1], self._exc_info[2] + raise self._exc_info[0](self._exc_info[1]).with_traceback(self._exc_info[2]) self._exc_info = None @@ -1354,7 +1358,7 @@ def _refl(name, rval=None, exit=False): if args and argd: args = args + ", " args = args + ", ".join([k+"="+repr(v) for k,v in argd.items()]) - print self._name+"("+args+")" + print(self._name+"("+args+")") if exit: raise ExitMainLoop() return self._rval diff --git a/urwid/monitored_list.py b/urwid/monitored_list.py index dc67c84..c159064 100755 --- a/urwid/monitored_list.py +++ b/urwid/monitored_list.py @@ -19,7 +19,9 @@ # # Urwid web site: http://excess.org/urwid/ -from urwid.compat import PYTHON3 +from __future__ import division, print_function + +from urwid.compat import PYTHON3, xrange def _call_modified(fn): @@ -238,7 +240,7 @@ class MonitoredFocusList(MonitoredList): """ num_new_items = len(new_items) start, stop, step = indices = slc.indices(len(self)) - num_removed = len(range(*indices)) + num_removed = len(list(xrange(*indices))) focus = self._validate_contents_modified(indices, new_items) if focus is not None: @@ -255,11 +257,11 @@ class MonitoredFocusList(MonitoredList): else: if not num_new_items: # extended slice being removed - if focus in range(start, stop, step): + if focus in xrange(start, stop, step): focus += 1 # adjust for removed items - focus -= len(range(start, min(focus, stop), step)) + focus -= len(list(xrange(start, min(focus, stop), step))) return min(focus, len(self) + num_new_items - num_removed -1) @@ -303,7 +305,7 @@ class MonitoredFocusList(MonitoredList): def __setitem__(self, i, y): """ >>> def modified(indices, new_items): - ... print "range%r <- %r" % (indices, new_items) + ... print("range%r <- %r" % (indices, new_items)) >>> ml = MonitoredFocusList([0,1,2,3], focus=2) >>> ml.set_validate_contents_modified(modified) >>> ml[0] = 9 @@ -347,7 +349,7 @@ class MonitoredFocusList(MonitoredList): def __imul__(self, n): """ >>> def modified(indices, new_items): - ... print "range%r <- %r" % (indices, list(new_items)) + ... print("range%r <- %r" % (indices, list(new_items))) >>> ml = MonitoredFocusList([0,1,2], focus=2) >>> ml.set_validate_contents_modified(modified) >>> ml *= 3 @@ -356,7 +358,7 @@ class MonitoredFocusList(MonitoredList): MonitoredFocusList([0, 1, 2, 0, 1, 2, 0, 1, 2], focus=2) >>> ml *= 0 range(0, 9, 1) <- [] - >>> print ml.focus + >>> print(ml.focus) None """ if n > 0: @@ -371,7 +373,7 @@ class MonitoredFocusList(MonitoredList): def append(self, item): """ >>> def modified(indices, new_items): - ... print "range%r <- %r" % (indices, new_items) + ... print("range%r <- %r" % (indices, new_items)) >>> ml = MonitoredFocusList([0,1,2], focus=2) >>> ml.set_validate_contents_modified(modified) >>> ml.append(6) @@ -386,7 +388,7 @@ class MonitoredFocusList(MonitoredList): def extend(self, items): """ >>> def modified(indices, new_items): - ... print "range%r <- %r" % (indices, list(new_items)) + ... print("range%r <- %r" % (indices, list(new_items))) >>> ml = MonitoredFocusList([0,1,2], focus=2) >>> ml.set_validate_contents_modified(modified) >>> ml.extend((6,7,8)) diff --git a/urwid/old_str_util.py b/urwid/old_str_util.py index 83190f5..2c6d1e0 100755 --- a/urwid/old_str_util.py +++ b/urwid/old_str_util.py @@ -19,11 +19,12 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Urwid web site: http://excess.org/urwid/ -from __future__ import print_function + +from __future__ import division, print_function import re -from urwid.compat import bytes, B, ord2 +from urwid.compat import bytes, B, ord2, text_type SAFE_ASCII_RE = re.compile(u"^[ -~]*$") SAFE_ASCII_BYTES_RE = re.compile(B("^[ -~]*$")) @@ -241,7 +242,7 @@ def is_wide_char(text, offs): text may be unicode or a byte string in the target _byte_encoding """ - if isinstance(text, unicode): + if isinstance(text, text_type): o = ord(text[offs]) return get_width(o) == 2 assert isinstance(text, bytes) @@ -257,7 +258,7 @@ def move_prev_char(text, start_offs, end_offs): Return the position of the character before end_offs. """ assert start_offs < end_offs - if isinstance(text, unicode): + if isinstance(text, text_type): return end_offs-1 assert isinstance(text, bytes) if _byte_encoding == "utf8": @@ -275,7 +276,7 @@ def move_next_char(text, start_offs, end_offs): Return the position of the character after start_offs. """ assert start_offs < end_offs - if isinstance(text, unicode): + if isinstance(text, text_type): return start_offs+1 assert isinstance(text, bytes) if _byte_encoding == "utf8": diff --git a/urwid/raw_display.py b/urwid/raw_display.py index e48e4b0..b0482b1 100644 --- a/urwid/raw_display.py +++ b/urwid/raw_display.py @@ -19,6 +19,8 @@ # # Urwid web site: http://excess.org/urwid/ +from __future__ import division, print_function + """ Direct terminal UI implementation """ @@ -663,8 +665,11 @@ class Screen(BaseScreen, RealTerminal): self._setup_G1_done = True - def draw_screen(self, (maxcol, maxrow), r ): + def draw_screen(self, maxres, r ): """Paint screen with rendered canvas.""" + + (maxcol, maxrow) = maxres + assert self._started assert maxrow == r.rows() @@ -832,7 +837,7 @@ class Screen(BaseScreen, RealTerminal): try: for l in o: if isinstance(l, bytes) and PYTHON3: - l = l.decode('utf-8') + l = l.decode('utf-8', 'replace') self.write(l) self.flush() except IOError as e: @@ -931,7 +936,7 @@ class Screen(BaseScreen, RealTerminal): fg = "39" st = ("1;" * a.bold + "3;" * a.italics + "4;" * a.underline + "5;" * a.blink + - "7;" * a.standout) + "7;" * a.standout + "9;" * a.strikethrough) if a.background_high: bg = "48;5;%d" % a.background_number elif a.background_basic: diff --git a/urwid/signals.py b/urwid/signals.py index 9e93597..0269dfd 100644 --- a/urwid/signals.py +++ b/urwid/signals.py @@ -19,6 +19,7 @@ # # Urwid web site: http://excess.org/urwid/ +from __future__ import division, print_function import itertools import weakref @@ -33,7 +34,7 @@ class MetaSignals(type): signals = d.get("signals", []) for superclass in cls.__bases__: signals.extend(getattr(superclass, 'signals', [])) - signals = dict([(x,None) for x in signals]).keys() + signals = list(dict([(x,None) for x in signals]).keys()) d["signals"] = signals register_signal(cls, signals) super(MetaSignals, cls).__init__(name, bases, d) diff --git a/urwid/split_repr.py b/urwid/split_repr.py index fb108b5..3d7cbeb 100755 --- a/urwid/split_repr.py +++ b/urwid/split_repr.py @@ -19,6 +19,8 @@ # # Urwid web site: http://excess.org/urwid/ +from __future__ import division, print_function + from inspect import getargspec from urwid.compat import PYTHON3, bytes @@ -129,12 +131,12 @@ def remove_defaults(d, fn): del args[-1] # create a dictionary of args with default values - ddict = dict(zip(args[len(args) - len(defaults):], defaults)) + ddict = dict(list(zip(args[len(args) - len(defaults):], defaults))) - for k, v in d.items(): + for k in list(d.keys()): if k in ddict: # remove values that match their defaults - if ddict[k] == v: + if ddict[k] == d[k]: del d[k] return d diff --git a/urwid/tests/test_doctests.py b/urwid/tests/test_doctests.py index 1720a48..611baf3 100644 --- a/urwid/tests/test_doctests.py +++ b/urwid/tests/test_doctests.py @@ -15,6 +15,7 @@ def load_tests(loader, tests, ignore): 'urwid.split_repr', # override function with same name urwid.util, urwid.signals, + urwid.graphics, ] for m in module_doctests: tests.addTests(doctest.DocTestSuite(m, diff --git a/urwid/tests/test_event_loops.py b/urwid/tests/test_event_loops.py index c85bbed..b01212d 100644 --- a/urwid/tests/test_event_loops.py +++ b/urwid/tests/test_event_loops.py @@ -30,9 +30,14 @@ class EventLoopTestMixin(object): def test_remove_watch_file(self): evl = self.evl - handle = evl.watch_file(5, lambda: None) - self.assertTrue(evl.remove_watch_file(handle)) - self.assertFalse(evl.remove_watch_file(handle)) + fd_r, fd_w = os.pipe() + try: + handle = evl.watch_file(fd_r, lambda: None) + self.assertTrue(evl.remove_watch_file(handle)) + self.assertFalse(evl.remove_watch_file(handle)) + finally: + os.close(fd_r) + os.close(fd_w) _expected_idle_handle = 1 diff --git a/urwid/tests/test_vterm.py b/urwid/tests/test_vterm.py index 4dadfcc..e47398c 100644 --- a/urwid/tests/test_vterm.py +++ b/urwid/tests/test_vterm.py @@ -18,6 +18,7 @@ # # Urwid web site: http://excess.org/urwid/ +import errno import os import sys import unittest @@ -28,7 +29,6 @@ from urwid import vterm from urwid import signals from urwid.compat import B - class DummyCommand(object): QUITSTRING = B('|||quit|||') @@ -41,12 +41,20 @@ class DummyCommand(object): stdout.write(B('\x1bc')) while True: - data = os.read(self.reader, 1024) + data = self.read(1024) if self.QUITSTRING == data: break stdout.write(data) stdout.flush() + def read(self, size): + while True: + try: + return os.read(self.reader, size) + except OSError as e: + if e.errno != errno.EINTR: + raise + def write(self, data): os.write(self.writer, data) @@ -183,7 +191,7 @@ class TermTest(unittest.TestCase): self.edgewall() self.expect('1-' + ' ' * 76 + '-2' + '\n' * 22 + '3-' + ' ' * 76 + '-4') - for y in xrange(23, 1, -1): + for y in range(23, 1, -1): self.resize(80, y, soft=True) self.write('\e[%df\e[J3-\e[%d;%df-4' % (y, y, 79)) desc = "try to rescale to 80x%d." % y diff --git a/urwid/tests/test_widget.py b/urwid/tests/test_widget.py index cc8c63e..3f28bc1 100644 --- a/urwid/tests/test_widget.py +++ b/urwid/tests/test_widget.py @@ -10,25 +10,25 @@ class TextTest(unittest.TestCase): self.t = urwid.Text("I walk the\ncity in the night") def test1_wrap(self): - expected = [B(t) for t in "I walk the","city in ","the night "] + expected = [B(t) for t in ("I walk the","city in ","the night ")] got = self.t.render((10,))._text assert got == expected, "got: %r expected: %r" % (got, expected) def test2_left(self): self.t.set_align_mode('left') - expected = [B(t) for t in "I walk the ","city in the night "] + expected = [B(t) for t in ("I walk the ","city in the night ")] got = self.t.render((18,))._text assert got == expected, "got: %r expected: %r" % (got, expected) def test3_right(self): self.t.set_align_mode('right') - expected = [B(t) for t in " I walk the"," city in the night"] + expected = [B(t) for t in (" I walk the"," city in the night")] got = self.t.render((18,))._text assert got == expected, "got: %r expected: %r" % (got, expected) def test4_center(self): self.t.set_align_mode('center') - expected = [B(t) for t in " I walk the "," city in the night"] + expected = [B(t) for t in (" I walk the "," city in the night")] got = self.t.render((18,))._text assert got == expected, "got: %r expected: %r" % (got, expected) diff --git a/urwid/text_layout.py b/urwid/text_layout.py index f09372b..d8663b6 100644 --- a/urwid/text_layout.py +++ b/urwid/text_layout.py @@ -19,9 +19,11 @@ # # Urwid web site: http://excess.org/urwid/ +from __future__ import division, print_function + from urwid.util import calc_width, calc_text_pos, calc_trim_text, is_wide_char, \ move_prev_char, move_next_char -from urwid.compat import bytes, PYTHON3, B +from urwid.compat import bytes, PYTHON3, B, xrange class TextLayout: def supports_align_mode(self, align): @@ -456,8 +458,8 @@ def calc_pos( text, layout, pref_col, row ): if pos is not None: return pos - rows_above = range(row-1,-1,-1) - rows_below = range(row+1,len(layout)) + rows_above = list(xrange(row-1,-1,-1)) + rows_below = list(xrange(row+1,len(layout))) while rows_above and rows_below: if rows_above: r = rows_above.pop(0) diff --git a/urwid/treetools.py b/urwid/treetools.py index 5b56d52..f2b1aad 100644 --- a/urwid/treetools.py +++ b/urwid/treetools.py @@ -20,6 +20,8 @@ # # Urwid web site: http://excess.org/urwid/ +from __future__ import division, print_function + """ Urwid tree view @@ -313,8 +315,8 @@ class ParentNode(TreeNode): def set_child_node(self, key, node): """Set the child node for a given key. Useful for bottom-up, lazy - population of a tree..""" - self._children[key]=node + population of a tree.""" + self._children[key] = node def change_child_key(self, oldkey, newkey): if newkey in self._children: diff --git a/urwid/util.py b/urwid/util.py index 3569f8c..f57c898 100644 --- a/urwid/util.py +++ b/urwid/util.py @@ -20,8 +20,10 @@ # # Urwid web site: http://excess.org/urwid/ +from __future__ import division, print_function + from urwid import escape -from urwid.compat import bytes +from urwid.compat import bytes, text_type, text_types import codecs @@ -108,7 +110,7 @@ def apply_target_encoding( s ): """ Return (encoded byte string, character set rle). """ - if _use_dec_special and type(s) == unicode: + if _use_dec_special and type(s) == text_type: # first convert drawing characters try: s = s.translate( escape.DEC_SPECIAL_CHARMAP ) @@ -118,7 +120,7 @@ def apply_target_encoding( s ): escape.ALT_DEC_SPECIAL_CHARS): s = s.replace( c, escape.SO+alt+escape.SI ) - if type(s) == unicode: + if type(s) == text_type: s = s.replace(escape.SI+escape.SO, u"") # remove redundant shifts s = codecs.encode(s, _target_encoding, 'replace') @@ -412,7 +414,7 @@ def _tagmarkup_recurse( tm, attr ): attr, element = tm return _tagmarkup_recurse( element, attr ) - if not isinstance(tm,(basestring, bytes)): + if not isinstance(tm, text_types + (bytes,)): raise TagMarkupException("Invalid markup element: %r" % tm) # text diff --git a/urwid/version.py b/urwid/version.py index e34283f..7b70ec5 100644 --- a/urwid/version.py +++ b/urwid/version.py @@ -1,3 +1,4 @@ +from __future__ import division, print_function VERSION = (1, 3, 1) __version__ = ''.join(['-.'[type(x) == int]+str(x) for x in VERSION])[1:] diff --git a/urwid/vterm.py b/urwid/vterm.py index 0f091ea..0c91ca5 100644 --- a/urwid/vterm.py +++ b/urwid/vterm.py @@ -20,6 +20,8 @@ # # Urwid web site: http://excess.org/urwid/ +from __future__ import division, print_function + import os import sys import time @@ -43,7 +45,7 @@ from urwid.escape import DEC_SPECIAL_CHARS, ALT_DEC_SPECIAL_CHARS from urwid.canvas import Canvas from urwid.widget import Widget, BOX from urwid.display_common import AttrSpec, RealTerminal, _BASIC_COLORS -from urwid.compat import ord2, chr2, B, bytes, PYTHON3 +from urwid.compat import ord2, chr2, B, bytes, PYTHON3, xrange ESC = chr(27) diff --git a/urwid/web_display.py b/urwid/web_display.py index 44a505c..2b2de46 100755 --- a/urwid/web_display.py +++ b/urwid/web_display.py @@ -19,6 +19,8 @@ # # Urwid web site: http://excess.org/urwid/ +from __future__ import division, print_function + """ Urwid web application display module """ @@ -659,7 +661,7 @@ class Screen: urwid_id = "%09d%09d"%(random.randrange(10**9), random.randrange(10**9)) self.pipe_name = os.path.join(_prefs.pipe_dir,"urwid"+urwid_id) - os.mkfifo(self.pipe_name+".in",0600) + os.mkfifo(self.pipe_name+".in",0o600) signal.signal(signal.SIGTERM,self._cleanup_pipe) self.input_fd = os.open(self.pipe_name+".in", @@ -743,9 +745,11 @@ class Screen: rows = MAX_ROWS self.screen_size = cols, rows - def draw_screen(self, (cols, rows), r ): + def draw_screen(self, size, r ): """Send a screen update to the client.""" + (cols, rows) = size + if cols != self.last_screen_width: self.last_screen = {} diff --git a/urwid/widget.py b/urwid/widget.py index 9a732e7..97cc4b9 100644 --- a/urwid/widget.py +++ b/urwid/widget.py @@ -19,8 +19,11 @@ # # Urwid web site: http://excess.org/urwid/ +from __future__ import division, print_function + from operator import attrgetter +from urwid.compat import text_type, with_metaclass from urwid.util import (MetaSuper, decompose_tagmarkup, calc_width, is_wide_char, move_prev_char, move_next_char) from urwid.text_layout import calc_pos, calc_coords, shift_line @@ -203,15 +206,10 @@ def cache_widget_rows(cls): return cached_rows -class Widget(object): +class Widget(with_metaclass(WidgetMeta, object)): """ Widget base class - .. attribute:: __metaclass__ - :annotation: = urwid.WidgetMeta - - See :class:`urwid.WidgetMeta` definition - .. attribute:: _selectable :annotation: = False @@ -443,8 +441,6 @@ class Widget(object): :returns: ``True`` if the position was set successfully anywhere on *row*, ``False`` otherwise """ - __metaclass__ = WidgetMeta - _selectable = False _sizing = frozenset([FLOW, BOX, FIXED]) _command_map = command_map @@ -827,7 +823,7 @@ class Text(Widget): >>> t = Text(('bold', u"stuff"), 'right', 'any') >>> t <Text flow widget 'stuff' align='right' wrap='any'> - >>> print t.text + >>> print(t.text) stuff >>> t.attrib [('bold', 5)] @@ -868,10 +864,10 @@ class Text(Widget): :type markup: text markup >>> t = Text(u"foo") - >>> print t.text + >>> print(t.text) foo >>> t.set_text(u"bar") - >>> print t.text + >>> print(t.text) bar >>> t.text = u"baz" # not supported because text stores text but set_text() takes markup Traceback (most recent call last): @@ -1101,11 +1097,22 @@ class Edit(Text): deletion. A caption may prefix the editing area. Uses text class for text layout. - Users of this class to listen for ``"change"`` events - sent when the value of edit_text changes. See :func:``connect_signal``. + Users of this class may listen for ``"change"`` or ``"postchange"`` + events. See :func:``connect_signal``. + + * ``"change"`` is sent just before the value of edit_text changes. + It receives the new text as an argument. Note that ``"change"`` cannot + change the text in question as edit_text changes the text afterwards. + * ``"postchange"`` is sent after the value of edit_text changes. + It receives the old value of the text as an argument and thus is + appropriate for changing the text. It is possible for a ``"postchange"`` + event handler to get into a loop of changing the text and then being + called when the event is re-emitted. It is up to the event + handler to guard against this case (for instance, by not changing the + text if it is signaled for for text that it has already changed once). """ # (this variable is picked up by the MetaSignals metaclass) - signals = ["change"] + signals = ["change", "postchange"] def valid_char(self, ch): """ @@ -1160,6 +1167,7 @@ class Edit(Text): self.allow_tab = allow_tab self._edit_pos = 0 self.set_caption(caption) + self._edit_text = '' self.set_edit_text(edit_text) if edit_pos is None: edit_pos = len(edit_text) @@ -1275,10 +1283,10 @@ class Edit(Text): >>> e = Edit("") >>> e.set_caption("cap1") - >>> print e.caption + >>> print(e.caption) cap1 >>> e.set_caption(('bold', "cap2")) - >>> print e.caption + >>> print(e.caption) cap2 >>> e.attrib [('bold', 4)] @@ -1289,7 +1297,9 @@ class Edit(Text): self._caption, self._attrib = decompose_tagmarkup(caption) self._invalidate() - caption = property(lambda self:self._caption) + caption = property(lambda self:self._caption, doc=""" + Read-only property returning the caption for this widget. + """) def set_edit_pos(self, pos): """ @@ -1321,7 +1331,9 @@ class Edit(Text): self._edit_pos = pos self._invalidate() - edit_pos = property(lambda self:self._edit_pos, set_edit_pos) + edit_pos = property(lambda self:self._edit_pos, set_edit_pos, doc=""" + Property controlling the edit position for this widget. + """) def set_mask(self, mask): """ @@ -1344,20 +1356,22 @@ class Edit(Text): >>> e = Edit() >>> e.set_edit_text(u"yes") - >>> print e.edit_text + >>> print(e.edit_text) yes >>> e <Edit selectable flow widget 'yes' edit_pos=0> >>> e.edit_text = u"no" # Urwid 0.9.9 or later - >>> print e.edit_text + >>> print(e.edit_text) no """ text = self._normalize_to_caption(text) self.highlight = None self._emit("change", text) + old_text = self._edit_text self._edit_text = text if self.edit_pos > len(text): self.edit_pos = len(text) + self._emit("postchange", old_text) self._invalidate() def get_edit_text(self): @@ -1365,15 +1379,15 @@ class Edit(Text): Return the edit text for this widget. >>> e = Edit(u"What? ", u"oh, nothing.") - >>> print e.get_edit_text() + >>> print(e.get_edit_text()) oh, nothing. - >>> print e.edit_text + >>> print(e.edit_text) oh, nothing. """ return self._edit_text edit_text = property(get_edit_text, set_edit_text, doc=""" - Read-only property returning the edit text for this widget. + Property controlling the edit text for this widget. """) def insert_text(self, text): @@ -1392,7 +1406,7 @@ class Edit(Text): <Edit selectable flow widget '42.5' edit_pos=4> >>> e.set_edit_pos(2) >>> e.insert_text(u"a") - >>> print e.edit_text + >>> print(e.edit_text) 42a.5 """ text = self._normalize_to_caption(text) @@ -1406,8 +1420,8 @@ class Edit(Text): Return text converted to the same type as self.caption (bytes or unicode) """ - tu = isinstance(text, unicode) - cu = isinstance(self._caption, unicode) + tu = isinstance(text, text_type) + cu = isinstance(self._caption, text_type) if tu == cu: return text if tu: @@ -1451,12 +1465,12 @@ class Edit(Text): >>> e.keypress(size, 'x') >>> e.keypress(size, 'left') >>> e.keypress(size, '1') - >>> print e.edit_text + >>> print(e.edit_text) 1x >>> e.keypress(size, 'backspace') >>> e.keypress(size, 'end') >>> e.keypress(size, '2') - >>> print e.edit_text + >>> print(e.edit_text) x2 >>> e.keypress(size, 'shift f1') 'shift f1' @@ -1465,8 +1479,8 @@ class Edit(Text): p = self.edit_pos if self.valid_char(key): - if (isinstance(key, unicode) and not - isinstance(self._caption, unicode)): + if (isinstance(key, text_type) and not + isinstance(self._caption, text_type)): # screen is sending us unicode input, must be using utf-8 # encoding because that's all we support, so convert it # to bytes to match our caption's type @@ -1700,10 +1714,10 @@ class IntEdit(Edit): >>> e, size = IntEdit(u"", 5002), (10,) >>> e.keypress(size, 'home') >>> e.keypress(size, 'delete') - >>> print e.edit_text + >>> print(e.edit_text) 002 >>> e.keypress(size, 'end') - >>> print e.edit_text + >>> print(e.edit_text) 2 """ (maxcol,) = size @@ -1728,7 +1742,7 @@ class IntEdit(Edit): True """ if self.edit_text: - return long(self.edit_text) + return int(self.edit_text) else: return 0 diff --git a/urwid/wimp.py b/urwid/wimp.py index 25e70c1..62a0819 100755 --- a/urwid/wimp.py +++ b/urwid/wimp.py @@ -19,6 +19,8 @@ # # Urwid web site: http://excess.org/urwid/ +from __future__ import division, print_function + from urwid.widget import (Text, WidgetWrap, delegate_to_widget_mixin, BOX, FLOW) from urwid.canvas import CompositeCanvas @@ -109,7 +111,7 @@ class CheckBox(WidgetWrap): # allow users of this class to listen for change events # sent when the state of this widget is modified # (this variable is picked up by the MetaSignals metaclass) - signals = ["change"] + signals = ["change", 'postchange'] def __init__(self, label, state=False, has_mixed=False, on_state_change=None, user_data=None): @@ -121,7 +123,7 @@ class CheckBox(WidgetWrap): function call for a single callback :param user_data: user_data for on_state_change - Signals supported: ``'change'`` + Signals supported: ``'change'``, ``"postchange"`` Register signal handler with:: @@ -184,12 +186,12 @@ class CheckBox(WidgetWrap): Return label text. >>> cb = CheckBox(u"Seriously") - >>> print cb.get_label() + >>> print(cb.get_label()) Seriously - >>> print cb.label + >>> print(cb.label) Seriously >>> cb.set_label([('bright_attr', u"flashy"), u" normal"]) - >>> print cb.label # only text is returned + >>> print(cb.label) # only text is returned flashy normal """ return self._label.text @@ -233,7 +235,8 @@ class CheckBox(WidgetWrap): # self._state is None is a special case when the CheckBox # has just been created - if do_callback and self._state is not None: + old_state = self._state + if do_callback and old_state is not None: self._emit('change', state) self._state = state # rebuild the display widget with the new state @@ -241,6 +244,8 @@ class CheckBox(WidgetWrap): ('fixed', self.reserve_columns, self.states[state] ), self._label ] ) self._w.focus_col = 0 + if do_callback and old_state is not None: + self._emit('postchange', old_state) def get_state(self): """Return the state of the checkbox.""" @@ -335,7 +340,7 @@ class RadioButton(CheckBox): This function will append the new radio button to group. "first True" will set to True if group is empty. - Signals supported: ``'change'`` + Signals supported: ``'change'``, ``"postchange"`` Register signal handler with:: @@ -504,9 +509,9 @@ class Button(WidgetWrap): Return label text. >>> b = Button(u"Ok") - >>> print b.get_label() + >>> print(b.get_label()) Ok - >>> print b.label + >>> print(b.label) Ok """ return self._label.text |