From 52b75fad0950bda69c0d702d9bc0665fd01a4059 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 9 Mar 2014 05:10:26 -0700 Subject: [PATCH 001/459] 'blessings' -> 'blessed', includes all my fixes and improvements overview of fixes and improvements in changelog. still need to add demonstration code for README. --- .gitignore | 5 +- README.rst | 132 ++++++--- {blessings => blessed}/__init__.py | 452 ++++++++++++++++++++++++----- blessed/keyboard.py | 226 +++++++++++++++ blessed/sequences.py | 442 ++++++++++++++++++++++++++++ blessings/tests.py | 270 ----------------- setup.cfg | 3 + setup.py | 74 +++-- test_keyboard.py | 122 ++++++++ tests/accessories.py | 199 +++++++++++++ tests/test_keyboard.py | 382 ++++++++++++++++++++++++ tests/test_sequence_length.py | 188 ++++++++++++ tests/test_sequences.py | 318 ++++++++++++++++++++ tests/test_wrap.py | 62 ++++ 14 files changed, 2473 insertions(+), 402 deletions(-) rename {blessings => blessed}/__init__.py (54%) create mode 100644 blessed/keyboard.py create mode 100644 blessed/sequences.py delete mode 100644 blessings/tests.py create mode 100644 setup.cfg mode change 100644 => 100755 setup.py create mode 100644 test_keyboard.py create mode 100644 tests/accessories.py create mode 100644 tests/test_keyboard.py create mode 100644 tests/test_sequence_length.py create mode 100644 tests/test_sequences.py create mode 100644 tests/test_wrap.py diff --git a/.gitignore b/.gitignore index 94748175..2e9b7023 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,10 @@ +.coverage +.cache .tox *.egg-info *.egg *.pyc build dist -docs/_build \ No newline at end of file +docs/_build +htmlcov diff --git a/README.rst b/README.rst index b7f24014..d88f45ce 100644 --- a/README.rst +++ b/README.rst @@ -1,18 +1,21 @@ -========= -Blessings -========= +======= +Blessed +======= -Coding with Blessings looks like this... :: +Coding with Blessed looks like this... :: - from blessings import Terminal +from blessed import Terminal - t = Terminal() +t = Terminal() - print t.bold('Hi there!') - print t.bold_red_on_bright_green('It hurts my eyes!') +print t.bold('Hi there!') +print t.bold_red_on_bright_green('It hurts my eyes!') - with t.location(0, t.height - 1): - print 'This is at the bottom.' +with t.location(0, t.height - 1): + print t.center(t.blink('press any key to continue.')) + +with t.key_at_a_time(): + t.keypress() Or, for byte-level control, you can drop down and play with raw terminal capabilities:: @@ -23,7 +26,7 @@ capabilities:: The Pitch ========= -Blessings lifts several of curses_' limiting assumptions, and it makes your +Blessed lifts several of curses_' limiting assumptions, and it makes your code pretty, too: * Use styles, color, and maybe a little positioning without necessarily @@ -41,7 +44,7 @@ code pretty, too: Before And After ---------------- -Without Blessings, this is how you'd print some underlined text at the bottom +Without Blessed, this is how you'd print some underlined text at the bottom of the screen:: from curses import tigetstr, setupterm, tparm @@ -72,9 +75,9 @@ of the screen:: print rc # Restore cursor position. That was long and full of incomprehensible trash! Let's try it again, this time -with Blessings:: +with Blessed:: - from blessings import Terminal + from blessed import Terminal term = Terminal() with term.location(0, term.height - 1): @@ -85,7 +88,7 @@ Much better. What It Provides ================ -Blessings provides just one top-level object: ``Terminal``. Instantiating a +Blessed provides just one top-level object: ``Terminal``. Instantiating a ``Terminal`` figures out whether you're on a terminal at all and, if so, does any necessary terminal setup. After that, you can proceed to ask it all sorts of things about the terminal. Terminal terminal terminal. @@ -96,7 +99,7 @@ Simple Formatting Lots of handy formatting codes ("capabilities" in low-level parlance) are available as attributes on a ``Terminal``. For example:: - from blessings import Terminal + from blessed import Terminal term = Terminal() print 'I am ' + term.bold + 'bold' + term.normal + '!' @@ -150,7 +153,7 @@ Color 16 colors, both foreground and background, are available as easy-to-remember attributes:: - from blessings import Terminal + from blessed import Terminal term = Terminal() print term.red + term.on_green + 'Red on green? Ick!' + term.normal @@ -192,7 +195,7 @@ no effect: the foreground and background colors will stay as they were. You can get fancy and do different things depending on the supported colors by checking `number_of_colors`_. -.. _`number_of_colors`: http://packages.python.org/blessings/#blessings.Terminal.number_of_colors +.. _`number_of_colors`: http://packages.python.org/blessed/#blessed.Terminal.number_of_colors Compound Formatting ------------------- @@ -200,7 +203,7 @@ Compound Formatting If you want to do lots of crazy formatting all at once, you can just mash it all together:: - from blessings import Terminal + from blessed import Terminal term = Terminal() print term.bold_underline_green_on_yellow + 'Woo' + term.normal @@ -233,7 +236,7 @@ Most often, you'll need to flit to a certain location, print something, and then return: for example, when updating a progress bar at the bottom of the screen. ``Terminal`` provides a context manager for doing this concisely:: - from blessings import Terminal + from blessed import Terminal term = Terminal() with term.location(0, term.height - 1): @@ -265,7 +268,7 @@ Moving Permanently If you just want to move and aren't worried about returning, do something like this:: - from blessings import Terminal + from blessed import Terminal term = Terminal() print term.move(10, 1) + 'Hi, mom!' @@ -310,7 +313,7 @@ Height And Width It's simple to get the height and width of the terminal, in characters:: - from blessings import Terminal + from blessed import Terminal term = Terminal() height = term.height @@ -322,7 +325,7 @@ SIGWINCH handlers. Clearing The Screen ------------------- -Blessings provides syntactic sugar over some screen-clearing capabilities: +Blessed provides syntactic sugar over some screen-clearing capabilities: ``clear`` Clear the whole screen. @@ -339,7 +342,7 @@ Full-Screen Mode Perhaps you have seen a full-screen program, such as an editor, restore the exact previous state of the terminal upon exiting, including, for example, the command-line prompt from which it was launched. Curses pretty much forces you -into this behavior, but Blessings makes it optional. If you want to do the +into this behavior, but Blessed makes it optional. If you want to do the state-restoration thing, use these capabilities: ``enter_fullscreen`` @@ -354,7 +357,7 @@ reserve it for when you don't want to leave anything behind in the scrollback. There's also a context manager you can use as a shortcut:: - from blessings import Terminal + from blessed import Terminal term = Terminal() with term.fullscreen(): @@ -380,7 +383,7 @@ you see whether your capabilities will return actual, working formatting codes. If it's false, you should refrain from drawing progress bars and other frippery and just stick to content, since you're apparently headed into a pipe:: - from blessings import Terminal + from blessed import Terminal term = Terminal() if term.does_styling: @@ -388,12 +391,45 @@ and just stick to content, since you're apparently headed into a pipe:: print 'Progress: [=======> ]' print term.bold('Important stuff') +Sequence Awareness +------------------ + +Blessed may measure the printable width of strings containing sequences, +providing ``.center``, ``.ljust``, and ``.rjust``, using the terminal +screen's width as the default ``width`` value:: + + from blessed import Terminal + + term = Terminal() + print (''.join(term.move(term.height / 2), # move-to vertical center + term.center(term.bold('X')) # horizontal ceneted + term.move(terminal.height -1),)) # move-to vertical bottom + +Any string containing sequences may have its printable length measured using +``.length``. Additionally, ``textwrap.wrap()`` is supplied on the Terminal class +as method ``.wrap`` method that is also sequence-aware, so now you may word-wrap +strings containing sequences. The following example uses a width value of 25 to +format a poem from Tao Te Ching:: + + from blessed import Terminal + + t = Terminal() + + poem = (term.bold_blue('Plan difficult tasks ') + + term.bold_black('through the simplest tasks'), + term.bold_cyan('Achieve large tasks ') + + term.cyan('through the smallest tasks')) + for line in poem: + print('\n'.join(term.wrap(line, width=25, + subsequent_indent=' '*4))) + + Shopping List ============= There are decades of legacy tied up in terminal interaction, so attention to detail and behavior in edge cases make a difference. Here are some ways -Blessings has your back: +Blessed has your back: * Uses the terminfo database so it works with any terminal type * Provides up-to-the-moment terminal height and width, so you can respond to @@ -408,7 +444,7 @@ Blessings has your back: * Keeps a minimum of internal state, so you can feel free to mix and match with calls to curses or whatever other terminal libraries you like -Blessings does not provide... +Blessed does not provide... * Native color support on the Windows command prompt. However, it should work when used in concert with colorama_. @@ -420,29 +456,55 @@ Bugs Bugs or suggestions? Visit the `issue tracker`_. -.. _`issue tracker`: https://github.com/erikrose/blessings/issues/ +.. _`issue tracker`: https://github.com/jquast/blessed/issues/ -.. image:: https://secure.travis-ci.org/erikrose/blessings.png +.. image:: https://secure.travis-ci.org/jquast/blessed.png License ======= -Blessings is under the MIT License. See the LICENSE file. +Blessed is derived from Blessings, which is under the MIT License, and +shares the same. See the LICENSE file. Version History =============== +1.7, Forked 'erikrose/blessings' to 'jquast/blessed'. +Includes the following changes, (jquast): + * Created ``python setup.py develop`` for developer environment. + * Converted nosetests to pytest, use ``python setup.py test``. + * introduced ``@as_subprocess`` to discover and resolve various issues. + * cannot call ``setupterm()`` more than once per process. + * ``number_of_colors`` fails when ``does_styling`` is ``False``. + * pokemon ``curses.error`` exception removed. + * warning emitted and ``does_styling`` set ``False`` when TERM is unset + or unknown. + * allow ``term.color(7)('string')`` to behave when ``does_styling`` is + ``False``. + * attributes that should be read-only have now raise exception when + re-assigned (properties). + * introduced ``term.center()``, ``term.rjust()``, and ``term.ljust()``, + allows text containing sequences to be aligned to screen or argument + ``width``. + * introduced ``term.wrap()``, allows text containing sequences to be + word-wrapped without breaking mid-sequence and honoring their printable + width. + * introduced context manager ``cbreak`` which is equivalent to ``tty.cbreak``, + placing the terminal in 'cooked' mode, allowing input from stdin to be read + as each key is pressed (line-buffering disabled). + * introduced method ``inkey()``, which will return 1 or more characters as + a unicode sequence, with attributes ``.code`` and ``.name`` non-None when + a multibyte sequence is received, allowing arrow keys and such to be + detected. Optional value ``timeout`` allows timed polling or blocking. + + 1.6 * Add ``does_styling`` property. This takes ``force_styling`` into account and should replace most uses of ``is_a_tty``. * Make ``is_a_tty`` a read-only property, like ``does_styling``. Writing to it never would have done anything constructive. * Add ``fullscreen()`` and ``hidden_cursor()`` to the auto-generated docs. - * Fall back to ``LINES`` and ``COLUMNS`` environment vars to find height and - width. (jquast) - * Support terminal types, such as kermit and avatar, that use bytes 127-255 - in their escape sequences. (jquast) 1.5.1 * Clean up fabfile, removing the redundant ``test`` command. diff --git a/blessings/__init__.py b/blessed/__init__.py similarity index 54% rename from blessings/__init__.py rename to blessed/__init__.py index 04b10a57..403cc5cb 100644 --- a/blessings/__init__.py +++ b/blessed/__init__.py @@ -1,30 +1,37 @@ -"""A thin, practical wrapper around terminal coloring, styling, and -positioning""" - -from contextlib import contextmanager +"""A thin, practical wrapper around curses terminal capabilities.""" + +# standard modules +import collections +import contextlib +import platform +import warnings +import termios +import codecs import curses -from curses import setupterm, tigetnum, tigetstr, tparm -from fcntl import ioctl - -try: - from io import UnsupportedOperation as IOUnsupportedOperation -except ImportError: - class IOUnsupportedOperation(Exception): - """A dummy exception to take the place of Python 3's - ``io.UnsupportedOperation`` in Python 2""" - -from os import isatty, environ -from platform import python_version_tuple +import locale +import select import struct +import fcntl +import time +import tty import sys -from termios import TIOCGWINSZ +import os +# local imports +import sequences +import keyboard __all__ = ['Terminal'] +try: + from io import UnsupportedOperation as IOUnsupportedOperation +except ImportError: + class IOUnsupportedOperation(Exception): + """A dummy exception to take the place of Python 3's + ``io.UnsupportedOperation`` in Python 2.5""" -if ('3', '0', '0') <= python_version_tuple() < ('3', '2', '2+'): # Good till - # 3.2.10 +if ('3', '0', '0') <= platform.python_version_tuple() < ('3', '2', '2+'): + # Good till 3.2.10 # Python 3.x < 3.2.3 has a bug in which tparm() erroneously takes a string. raise ImportError('Blessings needs Python 3.2.3 or greater for Python 3 ' 'support due to http://bugs.python.org/issue10570.') @@ -45,7 +52,6 @@ class Terminal(object): around with the terminal; it's almost always needed when the terminal is and saves sticking lots of extra args on client functions in practice. - """ def __init__(self, kind=None, stream=None, force_styling=False): """Initialize the terminal. @@ -75,8 +81,11 @@ def __init__(self, kind=None, stream=None, force_styling=False): ``force_styling=None``. """ + global _CUR_TERM if stream is None: stream = sys.__stdout__ + self.stream_kb = sys.__stdin__.fileno() + try: stream_descriptor = (stream.fileno() if hasattr(stream, 'fileno') and callable(stream.fileno) @@ -85,24 +94,60 @@ def __init__(self, kind=None, stream=None, force_styling=False): stream_descriptor = None self._is_a_tty = (stream_descriptor is not None and - isatty(stream_descriptor)) + os.isatty(stream_descriptor)) self._does_styling = ((self.is_a_tty or force_styling) and force_styling is not None) + # keyboard input only valid when stream is sys.stdout + # The desciptor to direct terminal initialization sequences to. # sys.__stdout__ seems to always have a descriptor of 1, even if output # is redirected. self._init_descriptor = (sys.__stdout__.fileno() if stream_descriptor is None else stream_descriptor) + self._kind = kind or os.environ.get('TERM', 'unknown') if self.does_styling: # Make things like tigetstr() work. Explicit args make setupterm() # work even when -s is passed to nosetests. Lean toward sending # init sequences to the stream if it has a file descriptor, and # send them to stdout as a fallback, since they have to go # somewhere. - setupterm(kind or environ.get('TERM', 'unknown'), - self._init_descriptor) + try: + curses.setupterm(self._kind, self._init_descriptor) + except curses.error: + warnings.warn('Failed to setupterm(kind=%s)' % (self._kind,)) + self._kind = None + self._does_styling = False + else: + if _CUR_TERM is None or self._kind == _CUR_TERM: + _CUR_TERM = self._kind + else: + warnings.warn( + 'A terminal of kind "%s" has been requested; due to an' + ' internal python curses bug, terminal capabilities' + ' for a terminal of kind "%s" will continue to be' + ' returned for the remainder of this process. see:' + ' https://github.com/erikrose/blessings/issues/33' % ( + self._kind, _CUR_TERM,)) + + if self.does_styling: + sequences.init_sequence_patterns(self) + + # build database of int code <=> KEY_NAME + self._keycodes = keyboard.get_keyboard_codes() + + # store attributes as: self.KEY_NAME = code + for key_code, key_name in self._keycodes.items(): + setattr(self, key_name, key_code) + + # build database of sequence <=> KEY_NAME + self._keymap = keyboard.get_keyboard_sequences(self) + + self._keyboard_buf = [] + locale.setlocale(locale.LC_ALL, '') + self._encoding = locale.getpreferredencoding() + self._keyboard_decoder = codecs.getincrementaldecoder(self._encoding)() self.stream = stream @@ -194,53 +239,65 @@ def is_a_tty(self): @property def height(self): - """The height of the terminal in characters + """T.height -> int - If no stream or a stream not representing a terminal was passed in at - construction, return the dimension of the controlling terminal so - piping to things that eventually display on the terminal (like ``less - -R``) work. If a stream representing a terminal was passed in, return - the dimensions of that terminal. If there somehow is no controlling - terminal, return ``None``. (Thus, you should check that the property - ``is_a_tty`` is true before doing any math on the result.) + The height of the terminal in characters. + If an alternative ``stream`` is chosen, the size of that stream + is returned if it is a connected to a terminal such as a pty. + Otherwise, the size of the controlling terminal is returned. + + If neither of these streams are terminals, such as when stdout is piped + to less(1), the values of the environment variable LINES and COLS are + returned. + + None may be returned if no suitable size is discovered. """ - return self._height_and_width()[0] + return self._height_and_width()[1] @property def width(self): - """The width of the terminal in characters + """T.width -> int - See ``height()`` for some corner cases. + The width of the terminal in characters. + None may be returned if no suitable size is discovered. """ - return self._height_and_width()[1] + return self._height_and_width()[0] - def _height_and_width(self): - """Return a tuple of (terminal height, terminal width). + @staticmethod + def _winsize(fd): + """T._winsize -> WINSZ(ws_row, ws_col, ws_xpixel, ws_ypixel) - Start by trying TIOCGWINSZ (Terminal I/O-Control: Get Window Size), - falling back to environment variables (LINES, COLUMNS), and returning - (None, None) if those are unavailable or invalid. + The tty connected by file desriptor fd is queried for its window size, + and returned as a collections.namedtuple instance WINSZ. + May raise exception IOError. """ - # tigetnum('lines') and tigetnum('cols') update only if we call - # setupterm() again. + data = fcntl.ioctl(fd, termios.TIOCGWINSZ, WINSZ._BUF) + return WINSZ(*struct.unpack(WINSZ._FMT, data)) + + def _height_and_width(self): + """Return a tuple of (terminal height, terminal width). + """ + # TODO(jquast): hey kids, even if stdout is redirected to a file, + # we can still query sys.__stdin__.fileno() for our terminal size. + # -- of course, if both are redirected, we have no use for this fd. for descriptor in self._init_descriptor, sys.__stdout__: try: - return struct.unpack( - 'hhhh', ioctl(descriptor, TIOCGWINSZ, '\000' * 8))[0:2] + winsize = self._winsize(descriptor) + return winsize.ws_row, winsize.ws_col except IOError: - # when the output stream or init descriptor is not a tty, such - # as when when stdout is piped to another program, fe. tee(1), - # these ioctls will raise IOError pass - try: - return int(environ.get('LINES')), int(environ.get('COLUMNS')) - except TypeError: - return None, None - @contextmanager + lines, cols = None, None + if os.environ.get('LINES', None) is not None: + lines = int(os.environ['LINES']) + if os.environ.get('COLUMNS', None) is not None: + cols = int(os.environ['COLUMNS']) + return lines, cols + + @contextlib.contextmanager def location(self, x=None, y=None): """Return a context manager for temporarily moving the cursor. @@ -274,7 +331,7 @@ def location(self, x=None, y=None): # Restore original cursor position: self.stream.write(self.restore) - @contextmanager + @contextlib.contextmanager def fullscreen(self): """Return a context manager that enters fullscreen mode while inside it and restores normal mode on leaving.""" @@ -284,7 +341,7 @@ def fullscreen(self): finally: self.stream.write(self.exit_fullscreen) - @contextmanager + @contextlib.contextmanager def hidden_cursor(self): """Return a context manager that hides the cursor while inside it and makes it visible on leaving.""" @@ -306,7 +363,8 @@ def color(self): :arg num: The number, 0-15, of the color """ - return ParametrizingString(self._foreground_color, self.normal) + return (ParametrizingString(self._foreground_color, self.normal) + if self.does_styling else NullCallableString()) @property def on_color(self): @@ -315,7 +373,8 @@ def on_color(self): See ``color()``. """ - return ParametrizingString(self._background_color, self.normal) + return (ParametrizingString(self._background_color, self.normal) + if self.does_styling else NullCallableString()) @property def number_of_colors(self): @@ -337,11 +396,11 @@ def number_of_colors(self): # don't name it after the underlying capability, because we deviate # slightly from its behavior, and we might someday wish to give direct # access to it. - colors = tigetnum('colors') # Returns -1 if no color support, -2 if no - # such cap. + # Returns -1 if no color support, -2 if no such capability. + colors = self.does_styling and curses.tigetnum('colors') or -1 # self.__dict__['colors'] = ret # Cache it. It's not changing. # (Doesn't work.) - return colors if colors >= 0 else 0 + return max(0, colors) def _resolve_formatter(self, attr): """Resolve a sugary or plain capability name, color, or compound @@ -374,9 +433,9 @@ def _resolve_capability(self, atom): (especially in Python 3) to concatenate with real (Unicode) strings. """ - code = tigetstr(self._sugar.get(atom, atom)) + code = curses.tigetstr(self._sugar.get(atom, atom)) if code: - # See the comment in ParametrizingString for why this is latin1. + # Decode sequences as latin1, as they are always 8-bit bytes. return code.decode('latin1') return u'' @@ -393,6 +452,8 @@ def _resolve_color(self, color): # bright colors at 8-15: offset = 8 if 'bright_' in color else 0 base_color = color.rsplit('_', 1)[-1] + if self.number_of_colors == 0: + return NullCallableString() return self._formatting_string( color_cap(getattr(curses, 'COLOR_' + base_color.upper()) + offset)) @@ -409,6 +470,219 @@ def _formatting_string(self, formatting): notion of "normal".""" return FormattingString(formatting, self.normal) + def ljust(self, text, width=None, fillchar=u' '): + """T.ljust(text, [width], [fillchar]) -> string + + Return string ``text``, left-justified by printable length ``width``. + Padding is done using the specified fill character (default is a + space). Default width is the attached terminal's width. ``text`` is + escape-sequence safe.""" + if width is None: + width = self.width + return sequences.Sequence(text, self).ljust(width, fillchar) + + def rjust(self, text, width=None, fillchar=u' '): + """T.rjust(text, [width], [fillchar]) -> string + + Return string ``text``, right-justified by printable length ``width``. + Padding is done using the specified fill character (default is a space) + Default width is the attached terminal's width. ``text`` is + escape-sequence safe.""" + if width is None: + width = self.width + return sequences.Sequence(text, self).rjust(width, fillchar) + + def center(self, text, width=None, fillchar=u' '): + """T.center(text, [width], [fillchar]) -> string + + Return string ``text``, centered by printable length ``width``. + Padding is done using the specified fill character (default is a + space). Default width is the attached terminal's width. ``text`` is + escape-sequence safe.""" + if width is None: + width = self.width + return sequences.Sequence(text, self).center(width, fillchar) + + def length(self, text): + """T.length(text) -> int + + Return printable length of string ``text``, which may contain (some + kinds) of sequences. Strings containing sequences such as 'clear', + which repositions the cursor will not give accurate results. + """ + return sequences.Sequence(text, self).length() + + def wrap(self, text, width=None, **kwargs): + """T.wrap(text, [width=None, indent=u'', ...]) -> unicode + + Wrap paragraphs containing escape sequences, ``text``, to the full + width of Terminal instance T, unless width is specified, wrapped by + the virtual printable length, irregardless of the video attribute + sequences it may contain. + + Returns a list of strings that may contain escape sequences. See + textwrap.TextWrapper class for available additional kwargs to + customize wrapping behavior. + + Note that the keyword argument ``break_long_words`` may not be set, + it is not sequence-safe. + """ + + _blw = 'break_long_words' + assert (_blw not in kwargs or not kwargs[_blw]), ( + "keyword argument, '{}' is not sequence-safe".format(_blw)) + + width = width is None and self.width or width + lines = [] + for line in text.splitlines(): + lines.extend( + (_linewrap for _linewrap in sequences.SequenceTextWrapper( + width=width, term=self, **kwargs).wrap(text)) + if line.strip() else (u'',)) + + return lines + + def kbhit(self, timeout=0): + """T.kbhit([timeout=0]) -> bool + + Returns True if a keypress has been detected on keyboard. + + When ``timeout`` is 0, this call is non-blocking(default), or blocking + indefinitely until keypress when ``None``, and blocking until keypress + or time elapsed when ``timeout`` is non-zero. + + If input is not a terminal, False is always returned. + """ + if self.keyboard_fd is None: + return False + + check_r, check_w, check_x = [self.stream_kb], [], [] + ready_r, ready_w, ready_x = select.select( + check_r, check_w, check_x, timeout) + + return check_r == ready_r + + @contextlib.contextmanager + def cbreak(self): + """Return a context manager that enters 'cbreak' mode: disabling line + buffering of keyboard input, making characters typed by the user + immediately available to the program. Also referred to as 'rare' + mode, this is the opposite of 'cooked' mode, the default for most + shells. + + In 'cbreak' mode, echo of input is also disabled: the application must + explicitly print any input received, if they so wish. + + More information can be found in the manual page for curses.h, + http://www.openbsd.org/cgi-bin/man.cgi?query=cbreak + + The python manual for curses, + http://docs.python.org/2/library/curses.html + + Note also that setcbreak sets VMIN = 1 and VTIME = 0, + http://www.unixwiz.net/techtips/termios-vmin-vtime.html + """ + assert self.is_a_tty, u'stream is not a a tty.' + if self.stream_kb is not None: + # save current terminal mode, + save_mode = termios.tcgetattr(self.stream_kb) + tty.setcbreak(self.stream_kb, termios.TCSANOW) + try: + yield + finally: + # restore prior mode, + termios.tcsetattr(self.stream_kb, termios.TCSAFLUSH, save_mode) + else: + yield + + def inkey(self, timeout=None, esc_delay=0.35): + """T.inkey(timeout=None, esc_delay=0.35) -> Keypress() + + Receive next keystroke from keyboard (stdin), blocking until a + keypress is received or ``timeout`` elapsed, if specified. + + When used without the context manager ``cbreak``, stdin remains + line-buffered, and this function will block until return is pressed. + + The value returned is an instance of ``Keystroke``, with properties + ``is_sequence``, and, when True, non-None values for ``code`` and + ``name``. The value of ``code`` may be compared against attributes + of this terminal beginning with KEY, such as KEY_ESCAPE. + + To distinguish between KEY_ESCAPE, and sequences beginning with + escape, the ``esc_delay`` specifies the amount of time after receiving + the escape character ('\x1b') to seek for application keys. + + """ + # TODO(jquast): "meta sends escape", where alt+1 would send '\x1b1', + # what do we do with that? Surely, something useful. + # comparator to term.KEY_meta('x') ? + # TODO(jquast): Ctrl characters, KEY_CTRL_[A-Z], and the rest; + # KEY_CTRL_\, KEY_CTRL_{, etc. are not legitimate + # attributes. comparator to term.KEY_ctrl('z') ? + def _timeleft(stime, timeout): + """_timeleft(stime, timeout) -> float + + Returns time-relative time remaining before ``timeout`` after time + elapsed since ``stime``. + """ + if timeout is not None: + if timeout is 0: + return 0 + return max(0, timeout - (time.time() - stime)) + + def _decode_next(): + """Read and decode next byte from stdin.""" + byte = os.read(self.stream_kb, 1) + return self._keyboard_decoder.decode(byte, final=False) + + def _resolve(text): + return keyboard.resolve_sequence(text=text, + mapper=self._keymap, + codes=self._keycodes) + + stime = time.time() + + # re-buffer previously received keystrokes, + ucs = u'' + while self._keyboard_buf: + ucs += self._keyboard_buf.pop() + + # receive all immediately available bytes + while self.kbhit(): + ucs += _decode_next() + + # decode keystroke, if any + ks = _resolve(ucs) + + # so long as the most immediately received or buffered keystroke is + # incomplete, (which may be a multibyte encoding), block until until + # one is received. + while not ks and self.kbhit(_timeleft(stime, timeout)): + ucs += _decode_next() + ks = _resolve(ucs) + + # handle escape key (KEY_ESCAPE) vs. escape sequence (which begins + # with KEY_ESCAPE, \x1b[, \x1bO, or \x1b?), up to esc_delay when + # received. This is not optimal, but causes least delay when + # (currently unhandled, and rare) "meta sends escape" is used, + # or when an unsupported sequence is sent. + + if ks.code is self.KEY_ESCAPE: + esctime = time.time() + while (ks.code is self.KEY_ESCAPE and + # XXX specially handle [?O sequence, + # which may soon become [?O{A,B,C,D}, as there + # is also an [?O in some terminal types + (len(ucs) <= 1 or ucs[1] in u'[?O') and + self.kbhit(_timeleft(esctime, esc_delay))): + ucs += _decode_next() + ks = _resolve(ucs) + + for remaining in ucs[len(ks):]: + self._keyboard_buf.insert(0, remaining) + return ks + def derivative_colors(colors): """Return the names of valid color variants, given the base colors.""" @@ -446,23 +720,10 @@ def __call__(self, *args): # Re-encode the cap, because tparm() takes a bytestring in Python # 3. However, appear to be a plain Unicode string otherwise so # concats work. - # - # We use *latin1* encoding so that bytes emitted by tparm are - # encoded to their native value: some terminal kinds, such as - # 'avatar' or 'kermit', emit 8-bit bytes in range 0x7f to 0xff. - # latin1 leaves these values unmodified in their conversion to - # unicode byte values. The terminal emulator will "catch" and - # handle these values, even if emitting utf8-encoded text, where - # these bytes would otherwise be illegal utf8 start bytes. - parametrized = tparm(self.encode('latin1'), *args).decode('latin1') + parametrized = curses.tparm( + self.encode('latin1'), *args).decode('latin1') return (parametrized if self._normal is None else FormattingString(parametrized, self._normal)) - except curses.error: - # Catch "must call (at least) setupterm() first" errors, as when - # running simply `nosetests` (without progressive) on nose- - # progressive. Perhaps the terminal has gone away between calling - # tigetstr and calling tparm. - return u'' except TypeError: # If the first non-int (i.e. incorrect) arg was a string, suggest # something intelligent: @@ -536,11 +797,27 @@ def __call__(self, *args): # determine which of 2 special-purpose classes, # NullParametrizableString or NullFormattingString, to return, and # retire this one. - return u'' + # As a NullCallableString, even when provided with a parameter, + # such as t.color(5), we must also still be callable, fe: + # >>> t.color(5)('shmoo') + # is actually simplified result of NullCallable()(), so + # turtles all the way down: we return another instance. + return NullCallableString() return args[0] # Should we force even strs in Python 2.x to be # unicodes? No. How would I know what encoding to use # to convert it? +WINSZ = collections.namedtuple('WINSZ', ( + 'ws_row', # /* rows, in characters */ + 'ws_col', # /* columns, in characters */ + 'ws_xpixel', # /* horizontal size, pixels */ + 'ws_ypixel', # /* vertical size, pixels */ +)) +#: format of termios structure +WINSZ._FMT = 'hhhh' +#: buffer of termios structure appropriate for ioctl argument +WINSZ._BUF = '\x00' * struct.calcsize(WINSZ._FMT) + def split_into_formatters(compound): """Split a possibly compound format string into segments. @@ -558,3 +835,24 @@ def split_into_formatters(compound): else: merged_segs.append(s) return merged_segs + +# From libcurses/doc/ncurses-intro.html (ESR, Thomas Dickey, et. al): +# +# "After the call to setupterm(), the global variable cur_term is set to +# point to the current structure of terminal capabilities. By calling +# setupterm() for each terminal, and saving and restoring cur_term, it +# is possible for a program to use two or more terminals at once." +# +# However, if you study Python's ./Modules/_cursesmodule.c, you'll find: +# +# if (!initialised_setupterm && setupterm(termstr,fd,&err) == ERR) { +# +# Python - perhaps wrongly - will not allow a re-initialisation of new +# terminals through setupterm(), so the value of cur_term cannot be changed +# once set: subsequent calls to setupterm() have no effect. +# +# Therefore, the ``kind`` of each Terminal() is, in essence, a singleton. +# This global variable reflects that, and a warning is emitted if somebody +# expects otherwise. + +_CUR_TERM = None diff --git a/blessed/keyboard.py b/blessed/keyboard.py new file mode 100644 index 00000000..8368fc74 --- /dev/null +++ b/blessed/keyboard.py @@ -0,0 +1,226 @@ +"""This sub-module provides 'keyboard awareness' for blessings.""" + +__author__ = 'Jeff Quast ' +__license__ = 'MIT' + +__all__ = ['Keystroke', 'get_keyboard_codes', 'get_keyboard_sequences'] + +import curses +import curses.has_key +import collections +if hasattr(collections, 'OrderedDict'): + OrderedDict = collections.OrderedDict +else: + # python 2.6 + import ordereddict + OrderedDict = ordereddict.OrderedDict + +get_curses_keycodes = lambda: dict( + ((keyname, getattr(curses, keyname)) + for keyname in dir(curses) + if keyname.startswith('KEY_')) +) + +# Inject missing KEY_TAB +if not hasattr(curses, 'KEY_TAB'): + curses.KEY_TAB = max(get_curses_keycodes().values()) + 1 + + +class Keystroke(unicode): + """A unicode-derived class for describing keyboard input returned by + the ``keypress()`` method of ``Terminal``, which may, at times, be a + multibyte sequence, providing properties ``is_sequence`` as True when + the string is a known sequence, and ``code``, which returns an integer + value that may be compared against the terminal class attributes such + as ``KEY_LEFT``. + """ + def __new__(cls, ucs='', code=None, name=None): + new = unicode.__new__(cls, ucs) + new._name = name + new._code = code + return new + + @property + def is_sequence(self): + """K.is_sequence -> bool + + Returns True if value represents a multibyte sequence. + """ + return self._code is not None + + def __repr__(self): + return self._name is None and unicode.__repr__(self) or self._name + __repr__.__doc__ = unicode.__doc__ + + @property + def name(self): + """K.name -> str + + Returns string-name of key sequence, such as 'KEY_LEFT' + """ + return self._name + + @property + def code(self): + """K.code -> int + + Returns integer keycode value of multibyte sequence. + """ + return self._code + + +def get_keyboard_codes(): + """get_keyboard_codes() -> dict + + Returns dictionary of (code, name) pairs for curses keyboard constant + values and their mnemonic name. Such as key 260, with the value of its + identity, 'KEY_LEFT'. These are derived from the attributes by the same + of the curses module, with the following exceptions: + * KEY_DELETE in place of KEY_DC + * KEY_INSERT in place of KEY_IC + * KEY_PGUP in place of KEY_PPAGE + * KEY_PGDOWN in place of KEY_NPAGE + * KEY_ESCAPE in place of KEY_EXIT + """ + keycodes = OrderedDict(get_curses_keycodes()) + keycodes.update(CURSES_KEYCODE_OVERRIDE_MIXIN) + + # invert dictionary (key, values) => (values, key), preferring the + # last-most inserted value ('KEY_DELETE' over 'KEY_DC'). + return dict(zip(keycodes.values(), keycodes.keys())) + + +def _alternative_left_right(term): + """Return dict of sequence term._cuf1, _cub1 as values curses.KEY_RIGHT, + LEFT when appropriate. + + some terminals report a different value for kcuf1 than cuf1, + but actually send the value of cuf1 for right arrow key + (which is non-destructive space). + """ + keymap = dict() + if term._cuf1 and term._cuf1 != u' ': + keymap[term._cuf1] = curses.KEY_RIGHT + if term._cub1 and term._cub1 != u'\b': + keymap[term._cub1] = curses.KEY_LEFT + return keymap + + +def get_keyboard_sequences(term): + """init_keyboard_sequences(T) -> (OrderedDict) + + Initialize and return a keyboard map and sequence lookup table, + (sequence, constant) from blessings Terminal instance ``term``, + where ``sequence`` is a multibyte input sequence, such as u'\x1b[D', + and ``constant`` is a constant, such as term.KEY_LEFT. The return + value is an OrderedDict instance, with their keys sorted longest-first. + """ + # A small gem from curses.has_key that makes this all possible, + # _capability_names: a lookup table of terminal capability names for + # keyboard sequences (fe. kcub1, key_left), keyed by the values of + # constants found beginning with KEY_ in the main curses module + # (such as KEY_LEFT). + # + # latin1 encoding is used so that bytes in 8-bit range of 127-255 + # have equivalent chr() and unichr() values, so that the sequence + # of a kermit or avatar terminal, for example, remains unchanged + # in its byte sequence values even when represented by unicode. + # + capability_names = curses.has_key._capability_names + sequence_map = dict(( + (seq.decode('latin1'), val) + for (seq, val) in ( + (curses.tigetstr(cap), val) + for (val, cap) in capability_names.iteritems() + ) if seq + )) + + sequence_map.update(_alternative_left_right(term)) + sequence_map.update(DEFAULT_SEQUENCE_MIXIN) + + # This is for fast lookup matching of sequences, preferring + # full-length sequence such as ('\x1b[D', KEY_LEFT) + # over simple sequences such as ('\x1b', KEY_EXIT). + return OrderedDict(( + (seq, sequence_map[seq]) for seq in sorted( + sequence_map, key=len, reverse=True))) + + +def resolve_sequence(text, mapper, codes): + """resolve_sequence(text, mapper, codes) -> Keystroke() + + Returns first matching Keystroke() instance for sequences found in + ``mapper`` beginning with input ``text``, where ``mapper`` is an + OrderedDict of unicode multibyte sequences, such as u'\x1b[D' paired by + their integer value (260), and ``codes`` is a dict of integer values (260) + paired by their mnemonic name, 'KEY_LEFT'. + """ + for sequence, code in mapper.iteritems(): + if text.startswith(sequence): + return Keystroke(ucs=sequence, code=code, name=codes[code]) + return Keystroke(ucs=text and text[0] or u'') + +# override a few curses constants with easier mnemonics, +# there may only be a 1:1 mapping, so for those who desire +# to use 'KEY_DC' from, perhaps, ported code, recommend +# that they simply compare with curses.KEY_DC. +CURSES_KEYCODE_OVERRIDE_MIXIN = ( + ('KEY_DELETE', curses.KEY_DC), + ('KEY_INSERT', curses.KEY_IC), + ('KEY_PGUP', curses.KEY_PPAGE), + ('KEY_PGDOWN', curses.KEY_NPAGE), + ('KEY_ESCAPE', curses.KEY_EXIT), +) + +# In a perfect world, terminal emulators would always send exactly what the +# terminfo(5) capability database plans for them, accordingly by the value +# of the TERM name they declare. +# +# But this isn't a perfect world. Many vt220-derived terminals, such as +# those declaring 'xterm', will continue to send vt220 codes instead of +# their native-declared codes. This goes for many: rxvt, putty, iTerm. +# +DEFAULT_SEQUENCE_MIXIN = ( + # these common control characters (and 127, ctrl+'?') mapped to + # an application key definition. + (unichr(10), curses.KEY_ENTER), + (unichr(13), curses.KEY_ENTER), + (unichr(8), curses.KEY_BACKSPACE), + (unichr(9), curses.KEY_TAB), + (unichr(27), curses.KEY_EXIT), + (unichr(127), curses.KEY_DC), + # vt100 application keys are still sent by xterm & friends, even if + # their reports otherwise; possibly, for compatibility reasons? + (u"\x1bOA", curses.KEY_UP), + (u"\x1bOB", curses.KEY_DOWN), + (u"\x1bOC", curses.KEY_RIGHT), + (u"\x1bOD", curses.KEY_LEFT), + (u"\x1bOH", curses.KEY_LEFT), + (u"\x1bOF", curses.KEY_END), + (u"\x1bOP", curses.KEY_F1), + (u"\x1bOQ", curses.KEY_F2), + (u"\x1bOR", curses.KEY_F3), + (u"\x1bOS", curses.KEY_F4), + # typical for vt220-derived terminals, just in case our terminal + # database reported something different, + (u"\x1b[A", curses.KEY_UP), + (u"\x1b[B", curses.KEY_DOWN), + (u"\x1b[C", curses.KEY_RIGHT), + (u"\x1b[D", curses.KEY_LEFT), + (u"\x1b[U", curses.KEY_NPAGE), + (u"\x1b[V", curses.KEY_PPAGE), + (u"\x1b[H", curses.KEY_HOME), + (u"\x1b[F", curses.KEY_END), + (u"\x1b[K", curses.KEY_END), + # atypical, + # (u"\x1bA", curses.KEY_UP), + # (u"\x1bB", curses.KEY_DOWN), + # (u"\x1bC", curses.KEY_RIGHT), + # (u"\x1bD", curses.KEY_LEFT), + # rxvt, + (u"\x1b?r", curses.KEY_DOWN), + (u"\x1b?x", curses.KEY_UP), + (u"\x1b?v", curses.KEY_RIGHT), + (u"\x1b?t", curses.KEY_LEFT), + (u"\x1b[@", curses.KEY_IC), +) diff --git a/blessed/sequences.py b/blessed/sequences.py new file mode 100644 index 00000000..a095115d --- /dev/null +++ b/blessed/sequences.py @@ -0,0 +1,442 @@ +""" This sub-module provides 'sequence awareness' for blessings. +""" + +__author__ = 'Jeff Quast ' +__license__ = 'MIT' + +__all__ = ['init_sequence_patterns', 'Sequence', 'SequenceTextWrapper'] + +import functools +import textwrap +import warnings +import math +import re + +_BINTERM_UNSUPPORTED = ('kermit', 'avatar') +_BINTERM_UNSUPPORTED_MSG = ('sequence-awareness for terminals emitting ' + 'binary-packed capabilities are not supported.') + +def _merge_sequences(inp): + """Merge a list of input sequence patterns for use in a regular expression. + Order by lengthyness (full sequence set precident over subset), + and exclude any empty (u'') sequences. + """ + return sorted(list(filter(None, inp)), key=len, reverse=True) + + +def _build_numeric_capability(term, cap, optional=False, base_num=99, nparams=1): + """ Build regexp from capabilities having matching numeric + parameter contained within termcap value: n->(\d+). + """ + _cap = getattr(term, cap) + opt = '?' if optional else '' + if _cap: + cap_re = re.escape(_cap(*((base_num,) * nparams))) + for num in range(base_num-1, base_num+2): + # search for matching ascii, n-1 through n+2 + if str(num) in cap_re: + # modify & return n to matching digit expression + cap_re = cap_re.replace(str(num), r'(\d+)%s' % (opt,)) + return cap_re + warnings.warn('Unknown parameter in %r, %r' % (cap, cap_re)) + return None # no such capability + + +def _build_any_numeric_capability(term, cap, num=99, nparams=1): + """ Build regexp from capabilities having *any* digit parameters + (substitute matching \d with pattern \d and return). + """ + _cap = getattr(term, cap) + if _cap: + cap_re = re.escape(_cap(*((num,) * nparams))) + cap_re = re.sub('(\d+)', r'(\d+)', cap_re) + if r'(\d+)' in cap_re: + return cap_re + warnings.warn('Missing numerics in %r, %r' % (cap, cap_re)) + return None # no such capability + + +def init_sequence_patterns(term): + """ Given a Terminal instance, ``term``, this function processes + and parses several known terminal capabilities, and builds a + database of regular expressions and attatches them to ``term`` + as attributes: + ``_re_will_move``: any sequence matching this pattern will + cause the terminal cursor to move (such as term.home). + ``_re_wont_move``: any sequence matching this pattern will + not cause the cursor to move (such as term.bold). + ``_re_cuf``: regular expression that matches term.cuf(N) + (move N characters forward). + ``_cuf1``: term.cuf1 sequence (cursor forward 1 character) + as a static value. + ``_re_cub``: regular expression that matches term.cub(N) + (move N characters backward). + ``_cub1``: term.cuf1 sequence (cursor backward 1 character) + as a static value. + + These attribtues make it possible to perform introspection on + strings containing sequences generated by this terminal, to determine + the printable length of a string. + + For debugging, complimentary lists of these sequence matching + pattern values prior to compilation are attached as attributes + ``_will_move``, ``_wont_move``, ``_cuf``, ``_cub``. + """ + if term._kind in _BINTERM_UNSUPPORTED: + warnings.warn(_BINTERM_UNSUPPORTED_MSG) + + bnc = functools.partial(_build_numeric_capability, term=term) + bna = functools.partial(_build_any_numeric_capability, term=term) + # Build will_move, a list of terminal capabilities that have + # indeterminate effects on the terminal cursor position. + will_move_seqs = set([ + # carriage_return + re.escape(term.cr), + # column_address: Horizontal position, absolute + bnc(cap='hpa'), + # row_address: Vertical position #1 absolute + bnc(cap='vpa'), + # cursor_address: Move to row #1 columns #2 + bnc(cap='cup', nparams=2), + # cursor_down: Down one line + re.escape(term.cud1), + # cursor_home: Home cursor (if no cup) + re.escape(term.home), + # cursor_left: Move left one space + re.escape(term.cub1), + # cursor_right: Non-destructive space (move right one space) + re.escape(term.cuf1), + # cursor_up: Up one line + re.escape(term.cuu1), + # param_down_cursor: Down #1 lines + bnc(cap='cud', optional=True), + # restore_cursor: Restore cursor to position of last save_cursor + re.escape(term.rc), + # clear_screen: clear screen and home cursor + re.escape(term.clear), + # cursor_up: Up one line + re.escape(term.enter_fullscreen), + re.escape(term.exit_fullscreen), + # forward cursor + term._cuf, + # backward cursor + term._cub, + ]) + + # Build wont_move, a list of terminal capabilities that mainly affect + # video attributes, for use with measure_length(). + wont_move_seqs = list([ + # print_screen: Print contents of screen + re.escape(term.mc0), + # prtr_off: Turn off printer + re.escape(term.mc4), + # prtr_on: Turn on printer + re.escape(term.mc5), + # save_cursor: Save current cursor position (P) + re.escape(term.sc), + # set_tab: Set a tab in every row, current columns + re.escape(term.hts), + # enter_bold_mode: Turn on bold (extra bright) mode + re.escape(term.bold), + # enter_standout_mode + re.escape(term.standout), + # enter_subscript_mode + re.escape(term.subscript), + # enter_superscript_mode + re.escape(term.superscript), + # enter_underline_mode: Begin underline mode + re.escape(term.underline), + # enter_blink_mode: Turn on blinking + re.escape(term.blink), + # enter_dim_mode: Turn on half-bright mode + re.escape(term.dim), + # cursor_invisible: Make cursor invisible + re.escape(term.civis), + # cursor_visible: Make cursor very visible + re.escape(term.cvvis), + # cursor_normal: Make cursor appear normal (undo civis/cvvis) + re.escape(term.cnorm), + # clear_all_tabs: Clear all tab stops + re.escape(term.tbc), + # change_scroll_region: Change region to line #1 to line #2 + bnc(cap='csr', nparams=2), + # clr_bol: Clear to beginning of line + re.escape(term.el1), + # clr_eol: Clear to end of line + re.escape(term.el), + # clr_eos: Clear to end of screen + re.escape(term.clear_eos), + # delete_character: Delete character + re.escape(term.dch1), + # delete_line: Delete line (P*) + re.escape(term.dl1), + # erase_chars: Erase #1 characters + bnc(cap='ech'), + # insert_line: Insert line (P*) + re.escape(term.il1), + # parm_dch: Delete #1 characters + bnc(cap='dch'), + # parm_delete_line: Delete #1 lines + bnc(cap='dl'), + # exit_alt_charset_mode: End alternate character set (P) + re.escape(term.rmacs), + # exit_am_mode: Turn off automatic margins + re.escape(term.rmam), + # exit_attribute_mode: Turn off all attributes + re.escape(term.sgr0), + # exit_ca_mode: Strings to end programs using cup + re.escape(term.rmcup), + # exit_insert_mode: Exit insert mode + re.escape(term.rmir), + # exit_standout_mode: Exit standout mode + re.escape(term.rmso), + # exit_underline_mode: Exit underline mode + re.escape(term.rmul), + # flash_hook: Flash switch hook + re.escape(term.hook), + # flash_screen: Visible bell (may not move cursor) + re.escape(term.flash), + # keypad_local: Leave 'keyboard_transmit' mode + re.escape(term.rmkx), + # keypad_xmit: Enter 'keyboard_transmit' mode + re.escape(term.smkx), + # meta_off: Turn off meta mode + re.escape(term.rmm), + # meta_on: Turn on meta mode (8th-bit on) + re.escape(term.smm), + # orig_pair: Set default pair to its original value + re.escape(term.op), + # parm_ich: Insert #1 characters + bnc(cap='ich'), + # parm_index: Scroll forward #1 + bnc(cap='indn'), + # parm_insert_line: Insert #1 lines + bnc(cap='il'), + # erase_chars: Erase #1 characters + bnc(cap='ech'), + # parm_rindex: Scroll back #1 lines + bnc(cap='rin'), + # parm_up_cursor: Up #1 lines + bnc(cap='cuu'), + # scroll_forward: Scroll text up (P) + re.escape(term.ind), + # scroll_reverse: Scroll text down (P) + re.escape(term.rev), + # tab: Tab to next 8-space hardware tab stop + re.escape(term.ht), + # set_a_background: Set background color to #1, using ANSI escape + bna(cap='setab', num=1), + bna(cap='setab', num=(term.number_of_colors - 1)), + # set_a_foreground: Set foreground color to #1, using ANSI escape + bna(cap='setaf', num=1), + bna(cap='setaf', num=(term.number_of_colors - 1)), + ] + [ + # set_attributes: Define video attributes #1-#9 (PG9) + # ( not *exactly* legal, being extra forgiving. ) + bna(cap='sgr', nparams=_num) for _num in range(1, 10) + # reset_{1,2,3}string: Reset string + ] + map(re.escape, (term.r1, term.r2, term.r3,))) + + # store pre-compiled list as '_will_move' and '_wont_move', for debugging + term._will_move = _merge_sequences(will_move_seqs) + term._wont_move = _merge_sequences(wont_move_seqs) + + # compile as regular expressions, OR'd. + term._re_will_move = re.compile('(%s)' % ('|'.join(term._will_move))) + term._re_wont_move = re.compile('(%s)' % ('|'.join(term._wont_move))) + + # static pattern matching for _horiontal_distance + # + # parm_right_cursor: Move #1 characters to the right + term._cuf = bnc(cap='cuf', optional=True) + term._re_cuf = re.compile(term._cuf) if term._cuf else None + # cursor_right: Non-destructive space (move right one space) + term._cuf1 = term.cuf1 + # parm_left_cursor: Move #1 characters to the left + term._cub = bnc(cap='cub', optional=True) + term._re_cub = re.compile(term._cub) if term._cub else None + # cursor_left: Move left one space + term._cub1 = term.cub1 + + +class SequenceTextWrapper(textwrap.TextWrapper): + def __init__(self, width, term, **kwargs): + self.term = term + assert kwargs.get('break_long_words', False) is False, ( + 'break_long_words is not sequence-safe') + kwargs['break_long_words'] = False + textwrap.TextWrapper.__init__(self, width, **kwargs) + + def _wrap_chunks(self, chunks): + """ + escape-sequence aware varient of _wrap_chunks. Though + movement sequences, such as term.left() are certainly not + honored, sequences such as term.bold() are, and are not + broken mid-sequence. + """ + lines = [] + if self.width <= 0 or not isinstance(self.width, int): + raise ValueError("invalid width %r(%s) (must be integer > 0)" % ( + self.width, type(self.width))) + chunks.reverse() + while chunks: + cur_line = [] + cur_len = 0 + if lines: + indent = self.subsequent_indent + else: + indent = self.initial_indent + width = self.width - len(indent) + if (not hasattr(self, 'drop_whitespace') + or self.drop_whitespace) and ( + chunks[-1].strip() == '' and lines): + del chunks[-1] + while chunks: + chunk_len = Sequence(chunks[-1], self.term).length() + if cur_len + chunk_len <= width: + cur_line.append(chunks.pop()) + cur_len += chunk_len + else: + break + if chunks and Sequence(chunks[-1], self.term).length() > width: + self._handle_long_word(chunks, cur_line, cur_len, width) + if (not hasattr(self, 'drop_whitespace') + or self.drop_whitespace) and ( + cur_line and cur_line[-1].strip() == ''): + del cur_line[-1] + if cur_line: + lines.append(indent + u''.join(cur_line)) + return lines + + +class Sequence(unicode): + """ + This unicode-derived class understands the effect of escape sequences + of printable length, allowing a properly implemented .rjust(), .ljust(), + .center(), and .len() + """ + + def __new__(cls, sequence_text, term): + new = unicode.__new__(cls, sequence_text) + new._term = term + return new + + def ljust(self, width, fillchar=u' '): + return self + fillchar * (max(0, width - self.length())) + + def rjust(self, width, fillchar=u' '): + return fillchar * (max(0, width - self.length())) + self + + def center(self, width, fillchar=u' '): + split = max(0.0, float(width) - self.length()) / 2 + return (fillchar * (max(0, int(math.floor(split)))) + self + + fillchar * (max(0, int(math.ceil(split))))) + + def length(self): + """ S.length() -> integer + + Return the printable length of a string that contains (some types) of + (escape) sequences. Although accounted for, strings containing + sequences such as 'clear' will not give accurate returns, it is + considered un-lengthy (length of 0). + """ + # nxt: points to first character beyond current escape sequence. + # width: currently estimated display length. + nxt = width = 0 + for idx in range(0, unicode.__len__(self)): + # account for width of sequences that contain padding (a sort of + # SGR-equivalent cheat for the python equivalent of ' '*N, for + # very large values of N that may xmit fewer bytes than many raw + # spaces. It should be noted, however, that this is a + # non-destructive space. + width += horizontal_distance(self[idx:], self._term) + if idx == nxt: + # point beyond this sequence + nxt = idx + measure_length(self[idx:], self._term) + if nxt <= idx: + # TODO: + # 'East Asian Fullwidth' and 'East Asian Wide' characters + # can take 2 cells, see http://www.unicode.org/reports/tr11/ + # and http://www.gossamer-threads.com/lists/python/bugs/972834 + width += 1 + # point beyond next sequence, if any, otherwise next character + nxt = idx + measure_length(self[idx:], self._term) + 1 + return width + + +def measure_length(ucs, term): + """ + measure_length(S) -> integer + + Returns non-zero for string ``S`` that begins with a terminal sequence, + that is: the width of the first unprintable sequence found in S. For use + as a *next* pointer to skip past sequences. If string ``S`` is not a + sequence, 0 is returned. + + A sequence may be a typical terminal sequence beginning with Escape (\x1b), + especially a Control Sequence Initiator ("CSI", '\x1b[' ... ), or those of + '\a', '\b', '\r', '\n', '\xe0' (shift in), '\x0f' (shift out). They do not + necessarily have to begin with CSI, they need only match the capabilities + of attributes ``_re_will_move`` and ``_re_wont_move`` of terminal ``term``. + """ + # simple terminal control characters, + ctrl_seqs = u'\a\b\r\n\x0e\x0f' + if any([ucs.startswith(_ch) for _ch in ctrl_seqs]): + return 1 + # known multibyte sequences, + matching_seq = term and ( + term._re_will_move.match(ucs) or + term._re_wont_move.match(ucs) or + term._re_cub and term._re_cub.match(ucs) or + term._re_cuf and term._re_cuf.match(ucs) + ) + if matching_seq: + start, end = matching_seq.span() + return end + # none found, must be printable! + return 0 + + +def horizontal_distance(ucs, term): + """ horizontal_distance(S) -> integer + + Returns Integer in SGR sequence of form [C (T.move_right(nn)). + Returns Integer -(n) in SGR sequence of form [D (T.move_left(nn)). + Returns -1 for backspace (0x08), Otherwise 0. + + Tabstop (\t) cannot be correctly calculated, as the relative column + position cannot be determined: 8 is always (and, incorrectly) returned. + """ + + def term_distance(cap, unit): + """ Match by simple cub1/cuf1 string matching (distance of 1) + Or, by regular expression (using dynamic regular expressions + built using cub(n) and cuf(n). Failing that, the standard + SGR sequences (\033[C, \033[D, \033[nC, \033[nD + """ + assert cap in ('cuf', 'cub') + # match cub1(left), cuf1(right) + one = getattr(term, '_%s1' % (cap,)) + if one and ucs.startswith(one): + return unit + + # match cub(n), cuf(n) using regular expressions + re_pattern = getattr(term, '_re_%s' % (cap,)) + _dist = re_pattern and re_pattern.match(ucs) + if _dist: + return unit * int(_dist.group(1)) + + if ucs.startswith('\b'): + return -1 + + elif ucs.startswith('\t'): + # as best as I can prove it, a tabstop is always 8 by default. + # Though, given that blessings is: + # 1. unaware of the output device's current cursor position, and + # 2. unaware of the location the callee may chose to output any + # given string, + # It is not possible to determine how many cells any particular + # \t would consume on the output device! + return 8 + + return term_distance('cub', -1) or term_distance('cuf', 1) or 0 diff --git a/blessings/tests.py b/blessings/tests.py deleted file mode 100644 index aff3a2f2..00000000 --- a/blessings/tests.py +++ /dev/null @@ -1,270 +0,0 @@ -# -*- coding: utf-8 -*- -"""Automated tests (as opposed to human-verified test patterns) - -It was tempting to mock out curses to get predictable output from ``tigetstr``, -but there are concrete integration-testing benefits in not doing so. For -instance, ``tigetstr`` changed its return type in Python 3.2.3. So instead, we -simply create all our test ``Terminal`` instances with a known terminal type. -All we require from the host machine is that a standard terminfo definition of -xterm-256color exists. - -""" -from __future__ import with_statement # Make 2.5-compatible -from curses import tigetstr, tparm -from functools import partial -from StringIO import StringIO -import sys - -from nose import SkipTest -from nose.tools import eq_ - -# This tests that __all__ is correct, since we use below everything that should -# be imported: -from blessings import * - - -TestTerminal = partial(Terminal, kind='xterm-256color') - - -def unicode_cap(cap): - """Return the result of ``tigetstr`` except as Unicode.""" - return tigetstr(cap).decode('latin1') - - -def unicode_parm(cap, *parms): - """Return the result of ``tparm(tigetstr())`` except as Unicode.""" - return tparm(tigetstr(cap), *parms).decode('latin1') - - -def test_capability(): - """Check that a capability lookup works. - - Also test that Terminal grabs a reasonable default stream. This test - assumes it will be run from a tty. - - """ - t = TestTerminal() - sc = unicode_cap('sc') - eq_(t.save, sc) - eq_(t.save, sc) # Make sure caching doesn't screw it up. - - -def test_capability_without_tty(): - """Assert capability templates are '' when stream is not a tty.""" - t = TestTerminal(stream=StringIO()) - eq_(t.save, u'') - eq_(t.red, u'') - - -def test_capability_with_forced_tty(): - """If we force styling, capabilities had better not (generally) be - empty.""" - t = TestTerminal(stream=StringIO(), force_styling=True) - eq_(t.save, unicode_cap('sc')) - - -def test_parametrization(): - """Test parametrizing a capability.""" - eq_(TestTerminal().cup(3, 4), unicode_parm('cup', 3, 4)) - - -def test_height_and_width(): - """Assert that ``height_and_width()`` returns ints.""" - t = TestTerminal() # kind shouldn't matter. - assert isinstance(t.height, int) - assert isinstance(t.width, int) - - -def test_stream_attr(): - """Make sure Terminal exposes a ``stream`` attribute that defaults to - something sane.""" - eq_(Terminal().stream, sys.__stdout__) - - -def test_location(): - """Make sure ``location()`` does what it claims.""" - t = TestTerminal(stream=StringIO(), force_styling=True) - - with t.location(3, 4): - t.stream.write(u'hi') - - eq_(t.stream.getvalue(), unicode_cap('sc') + - unicode_parm('cup', 4, 3) + - u'hi' + - unicode_cap('rc')) - - -def test_horizontal_location(): - """Make sure we can move the cursor horizontally without changing rows.""" - t = TestTerminal(stream=StringIO(), force_styling=True) - with t.location(x=5): - pass - eq_(t.stream.getvalue(), unicode_cap('sc') + - unicode_parm('hpa', 5) + - unicode_cap('rc')) - - -def test_null_location(): - """Make sure ``location()`` with no args just does position restoration.""" - t = TestTerminal(stream=StringIO(), force_styling=True) - with t.location(): - pass - eq_(t.stream.getvalue(), unicode_cap('sc') + - unicode_cap('rc')) - - -def test_zero_location(): - """Make sure ``location()`` pays attention to 0-valued args.""" - t = TestTerminal(stream=StringIO(), force_styling=True) - with t.location(0, 0): - pass - eq_(t.stream.getvalue(), unicode_cap('sc') + - unicode_parm('cup', 0, 0) + - unicode_cap('rc')) - - -def test_null_fileno(): - """Make sure ``Terminal`` works when ``fileno`` is ``None``. - - This simulates piping output to another program. - - """ - out = StringIO() - out.fileno = None - t = TestTerminal(stream=out) - eq_(t.save, u'') - - -def test_mnemonic_colors(): - """Make sure color shortcuts work.""" - def color(num): - return unicode_parm('setaf', num) - - def on_color(num): - return unicode_parm('setab', num) - - # Avoid testing red, blue, yellow, and cyan, since they might someday - # change depending on terminal type. - t = TestTerminal() - eq_(t.white, color(7)) - eq_(t.green, color(2)) # Make sure it's different than white. - eq_(t.on_black, on_color(0)) - eq_(t.on_green, on_color(2)) - eq_(t.bright_black, color(8)) - eq_(t.bright_green, color(10)) - eq_(t.on_bright_black, on_color(8)) - eq_(t.on_bright_green, on_color(10)) - - -def test_callable_numeric_colors(): - """``color(n)`` should return a formatting wrapper.""" - t = TestTerminal() - eq_(t.color(5)('smoo'), t.magenta + 'smoo' + t.normal) - eq_(t.color(5)('smoo'), t.color(5) + 'smoo' + t.normal) - eq_(t.on_color(2)('smoo'), t.on_green + 'smoo' + t.normal) - eq_(t.on_color(2)('smoo'), t.on_color(2) + 'smoo' + t.normal) - - -def test_null_callable_numeric_colors(): - """``color(n)`` should be a no-op on null terminals.""" - t = TestTerminal(stream=StringIO()) - eq_(t.color(5)('smoo'), 'smoo') - eq_(t.on_color(6)('smoo'), 'smoo') - - -def test_naked_color_cap(): - """``term.color`` should return a stringlike capability.""" - t = TestTerminal() - eq_(t.color + '', t.setaf + '') - - -def test_number_of_colors_without_tty(): - """``number_of_colors`` should return 0 when there's no tty.""" - # Hypothesis: once setupterm() has run and decided the tty supports 256 - # colors, it never changes its mind. - raise SkipTest - - t = TestTerminal(stream=StringIO()) - eq_(t.number_of_colors, 0) - t = TestTerminal(stream=StringIO(), force_styling=True) - eq_(t.number_of_colors, 0) - - -def test_number_of_colors_with_tty(): - """``number_of_colors`` should work.""" - t = TestTerminal() - eq_(t.number_of_colors, 256) - - -def test_formatting_functions(): - """Test crazy-ass formatting wrappers, both simple and compound.""" - t = TestTerminal() - # By now, it should be safe to use sugared attributes. Other tests test - # those. - eq_(t.bold(u'hi'), t.bold + u'hi' + t.normal) - eq_(t.green('hi'), t.green + u'hi' + t.normal) # Plain strs for Python 2.x - # Test some non-ASCII chars, probably not necessary: - eq_(t.bold_green(u'boö'), t.bold + t.green + u'boö' + t.normal) - eq_(t.bold_underline_green_on_red('boo'), - t.bold + t.underline + t.green + t.on_red + u'boo' + t.normal) - # Don't spell things like this: - eq_(t.on_bright_red_bold_bright_green_underline('meh'), - t.on_bright_red + t.bold + t.bright_green + t.underline + u'meh' + - t.normal) - - -def test_formatting_functions_without_tty(): - """Test crazy-ass formatting wrappers when there's no tty.""" - t = TestTerminal(stream=StringIO()) - eq_(t.bold(u'hi'), u'hi') - eq_(t.green('hi'), u'hi') - # Test non-ASCII chars, no longer really necessary: - eq_(t.bold_green(u'boö'), u'boö') - eq_(t.bold_underline_green_on_red('loo'), u'loo') - eq_(t.on_bright_red_bold_bright_green_underline('meh'), u'meh') - - -def test_nice_formatting_errors(): - """Make sure you get nice hints if you misspell a formatting wrapper.""" - t = TestTerminal() - try: - t.bold_misspelled('hey') - except TypeError, e: - assert 'probably misspelled' in e.args[0] - - try: - t.bold_misspelled(u'hey') # unicode - except TypeError, e: - assert 'probably misspelled' in e.args[0] - - try: - t.bold_misspelled(None) # an arbitrary non-string - except TypeError, e: - assert 'probably misspelled' not in e.args[0] - - try: - t.bold_misspelled('a', 'b') # >1 string arg - except TypeError, e: - assert 'probably misspelled' not in e.args[0] - - -def test_init_descriptor_always_initted(): - """We should be able to get a height and width even on no-tty Terminals.""" - t = Terminal(stream=StringIO()) - eq_(type(t.height), int) - - -def test_force_styling_none(): - """If ``force_styling=None`` is passed to the constructor, don't ever do - styling.""" - t = TestTerminal(force_styling=None) - eq_(t.save, '') - - -def test_null_callable_string(): - """Make sure NullCallableString tolerates all numbers and kinds of args it - might receive.""" - t = TestTerminal(stream=StringIO()) - eq_(t.clear, '') - eq_(t.move(1, 2), '') - eq_(t.move_x(1), '') diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..2507f1b6 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,3 @@ +[pytest] +flakes-ignore = + tests/*.py UnusedImport diff --git a/setup.py b/setup.py old mode 100644 new mode 100755 index ec4cfd10..3ab36c7e --- a/setup.py +++ b/setup.py @@ -1,31 +1,65 @@ +#!/usr/bin/env python +from setuptools import setup, find_packages, Command +from setuptools.command.develop import develop import sys - -# Prevent spurious errors during `python setup.py test`, a la -# http://www.eby-sarna.com/pipermail/peak/2010-May/003357.html: -try: - import multiprocessing -except ImportError: - pass - -from setuptools import setup, find_packages - +import os extra_setup = {} if sys.version_info >= (3,): extra_setup['use_2to3'] = True +if sys.version_info <= (2, 7,): + extra_setup['requires'] = ['ordereddict'] + +here = os.path.dirname(__file__) +dev_requirements = ['pytest', 'pytest-cov', 'pytest-pep8', + 'pytest-flakes', 'pytest-sugar', 'mock'] + + +class PyTest(Command): + user_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + import subprocess + test_files = os.path.join(here, 'tests') + errno = subprocess.call(['py.test', '-x', '--strict', + '--pep8', '--flakes', + '--cov', 'blessed', + '--cov-report', 'html', + test_files]) + raise SystemExit(errno) + +class SetupDevelop(develop): + """Setup development environment suitable for testing.""" + + def finalize_options(self): + assert os.getenv('VIRTUAL_ENV'), "Use a virtualenv for this option." + develop.finalize_options(self) + + def run(self): + import subprocess + subprocess.check_call('pip install {reqs}' + .format(reqs=u' '.join(dev_requirements)), + shell=True) + develop.run(self) setup( - name='blessings', - version='1.6', - description='A thin, practical wrapper around terminal coloring, styling, and positioning', + name='blessed', + version='1.7', + description="A feature-filled fork of Erik Rose's blessings project", long_description=open('README.rst').read(), - author='Erik Rose', - author_email='erikrose@grinchcentral.com', + author='Jeff Quast', + author_email='contact@jeffquast.com', license='MIT', packages=find_packages(exclude=['ez_setup']), - tests_require=['nose'], - test_suite='nose.collector', - url='https://github.com/erikrose/blessings', + tests_require=dev_requirements, + cmdclass={'test': PyTest, 'develop': SetupDevelop}, + url='https://github.com/jquast/blessed', include_package_data=True, classifiers=[ 'Intended Audience :: Developers', @@ -45,6 +79,8 @@ 'Topic :: Software Development :: User Interfaces', 'Topic :: Terminals' ], - keywords=['terminal', 'tty', 'curses', 'ncurses', 'formatting', 'style', 'color', 'console'], + keywords=['terminal', 'sequences', 'tty', 'curses', 'ncurses', + 'formatting', 'style', 'color', 'console', 'keyboard', + 'ansi', 'xterm'], **extra_setup ) diff --git a/test_keyboard.py b/test_keyboard.py new file mode 100644 index 00000000..7bcd25af --- /dev/null +++ b/test_keyboard.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python +import blessings +import sys + + +def main(): + """ + Displays all known key capabilities that may match the terminal. + As each key is pressed on input, it is lit up and points are scored. + """ + term = blessings.Terminal() + score = level = hit_highbit = hit_unicode = 0 + dirty = True + + def refresh(term, board, level, score, inp): + sys.stdout.write(term.home + term.clear) + level_color = level % 7 + if level_color == 0: + level_color = 4 + bottom = 0 + for keycode, attr in board.iteritems(): + sys.stdout.write(u''.join(( + term.move(attr['row'], attr['column']), + term.color(level_color), + (term.reverse if attr['hit'] else term.bold), + keycode, + term.normal))) + bottom = max(bottom, attr['row']) + sys.stdout.write(term.move(term.height, 0) + + 'level: %s score: %s' % (level, score,)) + sys.stdout.flush() + if bottom >= (term.height - 5): + sys.stderr.write( + '\n' * (term.height / 2) + + term.center(term.red_underline('cheater!')) + '\n') + sys.stderr.write( + term.center("(use a larger screen)") + + '\n' * (term.height / 2)) + sys.exit(1) + for row, inp in enumerate(inps[(term.height - (bottom + 2)) * -1:]): + sys.stdout.write(term.move(bottom + row+1)) + sys.stdout.write('%r, %s, %s' % (inp.__str__() if inp.is_sequence + else inp, inp.code, inp.name, )) + sys.stdout.flush() + + def build_gameboard(term): + column, row = 0, 0 + board = dict() + spacing = 2 + for keycode in sorted(term._keyboard_seqnames.values()): + if (keycode.startswith('KEY_F') + and keycode[-1].isdigit() + and int(keycode[len('KEY_F'):]) > 24): + continue + if column + len(keycode) + (spacing * 2) >= term.width: + column = 0 + row += 1 + board[keycode] = {'column': column, + 'row': row, + 'hit': 0, + } + column += len(keycode) + (spacing * 2) + return board + + def add_score(score, pts, level): + lvl_multiplier = 10 + score += pts + if 0 == (score % (pts * lvl_multiplier)): + level += 1 + return score, level + + gb = build_gameboard(term) + inps = [] + + with term.key_at_a_time(): + inp = term.keypress(timeout=0) + while inp.upper() != 'Q': + if dirty: + refresh(term, gb, level, score, inps) + dirty = False + inp = term.keypress(timeout=5.0) + dirty = True + if (inp.is_sequence and + inp.name in gb and + 0 == gb[inp.name]['hit']): + gb[inp.name]['hit'] = 1 + score, level = add_score(score, 100, level) + elif inp and not inp.is_sequence and 128 <= ord(inp) <= 255: + hit_highbit += 1 + if hit_highbit < 5: + score, level = add_score(score, 100, level) + elif inp and not inp.is_sequence and ord(inp) > 256: + hit_unicode += 1 + if hit_unicode < 5: + score, level = add_score(score, 100, level) + inps.append(inp) + + sys.stdout.write(u''.join(( + term.move(term.height), + term.clear_eol, + u'Your final score was %s' % (score,), + u' at level %s' % (level,), + term.clear_eol, + u'\n', + term.clear_eol, + u'You hit %s' % (hit_highbit,), + u' 8-bit (extended ascii) characters', + term.clear_eol, + u'\n', + term.clear_eol, + u'You hit %s' % (hit_unicode,), + u' unicode characters.', + term.clear_eol, + u'\n', + term.clear_eol, + u'press any key', + term.clear_eol,)) + ) + term.keypress() + +if __name__ == '__main__': + main() diff --git a/tests/accessories.py b/tests/accessories.py new file mode 100644 index 00000000..b23e732a --- /dev/null +++ b/tests/accessories.py @@ -0,0 +1,199 @@ +# -*- coding: utf-8 -*- +"""Accessories for automated tests.""" +from __future__ import with_statement +import contextlib +import functools +import traceback +import termios +import codecs +import curses +import sys +import pty +import os + +from blessed import Terminal + +import pytest + +TestTerminal = functools.partial(Terminal, kind='xterm-256color') +SEND_SEMAPHORE = SEMAPHORE = u'SEMAPHORE\n'.encode('ascii') +RECV_SEMAPHORE = u'%s\r\n' % (SEMAPHORE.rstrip(),) +all_xterms_params = ['xterm', 'xterm-256color'] +all_terms_params = ['screen', 'vt220', 'rxvt', 'cons25', 'linux', 'ansi'] +binpacked_terminal_params = ['avatar', 'kermit'] +many_lines_params = [30, 100] +many_columns_params = [5, 30, 150, 500] +if os.environ.get('TRAVIS', None) is None: + # TRAVIS-CI has a limited type of terminals, the others ... + all_terms_params.extend(['avatar', 'kermit', 'dtterm', 'wyse520', + 'minix', 'eterm', 'aixterm', 'putty']) +all_standard_terms_params = (set(all_terms_params) - + set(binpacked_terminal_params)) + + +class as_subprocess(object): + """This helper executes test cases in a child process, + avoiding a python-internal bug of _curses: setupterm() + may not be called more than once per process. + """ + _CHILD_PID = 0 + encoding = 'utf8' + + def __init__(self, func): + self.func = func + + def __call__(self, *args, **kwargs): + pid, master_fd = pty.fork() + if pid is self._CHILD_PID: + # child process executes function, raises exception + # if failed, causing a non-zero exit code, using the + # protected _exit() function of ``os``; to prevent the + # 'SystemExit' exception from being thrown. + try: + self.func(*args, **kwargs) + except Exception: + e_type, e_value, e_tb = sys.exc_info() + o_err = list() + for line in traceback.format_tb(e_tb): + o_err.append(line.rstrip().encode('utf-8')) + o_err.append(('-=' * 20).encode('ascii')) + o_err.extend([_exc.rstrip().encode('utf-8') for _exc in + traceback.format_exception_only( + e_type, e_value)]) + os.write(sys.__stdout__.fileno(), '\n'.join(o_err)) + os.close(sys.__stdout__.fileno()) + os.close(sys.__stderr__.fileno()) + os.close(sys.__stdin__.fileno()) + os._exit(1) + else: + os._exit(0) + + exc_output = unicode() + decoder = codecs.getincrementaldecoder(self.encoding)() + while True: + try: + _exc = os.read(master_fd, 65534) + except OSError: + # linux EOF + break + if not _exc: + # bsd EOF + break + exc_output += decoder.decode(_exc) + + # parent process asserts exit code is 0, causing test + # to fail if child process raised an exception/assertion + pid, status = os.waitpid(pid, 0) + os.close(master_fd) + + # Display any output written by child process + # (esp. any AssertionError exceptions written to stderr). + exc_output_msg = 'Output in child process:\n%s\n%s\n%s' % ( + u'=' * 40, exc_output, u'=' * 40,) + assert exc_output == '', exc_output_msg + + # Also test exit status is non-zero + assert os.WEXITSTATUS(status) == 0 + + +def read_until_semaphore(fd, semaphore=RECV_SEMAPHORE, + encoding='utf8', timeout=10): + """Read file descriptor ``fd`` until ``semaphore`` is found.""" + # note that when a child process writes xyz\\n, the parent + # process will ready xyz\\r\\n -- this is how pseudo terminals + # behave; a virtual terminal requires both carriage return and + # line feed, it is only for convenience that \\n does both. + # + # used to ensure the child process is awake and ready, for timing + # tests; without a semaphore, the time to fork() would be (incorrectly) + # included in the duration of the test, which can be very length on + # continuous integration servers such as Travis. + outp = unicode() + decoder = codecs.getincrementaldecoder(encoding)() + + while not outp.startswith(semaphore): + try: + _exc = os.read(fd, 1) + except OSError: # linux EOF + break + if not _exc: # bsd EOF + break + outp += decoder.decode(_exc, final=False) + assert outp.startswith(semaphore), ( + 'Semaphore not recv before EOF ' + '(expected %r, got %r)' % (semaphore, outp,)) + return outp[len(semaphore):] + + +def read_until_eof(fd, encoding='utf8'): + """Read file descriptor ``fd`` until EOF. Return decoded string.""" + decoder = codecs.getincrementaldecoder(encoding)() + outp = unicode() + while True: + try: + _exc = os.read(fd, 100) + except OSError: # linux EOF + break + if not _exc: # bsd EOF + break + outp += decoder.decode(_exc, final=False) + return outp + + +@contextlib.contextmanager +def echo_off(fd): + """Ensure any bytes written to pty fd are not duplicated as output.""" + try: + attrs = termios.tcgetattr(fd) + attrs[3] = attrs[3] & ~termios.ECHO + termios.tcsetattr(fd, termios.TCSANOW, attrs) + yield + finally: + attrs[3] = attrs[3] | termios.ECHO + termios.tcsetattr(fd, termios.TCSANOW, attrs) + + +def unicode_cap(cap): + """Return the result of ``tigetstr`` except as Unicode.""" + return curses.tigetstr(cap).decode('latin1') + + +def unicode_parm(cap, *parms): + """Return the result of ``tparm(tigetstr())`` except as Unicode.""" + return curses.tparm(curses.tigetstr(cap), *parms).decode('latin1') + + +@pytest.fixture(params=binpacked_terminal_params) +def unsupported_sequence_terminals(request): + """Terminals that emit warnings for unsupported sequence-awareness.""" + return request.param + + +@pytest.fixture(params=all_xterms_params) +def xterms(request): + """Common kind values for xterm terminals.""" + return request.param + + +@pytest.fixture(params=all_terms_params) +def all_terms(request): + """Common kind values for all kinds of terminals.""" + return request.param + + +@pytest.fixture(params=all_standard_terms_params) +def all_standard_terms(request): + """Common kind values for all kinds of terminals (except binary-packed).""" + return request.param + + +@pytest.fixture(params=many_lines_params) +def many_lines(request): + """Various number of lines for screen height.""" + return request.param + + +@pytest.fixture(params=many_columns_params) +def many_columns(request): + """Various number of columns for screen width.""" + return request.param diff --git a/tests/test_keyboard.py b/tests/test_keyboard.py new file mode 100644 index 00000000..7d97a161 --- /dev/null +++ b/tests/test_keyboard.py @@ -0,0 +1,382 @@ +# -*- coding: utf-8 -*- +"""Tests for keyboard support.""" +import curses +import time +import math +import pty +import sys +import os + +from accessories import ( + read_until_eof, + read_until_semaphore, + SEND_SEMAPHORE, + as_subprocess, + TestTerminal, + SEMAPHORE, + all_terms, + echo_off, + xterms, +) + +import mock + + +def test_inkey_0s_noinput(): + """0-second inkey without input; '' should be returned.""" + @as_subprocess + def child(): + term = TestTerminal() + with term.cbreak(): + stime = time.time() + inp = term.inkey(timeout=0) + assert (inp == u'') + assert (math.floor(time.time() - stime) == 0.0) + child() + + +def test_inkey_1s_noinput(): + """1-second inkey without input; '' should be returned after ~1 second.""" + @as_subprocess + def child(): + term = TestTerminal() + with term.cbreak(): + stime = time.time() + inp = term.inkey(timeout=1) + assert (inp == u'') + assert (math.floor(time.time() - stime) == 1.0) + child() + + +def test_inkey_0s_input(): + """0-second inkey with input; Keypress should be immediately returned.""" + pid, master_fd = pty.fork() + if pid is 0: + # child pauses, writes semaphore and begins awaiting input + term = TestTerminal() + read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.cbreak(): + inp = term.inkey(timeout=0) + os.write(sys.__stdout__.fileno(), inp) + os._exit(0) + + with echo_off(master_fd): + os.write(master_fd, SEND_SEMAPHORE) + os.write(master_fd, u'x'.encode('ascii')) + read_until_semaphore(master_fd) + stime = time.time() + output = read_until_eof(master_fd) + + pid, status = os.waitpid(pid, 0) + assert (output == u'x') + assert (os.WEXITSTATUS(status) == 0) + assert (math.floor(time.time() - stime) == 0.0) + + +def test_inkey_0s_multibyte_utf8(): + """0-second inkey with multibte utf-8 input; should decode immediately.""" + # utf-8 bytes represent "latin capital letter upsilon". + pid, master_fd = pty.fork() + if pid is 0: # child + term = TestTerminal() + read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.cbreak(): + inp = term.inkey(timeout=0) + os.write(sys.__stdout__.fileno(), inp.encode('utf-8')) + os._exit(0) + + with echo_off(master_fd): + os.write(master_fd, SEND_SEMAPHORE) + os.write(master_fd, u'\u01b1'.encode('utf-8')) + read_until_semaphore(master_fd) + stime = time.time() + output = read_until_eof(master_fd) + pid, status = os.waitpid(pid, 0) + assert (output == u'Ʊ') + assert (os.WEXITSTATUS(status) == 0) + assert (math.floor(time.time() - stime) == 0.0) + + +def test_inkey_0s_sequence(): + """0-second inkey with multibte sequence; should decode immediately.""" + pid, master_fd = pty.fork() + if pid is 0: # child + term = TestTerminal() + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.cbreak(): + inp = term.inkey(timeout=0) + os.write(sys.__stdout__.fileno(), ('%s' % (inp.name,))) + sys.stdout.flush() + os._exit(0) + + with echo_off(master_fd): + os.write(master_fd, u'\x1b[D'.encode('ascii')) + read_until_semaphore(master_fd) + stime = time.time() + output = read_until_eof(master_fd) + pid, status = os.waitpid(pid, 0) + assert (output == u'KEY_LEFT') + assert (os.WEXITSTATUS(status) == 0) + assert (math.floor(time.time() - stime) == 0.0) + + +def test_inkey_1s_input(): + """1-second inkey w/multibte sequence; should return after ~1 second.""" + pid, master_fd = pty.fork() + if pid is 0: # child + term = TestTerminal() + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.cbreak(): + inp = term.inkey(timeout=3) + os.write(sys.__stdout__.fileno(), ('%s' % (inp.name,))) + sys.stdout.flush() + os._exit(0) + + with echo_off(master_fd): + read_until_semaphore(master_fd) + stime = time.time() + time.sleep(1) + os.write(master_fd, u'\x1b[C'.encode('ascii')) + output = read_until_eof(master_fd) + + pid, status = os.waitpid(pid, 0) + assert (output == u'KEY_RIGHT') + assert (os.WEXITSTATUS(status) == 0) + assert (math.floor(time.time() - stime) == 1.0) + + +def test_esc_delay_035(): + """esc_delay will cause a single ESC (\\x1b) to delay for 0.35.""" + pid, master_fd = pty.fork() + if pid is 0: # child + term = TestTerminal() + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.cbreak(): + stime = time.time() + inp = term.inkey(timeout=5) + os.write(sys.__stdout__.fileno(), ('%s %i' % ( + inp.name, (time.time() - stime) * 100,))) + sys.stdout.flush() + os._exit(0) + + with echo_off(master_fd): + read_until_semaphore(master_fd) + stime = time.time() + os.write(master_fd, u'\x1b'.encode('ascii')) + key_name, duration_ms = read_until_eof(master_fd).split() + + pid, status = os.waitpid(pid, 0) + assert (key_name == u'KEY_ESCAPE') + assert (os.WEXITSTATUS(status) == 0) + assert (math.floor(time.time() - stime) == 0.0) + assert 35 <= int(duration_ms) <= 45, duration_ms + + +def test_esc_delay_135(): + """esc_delay=1.35 will cause a single ESC (\\x1b) to delay for 1.35.""" + pid, master_fd = pty.fork() + if pid is 0: # child + term = TestTerminal() + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.cbreak(): + stime = time.time() + inp = term.inkey(timeout=5, esc_delay=1.35) + os.write(sys.__stdout__.fileno(), ('%s %i' % ( + inp.name, (time.time() - stime) * 100,))) + sys.stdout.flush() + os._exit(0) + + with echo_off(master_fd): + read_until_semaphore(master_fd) + stime = time.time() + os.write(master_fd, u'\x1b'.encode('ascii')) + key_name, duration_ms = read_until_eof(master_fd).split() + + pid, status = os.waitpid(pid, 0) + assert (key_name == u'KEY_ESCAPE') + assert (os.WEXITSTATUS(status) == 0) + assert (math.floor(time.time() - stime) == 1.0) + assert 135 <= int(duration_ms) <= 145, int(duration_ms) + + +def test_esc_delay_timout_0(): + """esc_delay still in effect with timeout of 0 ("nonblocking").""" + pid, master_fd = pty.fork() + if pid is 0: # child + term = TestTerminal() + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.cbreak(): + stime = time.time() + inp = term.inkey(timeout=0) + os.write(sys.__stdout__.fileno(), ('%s %i' % ( + inp.name, (time.time() - stime) * 100,))) + sys.stdout.flush() + os._exit(0) + + with echo_off(master_fd): + os.write(master_fd, u'\x1b'.encode('ascii')) + read_until_semaphore(master_fd) + stime = time.time() + key_name, duration_ms = read_until_eof(master_fd).split() + + pid, status = os.waitpid(pid, 0) + assert (key_name == u'KEY_ESCAPE') + assert (os.WEXITSTATUS(status) == 0) + assert (math.floor(time.time() - stime) == 0.0) + assert 35 <= int(duration_ms) <= 45, int(duration_ms) + + +def test_no_keystroke(): + """Test keyboard.Keystroke constructor with default arguments.""" + from blessed.keyboard import Keystroke + ks = Keystroke() + assert ks._name is None + assert ks.name == ks._name + assert ks._code is None + assert ks.code == ks._code + assert u'x' == u'x' + ks + assert ks.is_sequence is False + assert repr(ks) == "u''" + + +def test_a_keystroke(): + """Test keyboard.Keystroke constructor with set arguments.""" + from blessed.keyboard import Keystroke + ks = Keystroke(ucs=u'x', code=1, name=u'the X') + assert ks._name is u'the X' + assert ks.name == ks._name + assert ks._code is 1 + assert ks.code == ks._code + assert u'xx' == u'x' + ks + assert ks.is_sequence is True + assert repr(ks) == "the X" + + +def test_get_keyboard_codes(): + """Test all values returned by get_keyboard_codes are from curses.""" + from blessed.keyboard import ( + get_keyboard_codes, + CURSES_KEYCODE_OVERRIDE_MIXIN, + ) + exemptions = dict(CURSES_KEYCODE_OVERRIDE_MIXIN) + for value, keycode in get_keyboard_codes().items(): + if keycode in exemptions: + assert value == exemptions[keycode] + continue + assert hasattr(curses, keycode) + assert getattr(curses, keycode) == value + + +def test_alternative_left_right(): + """Test alternative_left_right behavior for space/backspace.""" + from blessed.keyboard import _alternative_left_right + term = mock.Mock() + term._cuf1 = u'' + term._cub1 = u'' + assert not bool(_alternative_left_right(term)) + term._cuf1 = u' ' + term._cub1 = u'\b' + assert not bool(_alternative_left_right(term)) + term._cuf1 = u'x' + term._cub1 = u'y' + assert (_alternative_left_right(term) == { + u'x': curses.KEY_RIGHT, + u'y': curses.KEY_LEFT}) + + +def test_cuf1_and_cub1_as_RIGHT_LEFT(all_terms): + """Test that cuf1 and cub1 are assigned KEY_RIGHT and KEY_LEFT.""" + from blessed.keyboard import get_keyboard_sequences + + @as_subprocess + def child(kind): + term = TestTerminal(kind=kind, force_styling=True) + keymap = get_keyboard_sequences(term) + if term._cuf1: + assert term._cuf1 != u' ' + assert term._cuf1 in keymap + assert keymap[term._cuf1] == term.KEY_RIGHT + if term._cub1: + assert term._cub1 in keymap + if term._cub1 == '\b': + assert keymap[term._cub1] == term.KEY_BACKSPACE + else: + assert keymap[term._cub1] == term.KEY_LEFT + + child(all_terms) + + +def test_get_keyboard_sequences_sort_order(xterms): + @as_subprocess + def child(): + term = TestTerminal(force_styling=True) + maxlen = None + for sequence, code in term._keymap.items(): + if maxlen is not None: + assert len(sequence) <= maxlen + assert sequence + maxlen = len(sequence) + child() + + +def test_resolve_sequence(): + """Test resolve_sequence for order-dependent mapping.""" + from blessed.keyboard import resolve_sequence, OrderedDict + mapper = OrderedDict(((u'SEQ1', 1), + (u'SEQ2', 2), + # takes precedence over LONGSEQ, first-match + (u'KEY_LONGSEQ_longest', 3), + (u'LONGSEQ', 4), + # wont match, LONGSEQ is first-match in this order + (u'LONGSEQ_longer', 5), + # falls through for L{anything_else} + (u'L', 6))) + codes = {1: u'KEY_SEQ1', + 2: u'KEY_SEQ2', + 3: u'KEY_LONGSEQ_longest', + 4: u'KEY_LONGSEQ', + 5: u'KEY_LONGSEQ_longer', + 6: u'KEY_L'} + ks = resolve_sequence(u'', mapper, codes) + assert ks == u'' + assert ks.name is None + assert ks.code is None + assert ks.is_sequence is False + assert repr(ks) == u"u''" + + ks = resolve_sequence(u'notfound', mapper=mapper, codes=codes) + assert ks == u'n' + assert ks.name is None + assert ks.code is None + assert ks.is_sequence is False + assert repr(ks) == u"u'n'" + + ks = resolve_sequence(u'SEQ1', mapper, codes) + assert ks == u'SEQ1' + assert ks.name == u'KEY_SEQ1' + assert ks.code is 1 + assert ks.is_sequence is True + assert repr(ks) == u"KEY_SEQ1" + + ks = resolve_sequence(u'LONGSEQ_longer', mapper, codes) + assert ks == u'LONGSEQ' + assert ks.name == u'KEY_LONGSEQ' + assert ks.code is 4 + assert ks.is_sequence is True + assert repr(ks) == u"KEY_LONGSEQ" + + ks = resolve_sequence(u'LONGSEQ', mapper, codes) + assert ks == u'LONGSEQ' + assert ks.name == u'KEY_LONGSEQ' + assert ks.code is 4 + assert ks.is_sequence is True + assert repr(ks) == u"KEY_LONGSEQ" + + ks = resolve_sequence(u'Lxxxxx', mapper, codes) + assert ks == u'L' + assert ks.name == u'KEY_L' + assert ks.code is 6 + assert ks.is_sequence is True + assert repr(ks) == u"KEY_L" diff --git a/tests/test_sequence_length.py b/tests/test_sequence_length.py new file mode 100644 index 00000000..57dc6e53 --- /dev/null +++ b/tests/test_sequence_length.py @@ -0,0 +1,188 @@ +import itertools +import termios +import struct +import fcntl +import sys + +from accessories import ( + all_standard_terms, + as_subprocess, + TestTerminal, + many_columns, + many_lines, + all_terms, +) + + +def test_sequence_length(all_terms): + """Ensure T.length(string containing sequence) is correct.""" + @as_subprocess + def child(kind): + t = TestTerminal(kind=kind) + # Create a list of ascii characters, to be seperated + # by word, to be zipped up with a cycling list of + # terminal sequences. Then, compare the length of + # each, the basic plain_text.__len__ vs. the Terminal + # method length. They should be equal. + plain_text = ('The softest things of the world ' + 'Override the hardest things of the world ' + 'That which has no substance ' + 'Enters into that which has no openings') + if t.bold: + assert (t.length(t.bold) == 0) + assert (t.length(t.bold('x')) == 1) + assert (t.length(t.bold_red) == 0) + assert (t.length(t.bold_red('x')) == 1) + if t.underline: + assert (t.length(t.underline) == 0) + assert (t.length(t.underline('x')) == 1) + assert (t.length(t.underline_red) == 0) + assert (t.length(t.underline_red('x')) == 1) + if t.reverse: + assert (t.length(t.reverse) == 0) + assert (t.length(t.reverse('x')) == 1) + assert (t.length(t.reverse_red) == 0) + assert (t.length(t.reverse_red('x')) == 1) + if t.blink: + assert (t.length(t.blink) == 0) + assert (t.length(t.blink('x')) == 1) + assert (t.length(t.blink_red) == 0) + assert (t.length(t.blink_red('x')) == 1) + if t.home: + assert (t.length(t.home) == 0) + if t.clear_eol: + assert (t.length(t.clear_eol) == 0) + if t.enter_fullscreen: + assert (t.length(t.enter_fullscreen) == 0) + if t.exit_fullscreen: + assert (t.length(t.exit_fullscreen) == 0) + + # horizontally, we decide move_down and move_up are 0, + assert (t.length(t.move_down) == 0) + assert (t.length(t.move_down(2)) == 0) + assert (t.length(t.move_up) == 0) + assert (t.length(t.move_up(2)) == 0) + # other things aren't so simple, somewhat edge cases, + # moving backwards and forwards horizontally must be + # accounted for as a "length", as + # will result in a printed column length of 12 (even + # though columns 2-11 are non-destructive space + assert (t.length('\b') == -1) + assert (t.length(t.move_left) == -1) + if t.cub: + assert (t.length(t.cub(10)) == -10) + assert (t.length(t.move_right) == 1) + if t.cuf: + assert (t.length(t.cuf(10)) == 10) + + # vertical spacing is unaccounted as a 'length' + assert (t.length(t.move_up) == 0) + assert (t.length(t.cuu(10)) == 0) + assert (t.length(t.move_down) == 0) + assert (t.length(t.cud(10)) == 0) + # this is how manpages perform underlining, this is done + # with the 'overstrike' capability of teletypes, and aparently + # less(1), '123' -> '1\b_2\b_3\b_' + text_wseqs = u''.join(itertools.chain( + *zip(plain_text, itertools.cycle(['\b_'])))) + assert (t.length(text_wseqs) == len(plain_text)) + + child(all_terms) + + +def test_Sequence_alignment(all_terms, many_lines, many_columns): + """Tests methods related to Sequence class, namely ljust, rjust, center.""" + @as_subprocess + def child(kind, lines=25, cols=80): + # set the pty's virtual window size + val = struct.pack('HHHH', lines, cols, 0, 0) + fcntl.ioctl(sys.__stdout__.fileno(), termios.TIOCSWINSZ, val) + + t = TestTerminal(kind=kind) + pony_msg = 'pony express, all aboard, choo, choo!' + pony_len = len(pony_msg) + pony_colored = u''.join(['%s%s' % (t.color(n % 7), ch,) + for n, ch in enumerate(pony_msg)]) + pony_colored += t.normal + ladjusted = t.ljust(pony_colored) + radjusted = t.rjust(pony_colored) + centered = t.center(pony_colored) + assert (t.length(pony_colored) == pony_len) + assert (t.length(centered.strip()) == pony_len) + assert (t.length(centered) == len(pony_msg.center(t.width))) + assert (t.length(ladjusted.strip()) == pony_len) + assert (t.length(ladjusted) == len(pony_msg.ljust(t.width))) + assert (t.length(radjusted.strip()) == pony_len) + assert (t.length(radjusted) == len(pony_msg.rjust(t.width))) + + child(all_terms, many_lines, many_columns) + + +def test_sequence_is_movement_false(all_terms): + """Test parser about sequences that do not move the cursor.""" + @as_subprocess + def child_mnemonics_wontmove(kind='xterm-256color'): + from blessed.sequences import measure_length + t = TestTerminal(kind=kind) + assert (0 == measure_length(u'', t)) + # not even a mbs + assert (0 == measure_length(u'xyzzy', t)) + # negative numbers, though printable as %d, do not result + # in movement; just garbage. Also not a valid sequence. + assert (0 == measure_length(t.cuf(-333), t)) + assert (len(t.clear_eol) == measure_length(t.clear_eol, t)) + # various erases don't *move* + assert (len(t.clear_bol) == measure_length(t.clear_bol, t)) + assert (len(t.clear_eos) == measure_length(t.clear_eos, t)) + assert (len(t.bold) == measure_length(t.bold, t)) + # various paints don't move + assert (len(t.red) == measure_length(t.red, t)) + assert (len(t.civis) == measure_length(t.civis, t)) + if t.cvvis: + assert (len(t.cvvis) == measure_length(t.cvvis, t)) + assert (len(t.underline) == measure_length(t.underline, t)) + assert (len(t.reverse) == measure_length(t.reverse, t)) + for _num in range(t.number_of_colors): + assert (len(t.color(_num)) == measure_length(t.color(_num), t)) + assert (len(t.normal) == measure_length(t.normal, t)) + assert (len(t.normal_cursor) == measure_length(t.normal_cursor, t)) + assert (len(t.hide_cursor) == measure_length(t.hide_cursor, t)) + assert (len(t.save) == measure_length(t.save, t)) + assert (len(t.italic) == measure_length(t.italic, t)) + assert (len(t.standout) == measure_length(t.standout, t) + ), (t.standout, t._wont_move) + + child_mnemonics_wontmove(all_terms) + + +def test_sequence_is_movement_true(all_standard_terms): + """Test parsers about sequences that move the cursor.""" + @as_subprocess + def child_mnemonics_willmove(kind='xterm-256color'): + from blessed.sequences import measure_length + t = TestTerminal(kind=kind) + # movements + assert (len(t.move(98, 76)) == + measure_length(t.move(98, 76), t)) + assert (len(t.move(54)) == + measure_length(t.move(54), t)) + assert not t.cud1 or (len(t.cud1) == + measure_length(t.cud1, t)) + assert not t.cub1 or (len(t.cub1) == + measure_length(t.cub1, t)) + assert not t.cuf1 or (len(t.cuf1) == + measure_length(t.cuf1, t)) + assert not t.cuu1 or (len(t.cuu1) == + measure_length(t.cuu1, t)) + assert not t.cub or (len(t.cub(333)) == + measure_length(t.cub(333), t)) + assert not t.cuf or (len(t.cuf(333)) == + measure_length(t.cuf(333), t)) + assert not t.home or (len(t.home) == + measure_length(t.home, t)) + assert not t.restore or (len(t.restore) == + measure_length(t.restore, t)) + assert not t.clear or (len(t.clear) == + measure_length(t.clear, t)) + + child_mnemonics_willmove(all_standard_terms) diff --git a/tests/test_sequences.py b/tests/test_sequences.py new file mode 100644 index 00000000..bebc9c07 --- /dev/null +++ b/tests/test_sequences.py @@ -0,0 +1,318 @@ +# -*- coding: utf-8 -*- +"""Tests for Terminal() sequences and sequence-awareness.""" +import StringIO +import termios +import struct +import fcntl +import sys + +from accessories import ( + unsupported_sequence_terminals, + as_subprocess, + TestTerminal, + unicode_parm, + many_columns, + unicode_cap, + many_lines, + all_terms, +) + + +def test_capability(): + """Check that capability lookup works.""" + @as_subprocess + def child(): + # Also test that Terminal grabs a reasonable default stream. This test + # assumes it will be run from a tty. + t = TestTerminal() + sc = unicode_cap('sc') + assert t.save == sc + assert t.save == sc # Make sure caching doesn't screw it up. + + child() + + +def test_capability_without_tty(): + """Assert capability templates are '' when stream is not a tty.""" + @as_subprocess + def child(): + t = TestTerminal(stream=StringIO.StringIO()) + assert t.save == u'' + assert t.red == u'' + + child() + + +def test_capability_with_forced_tty(): + """force styling should return sequences even for non-ttys.""" + @as_subprocess + def child(): + t = TestTerminal(stream=StringIO.StringIO(), force_styling=True) + assert t.save == unicode_cap('sc') + + child() + + +def test_parametrization(): + """Test parametrizing a capability.""" + @as_subprocess + def child(): + assert TestTerminal().cup(3, 4) == unicode_parm('cup', 3, 4) + + child() + + +def test_height_and_width(): + """Assert that ``height_and_width()`` returns full integers.""" + @as_subprocess + def child(): + t = TestTerminal() # kind shouldn't matter. + assert isinstance(t.height, int) + assert isinstance(t.width, int) + + child() + + +def test_stream_attr(): + """Make sure Terminal ``stream`` is stdout by default.""" + @as_subprocess + def child(): + assert TestTerminal().stream == sys.__stdout__ + + child() + + +def test_emit_warnings_about_binpacked(unsupported_sequence_terminals): + """Test known binary-packed terminals (kermit, avatar) emit a warning.""" + @as_subprocess + def child(kind): + import warnings + from blessed.sequences import _BINTERM_UNSUPPORTED_MSG + warnings.filterwarnings("error", category=RuntimeWarning) + warnings.filterwarnings("error", category=UserWarning) + + try: + TestTerminal(kind=kind, force_styling=True) + except UserWarning, err: + assert (err.args[0] == _BINTERM_UNSUPPORTED_MSG or + err.args[0].startswith('Unknown parameter in ') + ), err + else: + assert 'warnings should have been emitted.' + finally: + del warnings + + child(unsupported_sequence_terminals) + + +def test_merge_sequences(): + """Test sequences are filtered and ordered longest-first.""" + from blessed.sequences import _merge_sequences + input_list = [u'a', u'aa', u'aaa', u''] + output_expected = [u'aaa', u'aa', u'a'] + assert (_merge_sequences(input_list) == output_expected) + + +def test_location(): + """Make sure ``location()`` does what it claims.""" + @as_subprocess + def child_with_styling(): + t = TestTerminal(stream=StringIO.StringIO(), force_styling=True) + with t.location(3, 4): + t.stream.write(u'hi') + expected_output = u''.join((unicode_cap('sc'), + unicode_parm('cup', 4, 3), + u'hi', unicode_cap('rc'))) + assert (t.stream.getvalue() == expected_output) + + child_with_styling() + + @as_subprocess + def child_without_styling(): + """No side effect for location as a context manager without styling.""" + t = TestTerminal(stream=StringIO.StringIO(), force_styling=None) + + with t.location(3, 4): + t.stream.write(u'hi') + + assert t.stream.getvalue() == u'hi' + + child_with_styling() + child_without_styling() + + +def test_horizontal_location(): + """Make sure we can move the cursor horizontally without changing rows.""" + @as_subprocess + def child(): + t = TestTerminal(stream=StringIO.StringIO(), force_styling=True) + with t.location(x=5): + pass + expected_output = u''.join((unicode_cap('sc'), + unicode_parm('hpa', 5), + unicode_cap('rc'))) + assert (t.stream.getvalue() == expected_output) + + child() + + +def test_zero_location(): + """Make sure ``location()`` pays attention to 0-valued args.""" + @as_subprocess + def child(): + t = TestTerminal(stream=StringIO.StringIO(), force_styling=True) + with t.location(0, 0): + pass + expected_output = u''.join((unicode_cap('sc'), + unicode_parm('cup', 0, 0), + unicode_cap('rc'))) + assert (t.stream.getvalue() == expected_output) + + child() + + +def test_mnemonic_colors(all_terms): + """Make sure color shortcuts work.""" + @as_subprocess + def child(kind): + def color(t, num): + return t.number_of_colors and unicode_parm('setaf', num) or '' + + def on_color(t, num): + return t.number_of_colors and unicode_parm('setab', num) or '' + + # Avoid testing red, blue, yellow, and cyan, since they might someday + # change depending on terminal type. + t = TestTerminal(kind=kind) + assert (t.white == color(t, 7)) + assert (t.green == color(t, 2)) # Make sure it's different than white. + assert (t.on_black == on_color(t, 0)) + assert (t.on_green == on_color(t, 2)) + assert (t.bright_black == color(t, 8)) + assert (t.bright_green == color(t, 10)) + assert (t.on_bright_black == on_color(t, 8)) + assert (t.on_bright_green == on_color(t, 10)) + + child(all_terms) + + +def test_callable_numeric_colors(all_terms): + """``color(n)`` should return a formatting wrapper.""" + @as_subprocess + def child(kind): + t = TestTerminal() + assert (t.color(5)('smoo') == t.magenta + 'smoo' + t.normal) + assert (t.color(5)('smoo') == t.color(5) + 'smoo' + t.normal) + assert (t.on_color(2)('smoo') == t.on_green + 'smoo' + t.normal) + assert (t.on_color(2)('smoo') == t.on_color(2) + 'smoo' + t.normal) + child(all_terms) + + +def test_null_callable_numeric_colors(all_terms): + """``color(n)`` should be a no-op on null terminals.""" + @as_subprocess + def child(kind): + t = TestTerminal(stream=StringIO.StringIO()) + assert (t.color(5)('smoo') == 'smoo') + assert (t.on_color(6)('smoo') == 'smoo') + + child(all_terms) + + +def test_naked_color_cap(all_terms): + """``term.color`` should return a stringlike capability.""" + @as_subprocess + def child(kind): + t = TestTerminal() + assert (t.color + '' == t.setaf + '') + + child(all_terms) + + +def test_formatting_functions(all_terms): + """Test simple and compound formatting wrappers.""" + @as_subprocess + def child(kind): + t = TestTerminal() + # test simple sugar, + expected_output = t.bold + u'hi' + t.normal + assert (t.bold(u'hi') == expected_output) + # Plain strs for Python 2.x + expected_output = t.green + 'hi' + t.normal + assert (t.green('hi') == expected_output) + # Test some non-ASCII chars, probably not necessary: + expected_output = u''.join((t.bold, t.green, u'boö', t.normal)) + assert (t.bold_green(u'boö') == expected_output) + expected_output = u''.join((t.bold, t.underline, t.green, t.on_red, + u'boo', t.normal)) + assert (t.bold_underline_green_on_red('boo') == expected_output) + # Very compounded strings + expected_output = u''.join((t.on_bright_red, t.bold, t.bright_green, + t.underline, u'meh', t.normal)) + assert (t.on_bright_red_bold_bright_green_underline('meh') + == expected_output) + + child(all_terms) + + +def test_formatting_functions_without_tty(all_terms): + """Test crazy-ass formatting wrappers when there's no tty.""" + @as_subprocess + def child(kind): + t = TestTerminal(kind=kind, stream=StringIO.StringIO()) + assert (t.bold(u'hi') == u'hi') + assert (t.green('hi') == u'hi') + # Test non-ASCII chars, no longer really necessary: + assert (t.bold_green(u'boö') == u'boö') + assert (t.bold_underline_green_on_red('loo') == u'loo') + assert (t.on_bright_red_bold_bright_green_underline('meh') == u'meh') + + child(all_terms) + + +def test_nice_formatting_errors(all_terms): + """Make sure you get nice hints if you misspell a formatting wrapper.""" + @as_subprocess + def child(kind): + t = TestTerminal() + try: + t.bold_misspelled('hey') + assert not t.is_a_tty or False, 'Should have thrown exception' + except TypeError, e: + assert 'probably misspelled' in e.args[0] + try: + t.bold_misspelled(u'hey') # unicode + assert not t.is_a_tty or False, 'Should have thrown exception' + except TypeError, e: + assert 'probably misspelled' in e.args[0] + + try: + t.bold_misspelled(None) # an arbitrary non-string + assert not t.is_a_tty or False, 'Should have thrown exception' + except TypeError, e: + assert 'probably misspelled' not in e.args[0] + + try: + t.bold_misspelled('a', 'b') # >1 string arg + assert not t.is_a_tty or False, 'Should have thrown exception' + except TypeError, e: + assert 'probably misspelled' not in e.args[0] + + child(all_terms) + + +def test_null_callable_string(all_terms): + """Make sure NullCallableString tolerates all kinds of args.""" + @as_subprocess + def child(kind='xterm-256color'): + t = TestTerminal(stream=StringIO.StringIO()) + assert (t.clear == '') + assert (t.move(1 == 2) == '') + assert (t.move_x(1) == '') + assert (t.bold() == '') + assert (t.bold('', 'x', 'huh?') == '') + assert (t.bold('', 9876) == '') + assert (t.uhh(9876) == '') + assert (t.clear('x') == 'x') + + child(all_terms) diff --git a/tests/test_wrap.py b/tests/test_wrap.py new file mode 100644 index 00000000..80b3f12e --- /dev/null +++ b/tests/test_wrap.py @@ -0,0 +1,62 @@ +import textwrap +import termios +import struct +import fcntl +import sys + +from accessories import ( + as_subprocess, + TestTerminal, + many_columns, + many_lines, + all_terms, +) + + +def test_SequenceWrapper(all_terms, many_lines, many_columns): + """Test that text wrapping accounts for sequences correctly.""" + @as_subprocess + def child(kind, lines=25, cols=80): + # set the pty's virtual window size + val = struct.pack('HHHH', lines, cols, 0, 0) + fcntl.ioctl(sys.__stdout__.fileno(), termios.TIOCSWINSZ, val) + + # build a test paragraph, along with a very colorful version + t = TestTerminal(kind=kind) + pgraph = 'pony express, all aboard, choo, choo! ' + ( + ('whugga ' * 10) + ('choo, choooOOOOOOOooooOOooOooOoo! ')) * 10 + pgraph_colored = u''.join([ + t.color(n % 7) + t.bold + ch + for n, ch in enumerate(pgraph)]) + + internal_wrapped = textwrap.wrap(pgraph, t.width, + break_long_words=False) + my_wrapped = t.wrap(pgraph) + my_wrapped_colored = t.wrap(pgraph_colored) + + # ensure we textwrap ascii the same as python + assert (internal_wrapped == my_wrapped) + + # ensure our first and last line wraps at its ends + first_l = internal_wrapped[0] + last_l = internal_wrapped[-1] + my_first_l = my_wrapped_colored[0] + my_last_l = my_wrapped_colored[-1] + assert (len(first_l) == t.length(my_first_l)) + assert (len(last_l) == t.length(my_last_l)) + assert (len(internal_wrapped[-1]) == t.length(my_wrapped_colored[-1])) + + # ensure our colored textwrap is the same line length + assert (len(internal_wrapped) == len(my_wrapped_colored)) + # test subsequent_indent= + internal_wrapped = textwrap.wrap(pgraph, t.width, + break_long_words=False, + subsequent_indent=' '*4) + my_wrapped = t.wrap(pgraph, subsequent_indent=' '*4) + my_wrapped_colored = t.wrap(pgraph_colored, subsequent_indent=' '*4) + + assert (internal_wrapped == my_wrapped) + assert (len(internal_wrapped) == len(my_wrapped_colored)) + assert (len(internal_wrapped[-1]) == t.length(my_wrapped_colored[-1])) + + child(all_terms, many_lines, many_columns) From fe86d1ddf2b04d31d4c2201bfee955c3d8f49e81 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 9 Mar 2014 05:16:15 -0700 Subject: [PATCH 002/459] cleanup changelog --- README.rst | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index d88f45ce..bc9e9f57 100644 --- a/README.rst +++ b/README.rst @@ -475,13 +475,15 @@ Includes the following changes, (jquast): * Created ``python setup.py develop`` for developer environment. * Converted nosetests to pytest, use ``python setup.py test``. * introduced ``@as_subprocess`` to discover and resolve various issues. - * cannot call ``setupterm()`` more than once per process. - * ``number_of_colors`` fails when ``does_styling`` is ``False``. - * pokemon ``curses.error`` exception removed. - * warning emitted and ``does_styling`` set ``False`` when TERM is unset - or unknown. - * allow ``term.color(7)('string')`` to behave when ``does_styling`` is - ``False``. + * cannot call ``setupterm()`` more than once per process -- issue a + warning about what terminal kind subsequent calls will use. + * resolved issue ``number_of_colors`` fails when ``does_styling`` is + ``False``. resolves piping tests output to stdout. + * removed pokemon ``curses.error`` exceptions. + * warn and set ``does_styling`` set ``False`` when TERM is unset or unknown. + * allow unsupported terminal capabilities to be callable just as supported + capabilities, so that the return value of ``term.color(n)`` may be called + on terminals without color capabilities. * attributes that should be read-only have now raise exception when re-assigned (properties). * introduced ``term.center()``, ``term.rjust()``, and ``term.ljust()``, @@ -498,7 +500,6 @@ Includes the following changes, (jquast): a multibyte sequence is received, allowing arrow keys and such to be detected. Optional value ``timeout`` allows timed polling or blocking. - 1.6 * Add ``does_styling`` property. This takes ``force_styling`` into account and should replace most uses of ``is_a_tty``. From e9f28771b6ec43233223d88c2de3d45c11160038 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 9 Mar 2014 05:19:33 -0700 Subject: [PATCH 003/459] fix rst, clarify authorship and version split --- README.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index bc9e9f57..fcd5a20b 100644 --- a/README.rst +++ b/README.rst @@ -470,8 +470,9 @@ shares the same. See the LICENSE file. Version History =============== -1.7, Forked 'erikrose/blessings' to 'jquast/blessed'. -Includes the following changes, (jquast): +1.7, + * Forked github project 'erikrose/blessings' to 'jquast/blessed', this + project was previously known as 'blessings' version 1.6 and prior. * Created ``python setup.py develop`` for developer environment. * Converted nosetests to pytest, use ``python setup.py test``. * introduced ``@as_subprocess`` to discover and resolve various issues. From 855a5f2a8206b5f4f05dcd51175011f39d4df52f Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 9 Mar 2014 05:19:58 -0700 Subject: [PATCH 004/459] remove nit ',' --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index fcd5a20b..47b972be 100644 --- a/README.rst +++ b/README.rst @@ -470,7 +470,7 @@ shares the same. See the LICENSE file. Version History =============== -1.7, +1.7 * Forked github project 'erikrose/blessings' to 'jquast/blessed', this project was previously known as 'blessings' version 1.6 and prior. * Created ``python setup.py develop`` for developer environment. From 9a02988231226279065735026b164d55d7e0f51f Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 9 Mar 2014 05:22:35 -0700 Subject: [PATCH 005/459] re-indent introduction code --- README.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index 47b972be..cada54c2 100644 --- a/README.rst +++ b/README.rst @@ -4,18 +4,18 @@ Blessed Coding with Blessed looks like this... :: -from blessed import Terminal + from blessed import Terminal -t = Terminal() + t = Terminal() -print t.bold('Hi there!') -print t.bold_red_on_bright_green('It hurts my eyes!') + print t.bold('Hi there!') + print t.bold_red_on_bright_green('It hurts my eyes!') -with t.location(0, t.height - 1): - print t.center(t.blink('press any key to continue.')) + with t.location(0, t.height - 1): + print t.center(t.blink('press any key to continue.')) -with t.key_at_a_time(): - t.keypress() + with t.key_at_a_time(): + t.keypress() Or, for byte-level control, you can drop down and play with raw terminal capabilities:: From 65ed24f27b944f82d9119b6232a015b25a83084a Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 9 Mar 2014 05:23:01 -0700 Subject: [PATCH 006/459] use cbreak and inkey in readme as intended. key_at_a_time and keypress was too lengthy for me. unbuffered_input was paul weaver's key_at_a_time. why don't we just call it cbreak, it has a manual page. look it up. --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index cada54c2..878632e4 100644 --- a/README.rst +++ b/README.rst @@ -14,8 +14,8 @@ Coding with Blessed looks like this... :: with t.location(0, t.height - 1): print t.center(t.blink('press any key to continue.')) - with t.key_at_a_time(): - t.keypress() + with t.cbreak(): + t.inkey() Or, for byte-level control, you can drop down and play with raw terminal capabilities:: From cd6101db80287ad8ff5e1a2d687b0e117d02c27a Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 9 Mar 2014 05:28:56 -0700 Subject: [PATCH 007/459] use pytest for travis-ci --- .travis.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index c2fdd242..4a06dc44 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,5 @@ matrix: - python: pypy # PyPy doesn't have _curses. script: - - pip install -q --use-mirrors nose - - python setup.py install - - nosetests -w /tmp blessings.tests + - python setup.py develop + - python setup.py test From a81c0d78b122b00c7830f2d392f6f756d9da3d2d Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 9 Mar 2014 05:40:09 -0700 Subject: [PATCH 008/459] deal with travis-ci requires and lack of TERM support --- .travis.yml | 3 +++ tests/test_sequences.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/.travis.yml b/.travis.yml index 4a06dc44..61d847ba 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,3 +15,6 @@ matrix: script: - python setup.py develop - python setup.py test + +install: + if [[ $TRAVIS_PYTHON_VERSION == 2.6 ]]; then pip install --use-mirrors ordereddict; fi diff --git a/tests/test_sequences.py b/tests/test_sequences.py index bebc9c07..deda7a3c 100644 --- a/tests/test_sequences.py +++ b/tests/test_sequences.py @@ -82,6 +82,8 @@ def child(): child() +@pytest.mark.skipif(os.environ.get('TRAVIS', None) is not None, + reason="travis-ci does not have binary-packed terminals.") def test_emit_warnings_about_binpacked(unsupported_sequence_terminals): """Test known binary-packed terminals (kermit, avatar) emit a warning.""" @as_subprocess From a9ab42d9b3daea63f456c81a42819d2e4a40d264 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 9 Mar 2014 05:43:04 -0700 Subject: [PATCH 009/459] can we get any newer pythons on travis yet? --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 61d847ba..11029522 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,9 @@ python: - 2.6 - 2.7 - 3.2 + - 3.3 + - 3.4 + - 3.5 - pypy matrix: From a87279620f6219f2529346872f98db361fea77b7 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 9 Mar 2014 05:43:56 -0700 Subject: [PATCH 010/459] import pytest and remove unused imports --- tests/test_sequences.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_sequences.py b/tests/test_sequences.py index deda7a3c..838fc64c 100644 --- a/tests/test_sequences.py +++ b/tests/test_sequences.py @@ -1,9 +1,6 @@ # -*- coding: utf-8 -*- """Tests for Terminal() sequences and sequence-awareness.""" import StringIO -import termios -import struct -import fcntl import sys from accessories import ( @@ -17,6 +14,8 @@ all_terms, ) +import pytest + def test_capability(): """Check that capability lookup works.""" From c92e565525eed5208d23947575ea973f830289bb Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 9 Mar 2014 05:44:19 -0700 Subject: [PATCH 011/459] add core tests --- tests/test_core.py | 171 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 tests/test_core.py diff --git a/tests/test_core.py b/tests/test_core.py new file mode 100644 index 00000000..9160858a --- /dev/null +++ b/tests/test_core.py @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- +"""Core blessed Terminal() tests.""" +import StringIO +import sys + +from accessories import ( + as_subprocess, + TestTerminal, + unicode_cap, + all_terms +) + + +def test_export_only_Terminal(): + """Ensure only Terminal instance is exported for import * statements.""" + import blessed + assert blessed.__all__ == ['Terminal'] + + +def test_null_location(all_terms): + """Make sure ``location()`` with no args just does position restoration.""" + @as_subprocess + def child(kind): + t = TestTerminal(stream=StringIO.StringIO(), force_styling=True) + with t.location(): + pass + expected_output = u''.join((unicode_cap('sc'), + unicode_cap('rc'))) + assert (t.stream.getvalue() == expected_output) + + child(all_terms) + + +def test_null_fileno(): + """Make sure ``Terminal`` works when ``fileno`` is ``None``.""" + @as_subprocess + def child(): + # This simulates piping output to another program. + out = StringIO.StringIO() + out.fileno = None + t = TestTerminal(stream=out) + assert (t.save == u'') + + child() + + +def test_number_of_colors_without_tty(): + """``number_of_colors`` should return 0 when there's no tty.""" + @as_subprocess + def child_256_nostyle(): + t = TestTerminal(stream=StringIO.StringIO()) + assert (t.number_of_colors == 0) + + @as_subprocess + def child_256_forcestyle(): + t = TestTerminal(stream=StringIO.StringIO(), force_styling=True) + assert (t.number_of_colors == 256) + + @as_subprocess + def child_8_forcestyle(): + t = TestTerminal(kind='ansi', stream=StringIO.StringIO(), + force_styling=True) + assert (t.number_of_colors == 8) + + @as_subprocess + def child_0_forcestyle(): + t = TestTerminal(kind='vt220', stream=StringIO.StringIO(), + force_styling=True) + assert (t.number_of_colors == 0) + + child_0_forcestyle() + child_8_forcestyle() + child_256_forcestyle() + child_256_nostyle() + + +def test_number_of_colors_with_tty(): + """``number_of_colors`` should work.""" + @as_subprocess + def child_256(): + t = TestTerminal() + assert (t.number_of_colors == 256) + + @as_subprocess + def child_8(): + t = TestTerminal(kind='ansi') + assert (t.number_of_colors == 8) + + @as_subprocess + def child_0(): + t = TestTerminal(kind='vt220') + assert (t.number_of_colors == 0) + + child_0() + child_8() + child_256() + + +def test_init_descriptor_always_initted(all_terms): + """Test height and width with non-tty Terminals.""" + @as_subprocess + def child(kind): + t = TestTerminal(kind=kind, stream=StringIO.StringIO()) + assert t._init_descriptor == sys.__stdout__.fileno() + assert (isinstance(t.height, int)) + assert (isinstance(t.width, int)) + + child(all_terms) + + +def test_force_styling_none(all_terms): + """If ``force_styling=None`` is used, don't ever do styling.""" + @as_subprocess + def child(kind): + t = TestTerminal(kind=kind, force_styling=None) + assert (t.save == '') + assert (t.color(9) == '') + assert (t.bold('oi') == 'oi') + + child(all_terms) + + +def test_setupterm_singleton_issue33(): + """A warning is emitted if a new terminal ``kind`` is used per process.""" + @as_subprocess + def child(): + import warnings + warnings.filterwarnings("error", category=UserWarning) + + # instantiate first terminal, of type xterm-256color + term = TestTerminal(force_styling=True) + + try: + # a second instantiation raises UserWarning + term = TestTerminal(kind="vt220", force_styling=True) + assert not term.is_a_tty or False, 'Should have thrown exception' + + except UserWarning, err: + assert (err.args[0].startswith( + 'A terminal of kind "vt220" has been requested') + ), err.args[0] + assert ('a terminal of kind "xterm-256color" will ' + 'continue to be returned' in err.args[0]), err.args[0] + finally: + del warnings + + child() + + +def test_setupterm_invalid_issue39(): + """A warning is emitted if TERM is invalid.""" + # https://bugzilla.mozilla.org/show_bug.cgi?id=878089 + + # if TERM is unset, defaults to 'unknown', which should + # fail to lookup and emit a warning, only. + @as_subprocess + def child(): + import warnings + warnings.filterwarnings("error", category=UserWarning) + + try: + term = TestTerminal(kind='unknown', force_styling=True) + assert not term.is_a_tty and not term.does_styling, ( + 'Should have thrown exception') + assert (term.number_of_colors == 0) + except UserWarning, err: + assert err.args[0] == 'Failed to setupterm(kind=unknown)' + finally: + del warnings + + child() From 9b4e60fd5c96ed226d93be925dcf38a77af5ba44 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 9 Mar 2014 05:46:26 -0700 Subject: [PATCH 012/459] newstyle exceptions --- tests/test_sequences.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_sequences.py b/tests/test_sequences.py index 838fc64c..6cd6750d 100644 --- a/tests/test_sequences.py +++ b/tests/test_sequences.py @@ -94,7 +94,7 @@ def child(kind): try: TestTerminal(kind=kind, force_styling=True) - except UserWarning, err: + except UserWarning(err): assert (err.args[0] == _BINTERM_UNSUPPORTED_MSG or err.args[0].startswith('Unknown parameter in ') ), err @@ -279,24 +279,24 @@ def child(kind): try: t.bold_misspelled('hey') assert not t.is_a_tty or False, 'Should have thrown exception' - except TypeError, e: + except TypeError(e): assert 'probably misspelled' in e.args[0] try: t.bold_misspelled(u'hey') # unicode assert not t.is_a_tty or False, 'Should have thrown exception' - except TypeError, e: + except TypeError(e): assert 'probably misspelled' in e.args[0] try: t.bold_misspelled(None) # an arbitrary non-string assert not t.is_a_tty or False, 'Should have thrown exception' - except TypeError, e: + except TypeError(e): assert 'probably misspelled' not in e.args[0] try: t.bold_misspelled('a', 'b') # >1 string arg assert not t.is_a_tty or False, 'Should have thrown exception' - except TypeError, e: + except TypeError(e): assert 'probably misspelled' not in e.args[0] child(all_terms) From 4b7d5b4b8a0a7a4cb8f398458fa29d2c9ab0a2e7 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 9 Mar 2014 05:46:55 -0700 Subject: [PATCH 013/459] ensure os module included as required --- tests/test_sequences.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_sequences.py b/tests/test_sequences.py index 6cd6750d..f85ce96a 100644 --- a/tests/test_sequences.py +++ b/tests/test_sequences.py @@ -2,6 +2,7 @@ """Tests for Terminal() sequences and sequence-awareness.""" import StringIO import sys +import os from accessories import ( unsupported_sequence_terminals, From 81e12cd0432baca51281f454ca252c5dd518f90c Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 9 Mar 2014 05:58:04 -0700 Subject: [PATCH 014/459] python 2.5 through 3.4-compatible exception handling --- tests/test_core.py | 6 ++++-- tests/test_sequences.py | 15 ++++++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index 9160858a..ba8edcf1 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -135,7 +135,8 @@ def child(): term = TestTerminal(kind="vt220", force_styling=True) assert not term.is_a_tty or False, 'Should have thrown exception' - except UserWarning, err: + except UserWarning: + eerr = sys.exc_info()[1] assert (err.args[0].startswith( 'A terminal of kind "vt220" has been requested') ), err.args[0] @@ -163,7 +164,8 @@ def child(): assert not term.is_a_tty and not term.does_styling, ( 'Should have thrown exception') assert (term.number_of_colors == 0) - except UserWarning, err: + except UserWarning: + err = sys.exc_info()[1] assert err.args[0] == 'Failed to setupterm(kind=unknown)' finally: del warnings diff --git a/tests/test_sequences.py b/tests/test_sequences.py index f85ce96a..f9cb5a10 100644 --- a/tests/test_sequences.py +++ b/tests/test_sequences.py @@ -95,7 +95,8 @@ def child(kind): try: TestTerminal(kind=kind, force_styling=True) - except UserWarning(err): + except UserWarning: + err = sys.exc_info()[1] assert (err.args[0] == _BINTERM_UNSUPPORTED_MSG or err.args[0].startswith('Unknown parameter in ') ), err @@ -280,24 +281,28 @@ def child(kind): try: t.bold_misspelled('hey') assert not t.is_a_tty or False, 'Should have thrown exception' - except TypeError(e): + except TypeError: + e = sys.exc_info()[1] assert 'probably misspelled' in e.args[0] try: t.bold_misspelled(u'hey') # unicode assert not t.is_a_tty or False, 'Should have thrown exception' - except TypeError(e): + except TypeError: + e = sys.exc_info()[1] assert 'probably misspelled' in e.args[0] try: t.bold_misspelled(None) # an arbitrary non-string assert not t.is_a_tty or False, 'Should have thrown exception' - except TypeError(e): + except TypeError: + e = sys.exc_info()[1] assert 'probably misspelled' not in e.args[0] try: t.bold_misspelled('a', 'b') # >1 string arg assert not t.is_a_tty or False, 'Should have thrown exception' - except TypeError(e): + except TypeError: + e = sys.exc_info()[1] assert 'probably misspelled' not in e.args[0] child(all_terms) From 87a573d6e42524fec76907c25c7fecc7d8555ec5 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 9 Mar 2014 06:00:07 -0700 Subject: [PATCH 015/459] travis.yml tweaks --- .travis.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 11029522..08eb65d1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,13 @@ language: python python: - - 2.5 + # - 2.5 - 2.6 - 2.7 - 3.2 - 3.3 - - 3.4 - - 3.5 + # - 3.4 + # - 3.5 - pypy matrix: @@ -20,4 +20,5 @@ script: - python setup.py test install: + if [[ $TRAVIS_PYTHON_VERSION == 2.5 ]]; then pip install --use-mirrors ordereddict; fi if [[ $TRAVIS_PYTHON_VERSION == 2.6 ]]; then pip install --use-mirrors ordereddict; fi From 8aae1be9d4c8a14cef8b5690fd782b976b3ba34a Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 9 Mar 2014 06:01:36 -0700 Subject: [PATCH 016/459] compress if statement, newlines become one line in build.sh --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 08eb65d1..406eedf8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,5 +20,4 @@ script: - python setup.py test install: - if [[ $TRAVIS_PYTHON_VERSION == 2.5 ]]; then pip install --use-mirrors ordereddict; fi - if [[ $TRAVIS_PYTHON_VERSION == 2.6 ]]; then pip install --use-mirrors ordereddict; fi + if [[ $TRAVIS_PYTHON_VERSION == 2.5 ]] || [[ $TRAVIS_PYTHON_VERSION == 2.6 ]]; then pip install --use-mirrors ordereddict; fi From 3865a1693492cf6ee60a486c91a5d7c1685e471f Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 9 Mar 2014 06:03:15 -0700 Subject: [PATCH 017/459] fix silly broke eerr, should be err --- tests/test_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_core.py b/tests/test_core.py index ba8edcf1..b47bc31c 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -136,7 +136,7 @@ def child(): assert not term.is_a_tty or False, 'Should have thrown exception' except UserWarning: - eerr = sys.exc_info()[1] + err = sys.exc_info()[1] assert (err.args[0].startswith( 'A terminal of kind "vt220" has been requested') ), err.args[0] From 843e8f2989d86870892c483417e680ed494de92a Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 9 Mar 2014 06:04:30 -0700 Subject: [PATCH 018/459] pleasure travis --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 406eedf8..c3289f63 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,13 @@ language: python python: - # - 2.5 + #- 2.5 - 2.6 - 2.7 - 3.2 - 3.3 - # - 3.4 - # - 3.5 + #- 3.4 + #- 3.5 - pypy matrix: From aab36d6652377cc7e683ddbfd776893d0727f760 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 9 Mar 2014 06:07:53 -0700 Subject: [PATCH 019/459] python 3.3 StringIO backwards-compatability workaround --- tests/test_core.py | 19 +++++++++++-------- tests/test_sequences.py | 23 +++++++++++++---------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index b47bc31c..5abc00e0 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,6 +1,9 @@ # -*- coding: utf-8 -*- """Core blessed Terminal() tests.""" -import StringIO +try: + from StringIO import StringIO +except ImportError: + from io import StringIO import sys from accessories import ( @@ -21,7 +24,7 @@ def test_null_location(all_terms): """Make sure ``location()`` with no args just does position restoration.""" @as_subprocess def child(kind): - t = TestTerminal(stream=StringIO.StringIO(), force_styling=True) + t = TestTerminal(stream=StringIO(), force_styling=True) with t.location(): pass expected_output = u''.join((unicode_cap('sc'), @@ -36,7 +39,7 @@ def test_null_fileno(): @as_subprocess def child(): # This simulates piping output to another program. - out = StringIO.StringIO() + out = StringIO() out.fileno = None t = TestTerminal(stream=out) assert (t.save == u'') @@ -48,23 +51,23 @@ def test_number_of_colors_without_tty(): """``number_of_colors`` should return 0 when there's no tty.""" @as_subprocess def child_256_nostyle(): - t = TestTerminal(stream=StringIO.StringIO()) + t = TestTerminal(stream=StringIO()) assert (t.number_of_colors == 0) @as_subprocess def child_256_forcestyle(): - t = TestTerminal(stream=StringIO.StringIO(), force_styling=True) + t = TestTerminal(stream=StringIO(), force_styling=True) assert (t.number_of_colors == 256) @as_subprocess def child_8_forcestyle(): - t = TestTerminal(kind='ansi', stream=StringIO.StringIO(), + t = TestTerminal(kind='ansi', stream=StringIO(), force_styling=True) assert (t.number_of_colors == 8) @as_subprocess def child_0_forcestyle(): - t = TestTerminal(kind='vt220', stream=StringIO.StringIO(), + t = TestTerminal(kind='vt220', stream=StringIO(), force_styling=True) assert (t.number_of_colors == 0) @@ -100,7 +103,7 @@ def test_init_descriptor_always_initted(all_terms): """Test height and width with non-tty Terminals.""" @as_subprocess def child(kind): - t = TestTerminal(kind=kind, stream=StringIO.StringIO()) + t = TestTerminal(kind=kind, stream=StringIO()) assert t._init_descriptor == sys.__stdout__.fileno() assert (isinstance(t.height, int)) assert (isinstance(t.width, int)) diff --git a/tests/test_sequences.py b/tests/test_sequences.py index f9cb5a10..b345b9cc 100644 --- a/tests/test_sequences.py +++ b/tests/test_sequences.py @@ -1,6 +1,9 @@ # -*- coding: utf-8 -*- """Tests for Terminal() sequences and sequence-awareness.""" -import StringIO +try: + from StringIO import StringIO +except ImportError: + from io import StringIO import sys import os @@ -36,7 +39,7 @@ def test_capability_without_tty(): """Assert capability templates are '' when stream is not a tty.""" @as_subprocess def child(): - t = TestTerminal(stream=StringIO.StringIO()) + t = TestTerminal(stream=StringIO()) assert t.save == u'' assert t.red == u'' @@ -47,7 +50,7 @@ def test_capability_with_forced_tty(): """force styling should return sequences even for non-ttys.""" @as_subprocess def child(): - t = TestTerminal(stream=StringIO.StringIO(), force_styling=True) + t = TestTerminal(stream=StringIO(), force_styling=True) assert t.save == unicode_cap('sc') child() @@ -120,7 +123,7 @@ def test_location(): """Make sure ``location()`` does what it claims.""" @as_subprocess def child_with_styling(): - t = TestTerminal(stream=StringIO.StringIO(), force_styling=True) + t = TestTerminal(stream=StringIO(), force_styling=True) with t.location(3, 4): t.stream.write(u'hi') expected_output = u''.join((unicode_cap('sc'), @@ -133,7 +136,7 @@ def child_with_styling(): @as_subprocess def child_without_styling(): """No side effect for location as a context manager without styling.""" - t = TestTerminal(stream=StringIO.StringIO(), force_styling=None) + t = TestTerminal(stream=StringIO(), force_styling=None) with t.location(3, 4): t.stream.write(u'hi') @@ -148,7 +151,7 @@ def test_horizontal_location(): """Make sure we can move the cursor horizontally without changing rows.""" @as_subprocess def child(): - t = TestTerminal(stream=StringIO.StringIO(), force_styling=True) + t = TestTerminal(stream=StringIO(), force_styling=True) with t.location(x=5): pass expected_output = u''.join((unicode_cap('sc'), @@ -163,7 +166,7 @@ def test_zero_location(): """Make sure ``location()`` pays attention to 0-valued args.""" @as_subprocess def child(): - t = TestTerminal(stream=StringIO.StringIO(), force_styling=True) + t = TestTerminal(stream=StringIO(), force_styling=True) with t.location(0, 0): pass expected_output = u''.join((unicode_cap('sc'), @@ -215,7 +218,7 @@ def test_null_callable_numeric_colors(all_terms): """``color(n)`` should be a no-op on null terminals.""" @as_subprocess def child(kind): - t = TestTerminal(stream=StringIO.StringIO()) + t = TestTerminal(stream=StringIO()) assert (t.color(5)('smoo') == 'smoo') assert (t.on_color(6)('smoo') == 'smoo') @@ -262,7 +265,7 @@ def test_formatting_functions_without_tty(all_terms): """Test crazy-ass formatting wrappers when there's no tty.""" @as_subprocess def child(kind): - t = TestTerminal(kind=kind, stream=StringIO.StringIO()) + t = TestTerminal(kind=kind, stream=StringIO()) assert (t.bold(u'hi') == u'hi') assert (t.green('hi') == u'hi') # Test non-ASCII chars, no longer really necessary: @@ -312,7 +315,7 @@ def test_null_callable_string(all_terms): """Make sure NullCallableString tolerates all kinds of args.""" @as_subprocess def child(kind='xterm-256color'): - t = TestTerminal(stream=StringIO.StringIO()) + t = TestTerminal(stream=StringIO()) assert (t.clear == '') assert (t.move(1 == 2) == '') assert (t.move_x(1) == '') From 7dca9ab5f78bc39d5a654593d5a8ff453e4c4b53 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 9 Mar 2014 06:20:46 -0700 Subject: [PATCH 020/459] workaround for py3.3 missing unicode() object --- .travis.yml | 4 ++-- tests/accessories.py | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index c3289f63..bfa643ca 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,8 +12,8 @@ python: matrix: allow_failures: - - python: 3.2 # Not a new enough 3.2 for blessings - - python: pypy # PyPy doesn't have _curses. + # bugs, u'literals' + - python: 3.2 script: - python setup.py develop diff --git a/tests/accessories.py b/tests/accessories.py index b23e732a..c362aac4 100644 --- a/tests/accessories.py +++ b/tests/accessories.py @@ -30,6 +30,10 @@ all_standard_terms_params = (set(all_terms_params) - set(binpacked_terminal_params)) +# workaround for missing unicode object (str is unicode) +if sys.version_info >= (3,): + unicode = str + class as_subprocess(object): """This helper executes test cases in a child process, From 4ca8c2ec3440ff2bbaff618a61cd9b3a132efc6a Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 9 Mar 2014 12:20:40 -0700 Subject: [PATCH 021/459] ensure unicode.startswith(arg) is also unicode --- .travis.yml | 3 +-- tests/accessories.py | 6 +++--- tests/test_keyboard.py | 6 +++--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index bfa643ca..7035860c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,8 +16,7 @@ matrix: - python: 3.2 script: - - python setup.py develop - - python setup.py test + - python setup.py develop test install: if [[ $TRAVIS_PYTHON_VERSION == 2.5 ]] || [[ $TRAVIS_PYTHON_VERSION == 2.6 ]]; then pip install --use-mirrors ordereddict; fi diff --git a/tests/accessories.py b/tests/accessories.py index c362aac4..cac05b0e 100644 --- a/tests/accessories.py +++ b/tests/accessories.py @@ -114,8 +114,8 @@ def read_until_semaphore(fd, semaphore=RECV_SEMAPHORE, # continuous integration servers such as Travis. outp = unicode() decoder = codecs.getincrementaldecoder(encoding)() - - while not outp.startswith(semaphore): + semaphore = semaphore.decode('ascii') + while not outp.startswith(): try: _exc = os.read(fd, 1) except OSError: # linux EOF @@ -125,7 +125,7 @@ def read_until_semaphore(fd, semaphore=RECV_SEMAPHORE, outp += decoder.decode(_exc, final=False) assert outp.startswith(semaphore), ( 'Semaphore not recv before EOF ' - '(expected %r, got %r)' % (semaphore, outp,)) + '(expected: %r, got: %r)' % (semaphore, outp,)) return outp[len(semaphore):] diff --git a/tests/test_keyboard.py b/tests/test_keyboard.py index 7d97a161..fa20a55e 100644 --- a/tests/test_keyboard.py +++ b/tests/test_keyboard.py @@ -75,7 +75,7 @@ def test_inkey_0s_input(): def test_inkey_0s_multibyte_utf8(): - """0-second inkey with multibte utf-8 input; should decode immediately.""" + """0-second inkey with multibyte utf-8 input; should decode immediately.""" # utf-8 bytes represent "latin capital letter upsilon". pid, master_fd = pty.fork() if pid is 0: # child @@ -100,7 +100,7 @@ def test_inkey_0s_multibyte_utf8(): def test_inkey_0s_sequence(): - """0-second inkey with multibte sequence; should decode immediately.""" + """0-second inkey with multibyte sequence; should decode immediately.""" pid, master_fd = pty.fork() if pid is 0: # child term = TestTerminal() @@ -123,7 +123,7 @@ def test_inkey_0s_sequence(): def test_inkey_1s_input(): - """1-second inkey w/multibte sequence; should return after ~1 second.""" + """1-second inkey w/multibyte sequence; should return after ~1 second.""" pid, master_fd = pty.fork() if pid is 0: # child term = TestTerminal() From c64b3fe6d337c8985ab3c8065757936d06e55b96 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 9 Mar 2014 17:15:08 -0700 Subject: [PATCH 022/459] pytest, tox, and coverage cleanup for python26->33 working around the issue of using 2to3 by testing the installed package within tox, somewhat similar to what was originally done with nose. --- .travis.yml | 27 ++--- blessed/sequences.py | 8 +- {tests => blessed/tests}/accessories.py | 33 +++--- {tests => blessed/tests}/test_core.py | 4 +- {tests => blessed/tests}/test_keyboard.py | 37 ++++--- .../tests/test_length_sequence.py | 14 ++- {tests => blessed/tests}/test_sequences.py | 30 +++--- {tests => blessed/tests}/test_wrap.py | 11 +- setup.py | 101 ++++++++++-------- tox.ini | 21 +++- 10 files changed, 164 insertions(+), 122 deletions(-) rename {tests => blessed/tests}/accessories.py (89%) rename {tests => blessed/tests}/test_core.py (97%) rename {tests => blessed/tests}/test_keyboard.py (91%) rename tests/test_sequence_length.py => blessed/tests/test_length_sequence.py (95%) rename {tests => blessed/tests}/test_sequences.py (92%) rename {tests => blessed/tests}/test_wrap.py (85%) diff --git a/.travis.yml b/.travis.yml index 7035860c..202c4063 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,22 +1,23 @@ language: python -python: - #- 2.5 - - 2.6 - - 2.7 - - 3.2 - - 3.3 - #- 3.4 - #- 3.5 - - pypy +env: + - TOXENV=py26 + - TOXENV=py27 + - TOXENV=py33 + # not supported .. + - TOXNENV=py25 # missing + - TOXNENV=py32 # will fail + - TOXNENV=py34 # missing + - TOXNENV=pypy # will fail matrix: allow_failures: - # bugs, u'literals' - - python: 3.2 + - python: 3.2 # bugs, no u'literals' + - pypy # a few (minor) bugs ?? script: - - python setup.py develop test + - tox install: - if [[ $TRAVIS_PYTHON_VERSION == 2.5 ]] || [[ $TRAVIS_PYTHON_VERSION == 2.6 ]]; then pip install --use-mirrors ordereddict; fi + # travis wants requirements.txt; we don't, use only 'requires=' in setup.py. + - if [[ $TRAVIS_PYTHON_VERSION == 2.5 ]] || [[ $TRAVIS_PYTHON_VERSION == 2.6 ]]; then pip install --use-mirrors ordereddict; fi diff --git a/blessed/sequences.py b/blessed/sequences.py index a095115d..c3f5d975 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -16,15 +16,17 @@ _BINTERM_UNSUPPORTED_MSG = ('sequence-awareness for terminals emitting ' 'binary-packed capabilities are not supported.') + def _merge_sequences(inp): """Merge a list of input sequence patterns for use in a regular expression. - Order by lengthyness (full sequence set precident over subset), + Order by lengthyness (full sequence set precedent over subset), and exclude any empty (u'') sequences. """ return sorted(list(filter(None, inp)), key=len, reverse=True) -def _build_numeric_capability(term, cap, optional=False, base_num=99, nparams=1): +def _build_numeric_capability(term, cap, optional=False, + base_num=99, nparams=1): """ Build regexp from capabilities having matching numeric parameter contained within termcap value: n->(\d+). """ @@ -59,7 +61,7 @@ def _build_any_numeric_capability(term, cap, num=99, nparams=1): def init_sequence_patterns(term): """ Given a Terminal instance, ``term``, this function processes and parses several known terminal capabilities, and builds a - database of regular expressions and attatches them to ``term`` + database of regular expressions and attaches them to ``term`` as attributes: ``_re_will_move``: any sequence matching this pattern will cause the terminal cursor to move (such as term.home). diff --git a/tests/accessories.py b/blessed/tests/accessories.py similarity index 89% rename from tests/accessories.py rename to blessed/tests/accessories.py index cac05b0e..e5eb2911 100644 --- a/tests/accessories.py +++ b/blessed/tests/accessories.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Accessories for automated tests.""" +"""Accessories for automated py.test runner.""" from __future__ import with_statement import contextlib import functools @@ -16,13 +16,13 @@ import pytest TestTerminal = functools.partial(Terminal, kind='xterm-256color') -SEND_SEMAPHORE = SEMAPHORE = u'SEMAPHORE\n'.encode('ascii') -RECV_SEMAPHORE = u'%s\r\n' % (SEMAPHORE.rstrip(),) +SEND_SEMAPHORE = SEMAPHORE = b'SEMAPHORE\n' +RECV_SEMAPHORE = b'SEMAPHORE\r\n' all_xterms_params = ['xterm', 'xterm-256color'] all_terms_params = ['screen', 'vt220', 'rxvt', 'cons25', 'linux', 'ansi'] binpacked_terminal_params = ['avatar', 'kermit'] many_lines_params = [30, 100] -many_columns_params = [5, 30, 150, 500] +many_columns_params = [10, 30, 100] if os.environ.get('TRAVIS', None) is None: # TRAVIS-CI has a limited type of terminals, the others ... all_terms_params.extend(['avatar', 'kermit', 'dtterm', 'wyse520', @@ -30,10 +30,6 @@ all_standard_terms_params = (set(all_terms_params) - set(binpacked_terminal_params)) -# workaround for missing unicode object (str is unicode) -if sys.version_info >= (3,): - unicode = str - class as_subprocess(object): """This helper executes test cases in a child process, @@ -102,25 +98,26 @@ def __call__(self, *args, **kwargs): def read_until_semaphore(fd, semaphore=RECV_SEMAPHORE, encoding='utf8', timeout=10): - """Read file descriptor ``fd`` until ``semaphore`` is found.""" + """Read file descriptor ``fd`` until ``semaphore`` is found. + + Used to ensure the child process is awake and ready. For timing + tests; without a semaphore, the time to fork() would be (incorrectly) + included in the duration of the test, which can be very length on + continuous integration servers (such as Travis-CI). + """ # note that when a child process writes xyz\\n, the parent - # process will ready xyz\\r\\n -- this is how pseudo terminals + # process will read xyz\\r\\n -- this is how pseudo terminals # behave; a virtual terminal requires both carriage return and # line feed, it is only for convenience that \\n does both. - # - # used to ensure the child process is awake and ready, for timing - # tests; without a semaphore, the time to fork() would be (incorrectly) - # included in the duration of the test, which can be very length on - # continuous integration servers such as Travis. outp = unicode() decoder = codecs.getincrementaldecoder(encoding)() semaphore = semaphore.decode('ascii') - while not outp.startswith(): + while not outp.startswith(semaphore): try: _exc = os.read(fd, 1) - except OSError: # linux EOF + except OSError: # Linux EOF break - if not _exc: # bsd EOF + if not _exc: # BSD EOF break outp += decoder.decode(_exc, final=False) assert outp.startswith(semaphore), ( diff --git a/tests/test_core.py b/blessed/tests/test_core.py similarity index 97% rename from tests/test_core.py rename to blessed/tests/test_core.py index 5abc00e0..dee7b523 100644 --- a/tests/test_core.py +++ b/blessed/tests/test_core.py @@ -27,8 +27,8 @@ def child(kind): t = TestTerminal(stream=StringIO(), force_styling=True) with t.location(): pass - expected_output = u''.join((unicode_cap('sc'), - unicode_cap('rc'))) + expected_output = u''.join( + (unicode_cap('sc'), unicode_cap('rc'))) assert (t.stream.getvalue() == expected_output) child(all_terms) diff --git a/tests/test_keyboard.py b/blessed/tests/test_keyboard.py similarity index 91% rename from tests/test_keyboard.py rename to blessed/tests/test_keyboard.py index fa20a55e..67540faf 100644 --- a/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -58,7 +58,7 @@ def test_inkey_0s_input(): os.write(sys.__stdout__.fileno(), SEMAPHORE) with term.cbreak(): inp = term.inkey(timeout=0) - os.write(sys.__stdout__.fileno(), inp) + os.write(sys.__stdout__.fileno(), inp.encode('utf-8')) os._exit(0) with echo_off(master_fd): @@ -107,7 +107,7 @@ def test_inkey_0s_sequence(): os.write(sys.__stdout__.fileno(), SEMAPHORE) with term.cbreak(): inp = term.inkey(timeout=0) - os.write(sys.__stdout__.fileno(), ('%s' % (inp.name,))) + os.write(sys.__stdout__.fileno(), inp.name.encode('ascii')) sys.stdout.flush() os._exit(0) @@ -130,7 +130,7 @@ def test_inkey_1s_input(): os.write(sys.__stdout__.fileno(), SEMAPHORE) with term.cbreak(): inp = term.inkey(timeout=3) - os.write(sys.__stdout__.fileno(), ('%s' % (inp.name,))) + os.write(sys.__stdout__.fileno(), inp.name.encode('utf-8')) sys.stdout.flush() os._exit(0) @@ -156,8 +156,9 @@ def test_esc_delay_035(): with term.cbreak(): stime = time.time() inp = term.inkey(timeout=5) - os.write(sys.__stdout__.fileno(), ('%s %i' % ( - inp.name, (time.time() - stime) * 100,))) + measured_time = (time.time() - stime) * 100 + os.write(sys.__stdout__.fileno(), ( + '%s %i' % (inp.name, measured_time,)).encode('ascii')) sys.stdout.flush() os._exit(0) @@ -183,8 +184,9 @@ def test_esc_delay_135(): with term.cbreak(): stime = time.time() inp = term.inkey(timeout=5, esc_delay=1.35) - os.write(sys.__stdout__.fileno(), ('%s %i' % ( - inp.name, (time.time() - stime) * 100,))) + measured_time = (time.time() - stime) * 100 + os.write(sys.__stdout__.fileno(), ( + '%s %i' % (inp.name, measured_time,)).encode('ascii')) sys.stdout.flush() os._exit(0) @@ -210,8 +212,9 @@ def test_esc_delay_timout_0(): with term.cbreak(): stime = time.time() inp = term.inkey(timeout=0) - os.write(sys.__stdout__.fileno(), ('%s %i' % ( - inp.name, (time.time() - stime) * 100,))) + measured_time = (time.time() - stime) * 100 + os.write(sys.__stdout__.fileno(), ( + '%s %i' % (inp.name, measured_time,)).encode('ascii')) sys.stdout.flush() os._exit(0) @@ -238,7 +241,8 @@ def test_no_keystroke(): assert ks.code == ks._code assert u'x' == u'x' + ks assert ks.is_sequence is False - assert repr(ks) == "u''" + assert repr(ks) in ("u''", # py26, 27 + "''",) # py33 def test_a_keystroke(): @@ -344,39 +348,40 @@ def test_resolve_sequence(): assert ks.name is None assert ks.code is None assert ks.is_sequence is False - assert repr(ks) == u"u''" + assert repr(ks) in ("u''", # py26, 27 + "''",) # py33 ks = resolve_sequence(u'notfound', mapper=mapper, codes=codes) assert ks == u'n' assert ks.name is None assert ks.code is None assert ks.is_sequence is False - assert repr(ks) == u"u'n'" + assert repr(ks) in (u"u'n'", "'n'",) ks = resolve_sequence(u'SEQ1', mapper, codes) assert ks == u'SEQ1' assert ks.name == u'KEY_SEQ1' assert ks.code is 1 assert ks.is_sequence is True - assert repr(ks) == u"KEY_SEQ1" + assert repr(ks) in (u"KEY_SEQ1", "KEY_SEQ1") ks = resolve_sequence(u'LONGSEQ_longer', mapper, codes) assert ks == u'LONGSEQ' assert ks.name == u'KEY_LONGSEQ' assert ks.code is 4 assert ks.is_sequence is True - assert repr(ks) == u"KEY_LONGSEQ" + assert repr(ks) in (u"KEY_LONGSEQ", "KEY_LONGSEQ") ks = resolve_sequence(u'LONGSEQ', mapper, codes) assert ks == u'LONGSEQ' assert ks.name == u'KEY_LONGSEQ' assert ks.code is 4 assert ks.is_sequence is True - assert repr(ks) == u"KEY_LONGSEQ" + assert repr(ks) in (u"KEY_LONGSEQ", "KEY_LONGSEQ") ks = resolve_sequence(u'Lxxxxx', mapper, codes) assert ks == u'L' assert ks.name == u'KEY_L' assert ks.code is 6 assert ks.is_sequence is True - assert repr(ks) == u"KEY_L" + assert repr(ks) in (u"KEY_L", "KEY_L") diff --git a/tests/test_sequence_length.py b/blessed/tests/test_length_sequence.py similarity index 95% rename from tests/test_sequence_length.py rename to blessed/tests/test_length_sequence.py index 57dc6e53..20c79677 100644 --- a/tests/test_sequence_length.py +++ b/blessed/tests/test_length_sequence.py @@ -8,7 +8,6 @@ all_standard_terms, as_subprocess, TestTerminal, - many_columns, many_lines, all_terms, ) @@ -90,19 +89,24 @@ def child(kind): child(all_terms) -def test_Sequence_alignment(all_terms, many_lines, many_columns): +def test_Sequence_alignment(all_terms, many_lines): """Tests methods related to Sequence class, namely ljust, rjust, center.""" @as_subprocess def child(kind, lines=25, cols=80): # set the pty's virtual window size val = struct.pack('HHHH', lines, cols, 0, 0) +# try: fcntl.ioctl(sys.__stdout__.fileno(), termios.TIOCSWINSZ, val) +# except IOError: +# # unable to set screen size of tty, skip +# return t = TestTerminal(kind=kind) pony_msg = 'pony express, all aboard, choo, choo!' pony_len = len(pony_msg) - pony_colored = u''.join(['%s%s' % (t.color(n % 7), ch,) - for n, ch in enumerate(pony_msg)]) + pony_colored = u''.join( + ['%s%s' % (t.color(n % 7), ch,) + for n, ch in enumerate(pony_msg)]) pony_colored += t.normal ladjusted = t.ljust(pony_colored) radjusted = t.rjust(pony_colored) @@ -115,7 +119,7 @@ def child(kind, lines=25, cols=80): assert (t.length(radjusted.strip()) == pony_len) assert (t.length(radjusted) == len(pony_msg.rjust(t.width))) - child(all_terms, many_lines, many_columns) + child(kind=all_terms, lines=many_lines) def test_sequence_is_movement_false(all_terms): diff --git a/tests/test_sequences.py b/blessed/tests/test_sequences.py similarity index 92% rename from tests/test_sequences.py rename to blessed/tests/test_sequences.py index b345b9cc..77bff4a3 100644 --- a/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -126,9 +126,10 @@ def child_with_styling(): t = TestTerminal(stream=StringIO(), force_styling=True) with t.location(3, 4): t.stream.write(u'hi') - expected_output = u''.join((unicode_cap('sc'), - unicode_parm('cup', 4, 3), - u'hi', unicode_cap('rc'))) + expected_output = u''.join( + (unicode_cap('sc'), + unicode_parm('cup', 4, 3), + u'hi', unicode_cap('rc'))) assert (t.stream.getvalue() == expected_output) child_with_styling() @@ -154,9 +155,10 @@ def child(): t = TestTerminal(stream=StringIO(), force_styling=True) with t.location(x=5): pass - expected_output = u''.join((unicode_cap('sc'), - unicode_parm('hpa', 5), - unicode_cap('rc'))) + expected_output = u''.join( + (unicode_cap('sc'), + unicode_parm('hpa', 5), + unicode_cap('rc'))) assert (t.stream.getvalue() == expected_output) child() @@ -169,9 +171,10 @@ def child(): t = TestTerminal(stream=StringIO(), force_styling=True) with t.location(0, 0): pass - expected_output = u''.join((unicode_cap('sc'), - unicode_parm('cup', 0, 0), - unicode_cap('rc'))) + expected_output = u''.join( + (unicode_cap('sc'), + unicode_parm('cup', 0, 0), + unicode_cap('rc'))) assert (t.stream.getvalue() == expected_output) child() @@ -249,12 +252,13 @@ def child(kind): # Test some non-ASCII chars, probably not necessary: expected_output = u''.join((t.bold, t.green, u'boö', t.normal)) assert (t.bold_green(u'boö') == expected_output) - expected_output = u''.join((t.bold, t.underline, t.green, t.on_red, - u'boo', t.normal)) + expected_output = u''.join( + (t.bold, t.underline, t.green, t.on_red, u'boo', t.normal)) assert (t.bold_underline_green_on_red('boo') == expected_output) # Very compounded strings - expected_output = u''.join((t.on_bright_red, t.bold, t.bright_green, - t.underline, u'meh', t.normal)) + expected_output = u''.join( + (t.on_bright_red, t.bold, t.bright_green, + t.underline, u'meh', t.normal)) assert (t.on_bright_red_bold_bright_green_underline('meh') == expected_output) diff --git a/tests/test_wrap.py b/blessed/tests/test_wrap.py similarity index 85% rename from tests/test_wrap.py rename to blessed/tests/test_wrap.py index 80b3f12e..a56f5e30 100644 --- a/tests/test_wrap.py +++ b/blessed/tests/test_wrap.py @@ -8,12 +8,11 @@ as_subprocess, TestTerminal, many_columns, - many_lines, all_terms, ) -def test_SequenceWrapper(all_terms, many_lines, many_columns): +def test_SequenceWrapper(all_terms, many_columns): """Test that text wrapping accounts for sequences correctly.""" @as_subprocess def child(kind, lines=25, cols=80): @@ -23,8 +22,10 @@ def child(kind, lines=25, cols=80): # build a test paragraph, along with a very colorful version t = TestTerminal(kind=kind) - pgraph = 'pony express, all aboard, choo, choo! ' + ( - ('whugga ' * 10) + ('choo, choooOOOOOOOooooOOooOooOoo! ')) * 10 + pgraph = u''.join( + ('a', 'ab', 'abc', 'abcd', 'abcde', 'abcdef', 'abcdefgh', + 'abcdefghi', 'abcdefghij', 'abcdefghijk', 'abcdefghijkl', + 'abcdefghijklm', 'abcdefghijklmn', 'abcdefghijklmno',) * 4) pgraph_colored = u''.join([ t.color(n % 7) + t.bold + ch for n, ch in enumerate(pgraph)]) @@ -59,4 +60,4 @@ def child(kind, lines=25, cols=80): assert (len(internal_wrapped) == len(my_wrapped_colored)) assert (len(internal_wrapped[-1]) == t.length(my_wrapped_colored[-1])) - child(all_terms, many_lines, many_columns) + child(kind=all_terms, lines=25, cols=many_columns) diff --git a/setup.py b/setup.py index 3ab36c7e..b358f067 100755 --- a/setup.py +++ b/setup.py @@ -1,66 +1,83 @@ #!/usr/bin/env python from setuptools import setup, find_packages, Command from setuptools.command.develop import develop +from setuptools.command.test import test import sys import os -extra_setup = {} -if sys.version_info >= (3,): - extra_setup['use_2to3'] = True -if sys.version_info <= (2, 7,): - extra_setup['requires'] = ['ordereddict'] +extra = {} -here = os.path.dirname(__file__) -dev_requirements = ['pytest', 'pytest-cov', 'pytest-pep8', - 'pytest-flakes', 'pytest-sugar', 'mock'] - - -class PyTest(Command): - user_options = [] +if sys.version_info < (2, 7,): + extra.update({'install_requires': 'ordereddict'}) - def initialize_options(self): - pass +elif sys.version_info >= (3,): + extra.update({'use_2to3': True}) - def finalize_options(self): - pass +# try: +# import setuptools +# except ImportError: +# from distribute_setup import use_setuptools +# use_setuptools() +# +# +# +#dev_requirements = ['pytest', 'pytest-cov', 'pytest-pep8', +# 'pytest-flakes', 'pytest-sugar', 'mock'] +# - def run(self): - import subprocess - test_files = os.path.join(here, 'tests') - errno = subprocess.call(['py.test', '-x', '--strict', - '--pep8', '--flakes', - '--cov', 'blessed', - '--cov-report', 'html', - test_files]) - raise SystemExit(errno) +#class PyTest(test): +# +# def initialize_options(self): +# test.initialize_options(self) +# test_suite = True +# +# def finalize_options(self): +# test.finalize_options(self) +# self.test_args = ['-x', '--strict', '--pep8', '--flakes', +# '--cov', 'blessed', '--cov-report', 'html', +# '--pyargs', 'blessed.tests'] +# +# def run(self): +# import pytest +## import blessed.tests +## print ('*') +## print(blessed.tests.__file__) +## print ('*') +# raise SystemExit(pytest.main(self.test_args)) -class SetupDevelop(develop): - """Setup development environment suitable for testing.""" - def finalize_options(self): - assert os.getenv('VIRTUAL_ENV'), "Use a virtualenv for this option." - develop.finalize_options(self) - - def run(self): - import subprocess - subprocess.check_call('pip install {reqs}' - .format(reqs=u' '.join(dev_requirements)), - shell=True) - develop.run(self) +#class SetupDevelop(develop): +# """Setup development environment suitable for testing.""" +# +# def finalize_options(self): +# assert os.getenv('VIRTUAL_ENV'), "Please use virtualenv." +# develop.finalize_options(self) +# +# def run(self): +# import subprocess +# reqs = dev_requirements +# reqs.extend(extra_setup['requires']) +# if extra_setup.get('use_2to3', False): +# # install in virtualenv, via 2to3 mechanism +# reqs.append(self.distribution.get_name()) +# subprocess.check_call('pip install {reqs}' +# .format(reqs=u' '.join(reqs)), +# shell=True) +# develop.run(self) +here = os.path.dirname(__file__) setup( name='blessed', version='1.7', description="A feature-filled fork of Erik Rose's blessings project", - long_description=open('README.rst').read(), + long_description=open(os.path.join(here, 'README.rst')).read(), author='Jeff Quast', author_email='contact@jeffquast.com', license='MIT', - packages=find_packages(exclude=['ez_setup']), - tests_require=dev_requirements, - cmdclass={'test': PyTest, 'develop': SetupDevelop}, + packages=['blessed', 'blessed.tests'], url='https://github.com/jquast/blessed', include_package_data=True, + test_suite='blessed.tests', classifiers=[ 'Intended Audience :: Developers', 'Natural Language :: English', @@ -82,5 +99,5 @@ def run(self): keywords=['terminal', 'sequences', 'tty', 'curses', 'ncurses', 'formatting', 'style', 'color', 'console', 'keyboard', 'ansi', 'xterm'], - **extra_setup + **extra ) diff --git a/tox.ini b/tox.ini index 06bd4fd6..1b3de780 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,19 @@ [tox] -envlist = py25, py26, py27, py32, py33 +envlist = py26, + py27, + py33, + pypy [testenv] -commands = nosetests blessings -deps = nose -# So Python 3 runs don't pick up incompatible, un-2to3'd source from the cwd: -changedir = .tox \ No newline at end of file +changedir = {toxworkdir}/{envname} +deps = pytest + pytest-pep8 + pytest-flakes + pytest-cov + mock +commands = py.test -x --strict --pep8 --flakes \ + --cov {envsitepackagesdir}/blessed \ + --cov-report html \ + --cov-report term \ + {envsitepackagesdir}/blessed/tests + From 9d232843b139bd31ac7f592c3d66b14fd25eea54 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 9 Mar 2014 17:17:26 -0700 Subject: [PATCH 023/459] ensure tox is installed --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 202c4063..d5e0489f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,3 +21,4 @@ script: install: # travis wants requirements.txt; we don't, use only 'requires=' in setup.py. - if [[ $TRAVIS_PYTHON_VERSION == 2.5 ]] || [[ $TRAVIS_PYTHON_VERSION == 2.6 ]]; then pip install --use-mirrors ordereddict; fi + - pip install --use-mirrors tox From f12d2c08c3783c4ad54c6479503ba477b185780b Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 9 Mar 2014 17:21:09 -0700 Subject: [PATCH 024/459] remove unsupported travis references --- .travis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index d5e0489f..cca7ebb0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,11 +4,11 @@ env: - TOXENV=py26 - TOXENV=py27 - TOXENV=py33 - # not supported .. - - TOXNENV=py25 # missing - - TOXNENV=py32 # will fail - - TOXNENV=py34 # missing - TOXNENV=pypy # will fail +# # not supported .. +# - TOXNENV=py25 # missing +# - TOXNENV=py32 # will fail +# - TOXNENV=py34 # missing matrix: allow_failures: From 19a9434e6a9edfdeba15473581f6aebbb810a7a0 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 9 Mar 2014 18:07:10 -0700 Subject: [PATCH 025/459] remove pypy --- .travis.yml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index cca7ebb0..786f7caa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,16 +4,6 @@ env: - TOXENV=py26 - TOXENV=py27 - TOXENV=py33 - - TOXNENV=pypy # will fail -# # not supported .. -# - TOXNENV=py25 # missing -# - TOXNENV=py32 # will fail -# - TOXNENV=py34 # missing - -matrix: - allow_failures: - - python: 3.2 # bugs, no u'literals' - - pypy # a few (minor) bugs ?? script: - tox From 804fea3af982062f61ed97ff01583823fdfaa5ce Mon Sep 17 00:00:00 2001 From: jquast Date: Thu, 13 Mar 2014 20:16:50 -0700 Subject: [PATCH 026/459] resolve keyboard demo --- test_keyboard.py => bin/test_keyboard.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) rename test_keyboard.py => bin/test_keyboard.py (92%) mode change 100644 => 100755 diff --git a/test_keyboard.py b/bin/test_keyboard.py old mode 100644 new mode 100755 similarity index 92% rename from test_keyboard.py rename to bin/test_keyboard.py index 7bcd25af..765bef2a --- a/test_keyboard.py +++ b/bin/test_keyboard.py @@ -1,14 +1,16 @@ #!/usr/bin/env python -import blessings +from blessed import Terminal import sys +# _keymap +# _keycodes def main(): """ Displays all known key capabilities that may match the terminal. As each key is pressed on input, it is lit up and points are scored. """ - term = blessings.Terminal() + term = Terminal() score = level = hit_highbit = hit_unicode = 0 dirty = True @@ -18,7 +20,7 @@ def refresh(term, board, level, score, inp): if level_color == 0: level_color = 4 bottom = 0 - for keycode, attr in board.iteritems(): + for keycode, attr in board.items(): sys.stdout.write(u''.join(( term.move(attr['row'], attr['column']), term.color(level_color), @@ -47,7 +49,7 @@ def build_gameboard(term): column, row = 0, 0 board = dict() spacing = 2 - for keycode in sorted(term._keyboard_seqnames.values()): + for keycode in sorted(term._keycodes.values()): if (keycode.startswith('KEY_F') and keycode[-1].isdigit() and int(keycode[len('KEY_F'):]) > 24): @@ -72,13 +74,13 @@ def add_score(score, pts, level): gb = build_gameboard(term) inps = [] - with term.key_at_a_time(): - inp = term.keypress(timeout=0) + with term.cbreak(): + inp = term.inkey(timeout=0) while inp.upper() != 'Q': if dirty: refresh(term, gb, level, score, inps) dirty = False - inp = term.keypress(timeout=5.0) + inp = term.inkey(timeout=5.0) dirty = True if (inp.is_sequence and inp.name in gb and @@ -116,7 +118,7 @@ def add_score(score, pts, level): u'press any key', term.clear_eol,)) ) - term.keypress() + term.inkey() if __name__ == '__main__': main() From a18d0047c0fbd9f2ad2672ba716a1b9916d082c7 Mon Sep 17 00:00:00 2001 From: jquast Date: Thu, 13 Mar 2014 20:19:00 -0700 Subject: [PATCH 027/459] resolve window size (thank you @polyphemus) have not even yet released to pypi, thank you though ! This was just the last little commit i had remaining :-) its much better than [0] and [1], for just this reason. thank you! --- blessed/__init__.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/blessed/__init__.py b/blessed/__init__.py index 403cc5cb..1e943ff5 100644 --- a/blessed/__init__.py +++ b/blessed/__init__.py @@ -253,7 +253,7 @@ def height(self): None may be returned if no suitable size is discovered. """ - return self._height_and_width()[1] + return self._height_and_width().ws_row @property def width(self): @@ -263,7 +263,7 @@ def width(self): None may be returned if no suitable size is discovered. """ - return self._height_and_width()[0] + return self._height_and_width().ws_col @staticmethod def _winsize(fd): @@ -285,17 +285,15 @@ def _height_and_width(self): # -- of course, if both are redirected, we have no use for this fd. for descriptor in self._init_descriptor, sys.__stdout__: try: - winsize = self._winsize(descriptor) - return winsize.ws_row, winsize.ws_col + return self._winsize(descriptor) except IOError: pass - lines, cols = None, None - if os.environ.get('LINES', None) is not None: - lines = int(os.environ['LINES']) - if os.environ.get('COLUMNS', None) is not None: - cols = int(os.environ['COLUMNS']) - return lines, cols + return WINSZ(ws_row=(os.environ.get('LINES', None) is not None + and int(os.environ['LINES']) or None), + ws_col=(os.environ.get('COLUMNS', None) is not None + and int(os.environ['COLUMNS']) or None), + ws_xpixel=None, ws_ypixel=None) @contextlib.contextmanager def location(self, x=None, y=None): From 3dee2a28ad1b47850ee1295b70b99edc28988798 Mon Sep 17 00:00:00 2001 From: jquast Date: Thu, 13 Mar 2014 20:19:11 -0700 Subject: [PATCH 028/459] assert window width with tests --- blessed/tests/test_core.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/blessed/tests/test_core.py b/blessed/tests/test_core.py index dee7b523..3af21370 100644 --- a/blessed/tests/test_core.py +++ b/blessed/tests/test_core.py @@ -107,6 +107,8 @@ def child(kind): assert t._init_descriptor == sys.__stdout__.fileno() assert (isinstance(t.height, int)) assert (isinstance(t.width, int)) + assert t.height == t._height_and_width()[0] + assert t.width == t._height_and_width()[1] child(all_terms) From 50312b53bfa87a8249bac81835f6012b1af0fe3f Mon Sep 17 00:00:00 2001 From: jquast Date: Thu, 13 Mar 2014 20:19:22 -0700 Subject: [PATCH 029/459] assert many window shapes also --- blessed/tests/test_length_sequence.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/blessed/tests/test_length_sequence.py b/blessed/tests/test_length_sequence.py index 20c79677..a73c5cb0 100644 --- a/blessed/tests/test_length_sequence.py +++ b/blessed/tests/test_length_sequence.py @@ -8,6 +8,7 @@ all_standard_terms, as_subprocess, TestTerminal, + many_columns, many_lines, all_terms, ) @@ -89,19 +90,32 @@ def child(kind): child(all_terms) +def test_winsize(many_lines, many_columns): + """Test height and width is appropriately queried in a pty.""" + @as_subprocess + def child(lines=25, cols=80): + # set the pty's virtual window size + val = struct.pack('HHHH', lines, cols, 0, 0) + fcntl.ioctl(sys.__stdout__.fileno(), termios.TIOCSWINSZ, val) + t = TestTerminal() + winsize = t._height_and_width() + assert t.width == cols + assert t.height == lines + assert winsize.ws_col == cols + assert winsize.ws_row == lines + + child(lines=many_lines, cols=many_columns) + + def test_Sequence_alignment(all_terms, many_lines): """Tests methods related to Sequence class, namely ljust, rjust, center.""" @as_subprocess def child(kind, lines=25, cols=80): # set the pty's virtual window size val = struct.pack('HHHH', lines, cols, 0, 0) -# try: fcntl.ioctl(sys.__stdout__.fileno(), termios.TIOCSWINSZ, val) -# except IOError: -# # unable to set screen size of tty, skip -# return + t = TestTerminal() - t = TestTerminal(kind=kind) pony_msg = 'pony express, all aboard, choo, choo!' pony_len = len(pony_msg) pony_colored = u''.join( From 7bbcf51e8d089a13cd45440e161943b06914c39d Mon Sep 17 00:00:00 2001 From: jquast Date: Thu, 13 Mar 2014 20:19:46 -0700 Subject: [PATCH 030/459] replace list with collections.deque() as we insert(0) and pop(), deque is much more efficient data structure to use --- blessed/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/blessed/__init__.py b/blessed/__init__.py index 1e943ff5..fc374067 100644 --- a/blessed/__init__.py +++ b/blessed/__init__.py @@ -144,7 +144,7 @@ def __init__(self, kind=None, stream=None, force_styling=False): # build database of sequence <=> KEY_NAME self._keymap = keyboard.get_keyboard_sequences(self) - self._keyboard_buf = [] + self._keyboard_buf = collections.deque() locale.setlocale(locale.LC_ALL, '') self._encoding = locale.getpreferredencoding() self._keyboard_decoder = codecs.getincrementaldecoder(self._encoding)() @@ -677,8 +677,7 @@ def _resolve(text): ucs += _decode_next() ks = _resolve(ucs) - for remaining in ucs[len(ks):]: - self._keyboard_buf.insert(0, remaining) + self._keyboard_buf.extendleft(ucs[len(ks):]) return ks From 03565e73078ec1af3e4dc3c5aec6b4c916408f04 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 16 Mar 2014 02:34:43 -0700 Subject: [PATCH 031/459] print '' -> print('') for python3 compatibility --- README.rst | 61 +++++++++++++++++++++++++++--------------------------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/README.rst b/README.rst index 878632e4..854a4996 100644 --- a/README.rst +++ b/README.rst @@ -8,11 +8,11 @@ Coding with Blessed looks like this... :: t = Terminal() - print t.bold('Hi there!') - print t.bold_red_on_bright_green('It hurts my eyes!') + print(t.bold('Hi there!')) + print(t.bold_red_on_bright_green('It hurts my eyes!')) with t.location(0, t.height - 1): - print t.center(t.blink('press any key to continue.')) + print(t.center(t.blink('press any key to continue.'))) with t.cbreak(): t.inkey() @@ -20,8 +20,7 @@ Coding with Blessed looks like this... :: Or, for byte-level control, you can drop down and play with raw terminal capabilities:: - print '{t.bold}All your {t.red}bold and red base{t.normal}'.format(t=t) - print t.wingo(2) + print('{t.bold}All your {t.red}bold and red base{t.normal}'.format(t=t)) The Pitch ========= @@ -65,14 +64,14 @@ of the screen:: normal = tigetstr('sgr0') else: sc = cup = rc = underline = normal = '' - print sc # Save cursor position. + print(sc) # Save cursor position. if cup: # tigetnum('lines') doesn't always update promptly, hence this: height = struct.unpack('hhhh', ioctl(0, TIOCGWINSZ, '\000' * 8))[0] - print tparm(cup, height - 1, 0) # Move cursor to bottom. - print 'This is {under}underlined{normal}!'.format(under=underline, - normal=normal) - print rc # Restore cursor position. + print(tparm(cup, height - 1, 0)) # Move cursor to bottom. + print('This is {under}underlined{normal}!'.format(under=underline, + normal=normal)) + print(rc) # Restore cursor position. That was long and full of incomprehensible trash! Let's try it again, this time with Blessed:: @@ -81,7 +80,7 @@ with Blessed:: term = Terminal() with term.location(0, term.height - 1): - print 'This is', term.underline('pretty!') + print('This is', term.underline('pretty!')) Much better. @@ -102,18 +101,19 @@ available as attributes on a ``Terminal``. For example:: from blessed import Terminal term = Terminal() - print 'I am ' + term.bold + 'bold' + term.normal + '!' + print('I am ' + term.bold + 'bold' + term.normal + '!') Though they are strings at heart, you can also use them as callable wrappers so you don't have to say ``normal`` afterward:: - print 'I am', term.bold('bold') + '!' + print('I am', term.bold('bold') + '!') Or, if you want fine-grained control while maintaining some semblance of brevity, you can combine it with Python's string formatting, which makes attributes easy to access:: - print 'All your {t.red}base {t.underline}are belong to us{t.normal}'.format(t=term) + print('All your {t.red}base {t.underline}are belong to us{t.normal}' + .format(t=term)) Simple capabilities of interest include... @@ -156,14 +156,14 @@ attributes:: from blessed import Terminal term = Terminal() - print term.red + term.on_green + 'Red on green? Ick!' + term.normal - print term.bright_red + term.on_bright_blue + 'This is even worse!' + term.normal + print(term.red + term.on_green + 'Red on green? Ick!' + term.normal) + print(term.bright_red + term.on_bright_blue + 'This is even worse!' + term.normal) You can also call them as wrappers, which sets everything back to normal at the end:: - print term.red_on_green('Red on green? Ick!') - print term.yellow('I can barely see it.') + print(term.red_on_green('Red on green? Ick!')) + print(term.yellow('I can barely see it.')) The available colors are... @@ -211,7 +211,7 @@ all together:: Or you can use your newly coined attribute as a wrapper, which implicitly sets everything back to normal afterward:: - print term.bold_underline_green_on_yellow('Woo') + print(term.bold_underline_green_on_yellow('Woo')) This compound notation comes in handy if you want to allow users to customize the formatting of your app: just have them pass in a format specifier like @@ -240,22 +240,22 @@ screen. ``Terminal`` provides a context manager for doing this concisely:: term = Terminal() with term.location(0, term.height - 1): - print 'Here is the bottom.' - print 'This is back where I came from.' + print('Here is the bottom.') + print('This is back where I came from.') Parameters to ``location()`` are ``x`` and then ``y``, but you can also pass just one of them, leaving the other alone. For example... :: with term.location(y=10): - print 'We changed just the row.' + print('We changed just the row.') If you're doing a series of ``move`` calls (see below) and want to return the cursor to its original position afterward, call ``location()`` with no arguments, and it will do only the position restoring:: with term.location(): - print term.move(1, 1) + 'Hi' - print term.move(9, 9) + 'Mom' + print(term.move(1, 1) + 'Hi') + print(term.move(9, 9) + 'Mom') Note that, since ``location()`` uses the terminal's built-in position-remembering machinery, you can't usefully nest multiple calls. Use @@ -271,7 +271,7 @@ this:: from blessed import Terminal term = Terminal() - print term.move(10, 1) + 'Hi, mom!' + print(term.move(10, 1) + 'Hi, mom!') ``move`` Position the cursor elsewhere. Parameters are y coordinate, then x @@ -306,7 +306,7 @@ cursor one character in various directions: For example... :: - print term.move_up + 'Howdy!' + print(term.move_up + 'Howdy!') Height And Width ---------------- @@ -388,8 +388,8 @@ and just stick to content, since you're apparently headed into a pipe:: term = Terminal() if term.does_styling: with term.location(0, term.height - 1): - print 'Progress: [=======> ]' - print term.bold('Important stuff') + print('Progress: [=======> ]') + print(term.bold('Important stuff')) Sequence Awareness ------------------ @@ -401,9 +401,8 @@ screen's width as the default ``width`` value:: from blessed import Terminal term = Terminal() - print (''.join(term.move(term.height / 2), # move-to vertical center - term.center(term.bold('X')) # horizontal ceneted - term.move(terminal.height -1),)) # move-to vertical bottom + with term.location(y=term.height / 2): + print (term.center(term.bold('X')) Any string containing sequences may have its printable length measured using ``.length``. Additionally, ``textwrap.wrap()`` is supplied on the Terminal class From e0dcf5a6120ac93d2d3dec79e21c7d04e417f25f Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 16 Mar 2014 02:35:44 -0700 Subject: [PATCH 032/459] cleanup wrap() documentation --- README.rst | 95 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 85 insertions(+), 10 deletions(-) diff --git a/README.rst b/README.rst index 854a4996..21e55a28 100644 --- a/README.rst +++ b/README.rst @@ -404,23 +404,98 @@ screen's width as the default ``width`` value:: with term.location(y=term.height / 2): print (term.center(term.bold('X')) -Any string containing sequences may have its printable length measured using -``.length``. Additionally, ``textwrap.wrap()`` is supplied on the Terminal class -as method ``.wrap`` method that is also sequence-aware, so now you may word-wrap -strings containing sequences. The following example uses a width value of 25 to -format a poem from Tao Te Ching:: +Any string containing sequences may have its printable length measured using the +``.length`` method. Additionally, ``textwrap.wrap()`` is supplied on the Terminal +class as method ``.wrap`` method that is also sequence-aware, so now you may +word-wrap strings containing sequences. The following example displays a poem +from Tao Te Ching, word-wrapped to 25 columns:: from blessed import Terminal t = Terminal() - poem = (term.bold_blue('Plan difficult tasks ') - + term.bold_black('through the simplest tasks'), - term.bold_cyan('Achieve large tasks ') - + term.cyan('through the smallest tasks')) + poem = u''.join((term.bold_blue('Plan difficult tasks '), + term.bold_black('through the simplest tasks'), + term.bold_cyan('Achieve large tasks '), + term.cyan('through the smallest tasks')) for line in poem: print('\n'.join(term.wrap(line, width=25, - subsequent_indent=' '*4))) + subsequent_indent=' ' * 4))) + +Keyboard Input +-------------- + +You may have noticed that the built-in python ``raw_input`` doesn't return +until the return key is pressed (line buffering). Special `termios(4)`_ routines +are required to enter Non-canonical, known in curses as `cbreak(3)_`. + +You may also have noticed that special keys, such as arrow keys, actually +input several byte characters, and different terminals send different strings. + +Finally, you may have noticed characters such as ä from ``raw_input`` are also +several byte characters in a sequence ('\xc3\xa4') that must be decoded. + +Handling all of these possibilities can be quite difficult, but Blessed has +you covered! + +cbreak +~~~~~~ + +The context manager ``cbreak`` can be used to enter key-at-a-time mode. +Any keypress by the user is immediately value:: + + from blessed import Terminal + import sys + + t = Terminal() + + with t.cbreak(): + # blocks until any key is pressed. + sys.stdin.read(1) + +inkey +~~~~~ + +The method ``inkey`` resolves many issues with terminal input by returning +a unicode-derived ``Keypress`` instance. Although its return value may be +printed, joined with, or compared to other unicode strings, it also provides +the special attributes ``is_sequence`` (bool), ``code`` (int), +and ``name`` (str):: + + from blessed import Terminal + + t = Terminal() + + print("press 'q' to quit.") + with t.cbreak(): + val = None + while val not in (u'q', u'Q',): + val = t.inkey(timeout=5) + if not val: + # timeout + print("It sure is quiet in here ...") + elif val.is_sequence: + print("got sequence: {}.".format((str(val), val.name, val.code))) + elif val: + print("got {}.".format(val)) + print('bye!') + +Its output might appear as:: + + got sequence: ('\x1b[A', 'KEY_UP', 259). + got sequence: ('\x1b[1;2A', 'KEY_SUP', 337). + got sequence: ('\x1b[17~', 'KEY_F6', 270). + got sequence: ('\x1b', 'KEY_ESCAPE', 361). + got sequence: ('\n', 'KEY_ENTER', 343). + got /. + It sure is quiet in here ... + got sequence: ('\x1bOP', 'KEY_F1', 265). + It sure is quiet in here ... + got q. + bye! + +.. _`cbreak(3)`: www.openbsd.org/cgi-bin/man.cgi?query=cbreak&apropos=0&sektion=3 +.. _`termios(4)`: www.openbsd.org/cgi-bin/man.cgi?query=termios&apropos=0&sektion=4 Shopping List From 50a39db8a64460b92165dbe112a15c4aa8eb16dc Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 16 Mar 2014 02:36:35 -0700 Subject: [PATCH 033/459] document inkey, cbreak + polishing up remaining --- README.rst | 100 ++++++++++++++++++++++------------------------------- 1 file changed, 42 insertions(+), 58 deletions(-) diff --git a/README.rst b/README.rst index 21e55a28..46180775 100644 --- a/README.rst +++ b/README.rst @@ -29,10 +29,9 @@ Blessed lifts several of curses_' limiting assumptions, and it makes your code pretty, too: * Use styles, color, and maybe a little positioning without necessarily - clearing the whole - screen first. + clearing the whole screen first. * Leave more than one screenful of scrollback in the buffer after your program - exits, like a well-behaved command-line app should. + exits, like a well-behaved command-line application should. * Get rid of all those noisy, C-like calls to ``tigetstr`` and ``tparm``, so your code doesn't get crowded out by terminal bookkeeping. * Act intelligently when somebody redirects your output to a file, omitting the @@ -95,22 +94,20 @@ of things about the terminal. Terminal terminal terminal. Simple Formatting ----------------- -Lots of handy formatting codes ("capabilities" in low-level parlance) are -available as attributes on a ``Terminal``. For example:: +Lots of handy formatting codes (capabilities, `terminfo(5)`_) are available +as attributes on a ``Terminal``. For example:: from blessed import Terminal term = Terminal() print('I am ' + term.bold + 'bold' + term.normal + '!') -Though they are strings at heart, you can also use them as callable wrappers so -you don't have to say ``normal`` afterward:: +Though they are strings at heart, you can also use them as callable wrappers, +which automatically ends each string with ``normal`` attributes:: print('I am', term.bold('bold') + '!') -Or, if you want fine-grained control while maintaining some semblance of -brevity, you can combine it with Python's string formatting, which makes -attributes easy to access:: +You may also use Python's string ``.format`` method:: print('All your {t.red}base {t.underline}are belong to us{t.normal}' .format(t=term)) @@ -119,14 +116,14 @@ Simple capabilities of interest include... * ``bold`` * ``reverse`` -* ``underline`` -* ``no_underline`` (which turns off underlining) * ``blink`` * ``normal`` (which turns off everything, even colors) Here are a few more which are less likely to work on all terminals: * ``dim`` +* ``underline`` +* ``no_underline`` (which turns off underlining) * ``italic`` and ``no_italic`` * ``shadow`` and ``no_shadow`` * ``standout`` and ``no_standout`` @@ -142,10 +139,10 @@ undo certain pieces of formatting, even at the lowest level. You might also notice that the above aren't the typical incomprehensible terminfo capability names; we alias a few of the harder-to-remember ones for readability. However, you aren't limited to these: you can reference any -string-returning capability listed on the `terminfo man page`_ by the name -under the "Cap-name" column: for example, ``term.rum``. +string-returning capability listed on the `terminfo(5)`_ manual page, by the name +under the **Cap-name** column: for example, ``term.rum`` (End reverse character). -.. _`terminfo man page`: http://www.manpagez.com/man/5/terminfo/ +.. _`terminfo(5)`: http://www.openbsd.org/cgi-bin/man.cgi?query=terminfo&apropos=0&sektion=5 Color ----- @@ -206,11 +203,6 @@ all together:: from blessed import Terminal term = Terminal() - print term.bold_underline_green_on_yellow + 'Woo' + term.normal - -Or you can use your newly coined attribute as a wrapper, which implicitly sets -everything back to normal afterward:: - print(term.bold_underline_green_on_yellow('Woo')) This compound notation comes in handy if you want to allow users to customize @@ -232,9 +224,9 @@ a few choices. Moving Temporarily ~~~~~~~~~~~~~~~~~~ -Most often, you'll need to flit to a certain location, print something, and -then return: for example, when updating a progress bar at the bottom of the -screen. ``Terminal`` provides a context manager for doing this concisely:: +Most often, moving to a screen position is only temporary. A contest manager, +``location`` is provided to move to a screen position and restore the previous +position upon exit:: from blessed import Terminal @@ -291,7 +283,8 @@ parametrized if you pass params to them as if they were functions. Consequently, you can also reference any other string-returning capability listed on the `terminfo man page`_ by its name under the "Cap-name" column. -.. _`terminfo man page`: http://www.manpagez.com/man/5/terminfo/ + +.. _`terminfo(5)`: http://www.openbsd.org/cgi-bin/man.cgi?query=terminfo&apropos=0&sektion=5 One-Notch Movement ~~~~~~~~~~~~~~~~~~ @@ -339,21 +332,14 @@ Blessed provides syntactic sugar over some screen-clearing capabilities: Full-Screen Mode ---------------- -Perhaps you have seen a full-screen program, such as an editor, restore the -exact previous state of the terminal upon exiting, including, for example, the -command-line prompt from which it was launched. Curses pretty much forces you -into this behavior, but Blessed makes it optional. If you want to do the -state-restoration thing, use these capabilities: +If you've ever noticed a program, such as an editor, restores the previous +screen state (Your shell prompt) after exiting, you're seeing the +``enter_fullscreen`` and ``exit_fullscreen`` attributes in effect. ``enter_fullscreen`` - Switch to the terminal mode where full-screen output is sanctioned. Print - this before you do any output. + Switch to alternate screen, previous screen is stored by terminal driver. ``exit_fullscreen`` - Switch back to normal mode, restoring the exact state from before - ``enter_fullscreen`` was used. - -Using ``exit_fullscreen`` will wipe away any trace of your program's output, so -reserve it for when you don't want to leave anything behind in the scrollback. + Switch back to standard screen, restoring the same termnal state. There's also a context manager you can use as a shortcut:: @@ -361,27 +347,25 @@ There's also a context manager you can use as a shortcut:: term = Terminal() with term.fullscreen(): - # Print some stuff. - -Besides brevity, another advantage is that it switches back to normal mode even -if an exception is raised in the ``with`` block. + print(term.move_y(term.height/2) + + term.center('press any key')) + term.inkey() Pipe Savvy ---------- -If your program isn't attached to a terminal, like if it's being piped to -another command or redirected to a file, all the capability attributes on +If your program isn't attached to a terminal, such as piped to a program +like ``less(1)`` or redirected to a file, all the capability attributes on ``Terminal`` will return empty strings. You'll get a nice-looking file without any formatting codes gumming up the works. -If you want to override this--like if you anticipate your program being piped -through ``less -r``, which handles terminal escapes just fine--pass +If you want to override this, such as using ``less -r``, pass argument ``force_styling=True`` to the ``Terminal`` constructor. In any case, there is a ``does_styling`` attribute on ``Terminal`` that lets -you see whether your capabilities will return actual, working formatting codes. -If it's false, you should refrain from drawing progress bars and other frippery -and just stick to content, since you're apparently headed into a pipe:: +you see whether the terminal attached to the output stream is capable of +formatting. If it is ``False``, you may refrain from drawing progress +bars and other frippery and just stick to content:: from blessed import Terminal @@ -395,8 +379,8 @@ Sequence Awareness ------------------ Blessed may measure the printable width of strings containing sequences, -providing ``.center``, ``.ljust``, and ``.rjust``, using the terminal -screen's width as the default ``width`` value:: +providing ``.center``, ``.ljust``, and ``.rjust`` methods, using the +terminal screen's width as the default ``width`` value:: from blessed import Terminal @@ -505,16 +489,15 @@ There are decades of legacy tied up in terminal interaction, so attention to detail and behavior in edge cases make a difference. Here are some ways Blessed has your back: -* Uses the terminfo database so it works with any terminal type +* Uses the `terminfo(5)`_ database so it works with any terminal type * Provides up-to-the-moment terminal height and width, so you can respond to terminal size changes (SIGWINCH signals). (Most other libraries query the ``COLUMNS`` and ``LINES`` environment variables or the ``cols`` or ``lines`` terminal capabilities, which don't update promptly, if at all.) -* Avoids making a mess if the output gets piped to a non-terminal -* Works great with standard Python string templating -* Provides convenient access to all terminal capabilities, not just a sugared - few -* Outputs to any file-like object, not just stdout +* Avoids making a mess if the output gets piped to a non-terminal. +* Works great with standard Python string formatting. +* Provides convenient access to **all** terminal capabilities. +* Outputs to any file-like object (StringIO, file), not just stdout. * Keeps a minimum of internal state, so you can feel free to mix and match with calls to curses or whatever other terminal libraries you like @@ -545,6 +528,10 @@ Version History =============== 1.7 + * introduced context manager ``cbreak`` which is equivalent to ``tty.cbreak``, + placing the terminal in 'cooked' mode, allowing input from stdin to be read + as each key is pressed (line-buffering disabled). + * Forked github project 'erikrose/blessings' to 'jquast/blessed', this project was previously known as 'blessings' version 1.6 and prior. * Created ``python setup.py develop`` for developer environment. @@ -567,9 +554,6 @@ Version History * introduced ``term.wrap()``, allows text containing sequences to be word-wrapped without breaking mid-sequence and honoring their printable width. - * introduced context manager ``cbreak`` which is equivalent to ``tty.cbreak``, - placing the terminal in 'cooked' mode, allowing input from stdin to be read - as each key is pressed (line-buffering disabled). * introduced method ``inkey()``, which will return 1 or more characters as a unicode sequence, with attributes ``.code`` and ``.name`` non-None when a multibyte sequence is received, allowing arrow keys and such to be From a1df2322ceeb8e946de8e5deb918fa3a743eeb9b Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 16 Mar 2014 02:38:06 -0700 Subject: [PATCH 034/459] allow pypy to skip on TIOCSWINSIZ make a static 27-column test to replace it --- blessed/tests/test_wrap.py | 52 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/blessed/tests/test_wrap.py b/blessed/tests/test_wrap.py index a56f5e30..15acbc27 100644 --- a/blessed/tests/test_wrap.py +++ b/blessed/tests/test_wrap.py @@ -1,3 +1,4 @@ +import platform import textwrap import termios import struct @@ -11,7 +12,10 @@ all_terms, ) +import pytest +@pytest.mark.skipif(platform.python_implementation() == 'PyPy', + reason='PyPy fails TIOCSWINSZ') def test_SequenceWrapper(all_terms, many_columns): """Test that text wrapping accounts for sequences correctly.""" @as_subprocess @@ -61,3 +65,51 @@ def child(kind, lines=25, cols=80): assert (len(internal_wrapped[-1]) == t.length(my_wrapped_colored[-1])) child(kind=all_terms, lines=25, cols=many_columns) + + +def test_SequenceWrapper_27(all_terms): + """Test that text wrapping accounts for sequences correctly.""" + WIDTH = 27 + @as_subprocess + def child(kind): + # build a test paragraph, along with a very colorful version + t = TestTerminal(kind=kind) + pgraph = u''.join( + ('a', 'ab', 'abc', 'abcd', 'abcde', 'abcdef', 'abcdefgh', + 'abcdefghi', 'abcdefghij', 'abcdefghijk', 'abcdefghijkl', + 'abcdefghijklm', 'abcdefghijklmn', 'abcdefghijklmno',) * 4) + + pgraph_colored = u''.join([ + t.color(n % 7) + t.bold + ch + for n, ch in enumerate(pgraph)]) + + internal_wrapped = textwrap.wrap(pgraph, width=WIDTH, + break_long_words=False) + my_wrapped = t.wrap(pgraph) + my_wrapped_colored = t.wrap(pgraph_colored) + + # ensure we textwrap ascii the same as python + assert (internal_wrapped == my_wrapped) + + # ensure our first and last line wraps at its ends + first_l = internal_wrapped[0] + last_l = internal_wrapped[-1] + my_first_l = my_wrapped_colored[0] + my_last_l = my_wrapped_colored[-1] + assert (len(first_l) == t.length(my_first_l)) + assert (len(last_l) == t.length(my_last_l)) + assert (len(internal_wrapped[-1]) == t.length(my_wrapped_colored[-1])) + + # ensure our colored textwrap is the same line length + assert (len(internal_wrapped) == len(my_wrapped_colored)) + # test subsequent_indent= + internal_wrapped = textwrap.wrap(pgraph, width, break_long_words=False, + subsequent_indent=' '*4) + my_wrapped = t.wrap(pgraph, subsequent_indent=' '*4) + my_wrapped_colored = t.wrap(pgraph_colored, subsequent_indent=' '*4) + + assert (internal_wrapped == my_wrapped) + assert (len(internal_wrapped) == len(my_wrapped_colored)) + assert (len(internal_wrapped[-1]) == t.length(my_wrapped_colored[-1])) + + child(kind=all_terms) From 65784fa010975851e30d9851660cb7265a5b025a Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 16 Mar 2014 02:39:12 -0700 Subject: [PATCH 035/459] use kind= value, previously skipped and resolve various tests about compound formatters. we've learned that term.green('xyz') becomes 'xyz' on terminals without color. However, term.underline('xyz') actually becomes 'xyz' + term.normal for terminals without underline due to an error in formatters.py --- blessed/tests/test_sequences.py | 123 ++++++++++++++++++++------------ 1 file changed, 77 insertions(+), 46 deletions(-) diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index 77bff4a3..3cf40330 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -4,18 +4,19 @@ from StringIO import StringIO except ImportError: from io import StringIO +import platform import sys import os from accessories import ( unsupported_sequence_terminals, + all_standard_terms, as_subprocess, TestTerminal, unicode_parm, many_columns, unicode_cap, many_lines, - all_terms, ) import pytest @@ -180,7 +181,7 @@ def child(): child() -def test_mnemonic_colors(all_terms): +def test_mnemonic_colors(all_standard_terms): """Make sure color shortcuts work.""" @as_subprocess def child(kind): @@ -202,74 +203,102 @@ def on_color(t, num): assert (t.on_bright_black == on_color(t, 8)) assert (t.on_bright_green == on_color(t, 10)) - child(all_terms) + child(all_standard_terms) -def test_callable_numeric_colors(all_terms): +def test_callable_numeric_colors(all_standard_terms): """``color(n)`` should return a formatting wrapper.""" @as_subprocess def child(kind): - t = TestTerminal() + t = TestTerminal(kind=kind) assert (t.color(5)('smoo') == t.magenta + 'smoo' + t.normal) assert (t.color(5)('smoo') == t.color(5) + 'smoo' + t.normal) assert (t.on_color(2)('smoo') == t.on_green + 'smoo' + t.normal) assert (t.on_color(2)('smoo') == t.on_color(2) + 'smoo' + t.normal) - child(all_terms) + child(all_standard_terms) -def test_null_callable_numeric_colors(all_terms): +def test_null_callable_numeric_colors(all_standard_terms): """``color(n)`` should be a no-op on null terminals.""" @as_subprocess def child(kind): - t = TestTerminal(stream=StringIO()) + t = TestTerminal(stream=StringIO(), kind=kind) assert (t.color(5)('smoo') == 'smoo') assert (t.on_color(6)('smoo') == 'smoo') - child(all_terms) + child(all_standard_terms) -def test_naked_color_cap(all_terms): +def test_naked_color_cap(all_standard_terms): """``term.color`` should return a stringlike capability.""" @as_subprocess def child(kind): - t = TestTerminal() + t = TestTerminal(kind=kind) assert (t.color + '' == t.setaf + '') - child(all_terms) + child(all_standard_terms) -def test_formatting_functions(all_terms): +def test_formatting_functions(all_standard_terms): """Test simple and compound formatting wrappers.""" @as_subprocess def child(kind): - t = TestTerminal() + t = TestTerminal(kind=kind) # test simple sugar, - expected_output = t.bold + u'hi' + t.normal - assert (t.bold(u'hi') == expected_output) + if t.bold: + expected_output = u''.join((t.bold, u'hi', t.normal)) + else: + expected_output = u'hi' + assert t.bold(u'hi') == expected_output # Plain strs for Python 2.x - expected_output = t.green + 'hi' + t.normal - assert (t.green('hi') == expected_output) - # Test some non-ASCII chars, probably not necessary: - expected_output = u''.join((t.bold, t.green, u'boö', t.normal)) - assert (t.bold_green(u'boö') == expected_output) - expected_output = u''.join( - (t.bold, t.underline, t.green, t.on_red, u'boo', t.normal)) - assert (t.bold_underline_green_on_red('boo') == expected_output) - # Very compounded strings - expected_output = u''.join( - (t.on_bright_red, t.bold, t.bright_green, - t.underline, u'meh', t.normal)) + if t.green: + expected_output = u''.join((t.green, 'hi', t.normal)) + else: + expected_output = u'hi' + assert t.green('hi') == expected_output + # Test unicode + if t.underline: + expected_output = u''.join((t.underline, u'boö', t.normal)) + else: + expected_output = u'boö' + assert (t.underline(u'boö') == expected_output) + + if t.subscript: + expected_output = u''.join((t.subscript, u'[1]', t.normal)) + else: + expected_output = u'[1]' + + assert (t.subscript(u'[1]') == expected_output) + + child(all_standard_terms) + + +def test_compound_formatting(all_standard_terms): + """Test simple and compound formatting wrappers.""" + @as_subprocess + def child(kind): + t = TestTerminal(kind=kind) + if any((t.bold, t.green)): + expected_output = u''.join((t.bold, t.green, u'boö', t.normal)) + else: + expected_output = u'boö' + assert t.bold_green(u'boö') == expected_output + + if any((t.on_bright_red, t.bold, t.bright_green, t.underline)): + expected_output = u''.join( + (t.on_bright_red, t.bold, t.bright_green, t.underline, u'meh', + t.normal)) + else: + expected_output = u'meh' assert (t.on_bright_red_bold_bright_green_underline('meh') == expected_output) - child(all_terms) - -def test_formatting_functions_without_tty(all_terms): +def test_formatting_functions_without_tty(all_standard_terms): """Test crazy-ass formatting wrappers when there's no tty.""" @as_subprocess def child(kind): - t = TestTerminal(kind=kind, stream=StringIO()) + t = TestTerminal(kind=kind, stream=StringIO(), force_styling=False) assert (t.bold(u'hi') == u'hi') assert (t.green('hi') == u'hi') # Test non-ASCII chars, no longer really necessary: @@ -277,14 +306,14 @@ def child(kind): assert (t.bold_underline_green_on_red('loo') == u'loo') assert (t.on_bright_red_bold_bright_green_underline('meh') == u'meh') - child(all_terms) + child(all_standard_terms) -def test_nice_formatting_errors(all_terms): +def test_nice_formatting_errors(all_standard_terms): """Make sure you get nice hints if you misspell a formatting wrapper.""" @as_subprocess def child(kind): - t = TestTerminal() + t = TestTerminal(kind=kind) try: t.bold_misspelled('hey') assert not t.is_a_tty or False, 'Should have thrown exception' @@ -305,21 +334,23 @@ def child(kind): e = sys.exc_info()[1] assert 'probably misspelled' not in e.args[0] - try: - t.bold_misspelled('a', 'b') # >1 string arg - assert not t.is_a_tty or False, 'Should have thrown exception' - except TypeError: - e = sys.exc_info()[1] - assert 'probably misspelled' not in e.args[0] + if platform.python_implementation() != 'PyPy': + # PyPy fails to toss an exception? + try: + t.bold_misspelled('a', 'b') # >1 string arg + assert not t.is_a_tty or False, 'Should have thrown exception' + except TypeError: + e = sys.exc_info()[1] + assert 'probably misspelled' not in e.args[0] - child(all_terms) + child(all_standard_terms) -def test_null_callable_string(all_terms): +def test_null_callable_string(all_standard_terms): """Make sure NullCallableString tolerates all kinds of args.""" @as_subprocess - def child(kind='xterm-256color'): - t = TestTerminal(stream=StringIO()) + def child(kind): + t = TestTerminal(stream=StringIO(), kind=kind) assert (t.clear == '') assert (t.move(1 == 2) == '') assert (t.move_x(1) == '') @@ -329,4 +360,4 @@ def child(kind='xterm-256color'): assert (t.uhh(9876) == '') assert (t.clear('x') == 'x') - child(all_terms) + child(all_standard_terms) From b906d620d8ec04170172336b96568734bc602c14 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 16 Mar 2014 02:47:29 -0700 Subject: [PATCH 036/459] test os.environ['COLUMNS'] fallback --- blessed/tests/test_length_sequence.py | 29 +++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/blessed/tests/test_length_sequence.py b/blessed/tests/test_length_sequence.py index a73c5cb0..c3ea6052 100644 --- a/blessed/tests/test_length_sequence.py +++ b/blessed/tests/test_length_sequence.py @@ -3,6 +3,11 @@ import struct import fcntl import sys +import os +try: + from StringIO import StringIO +except ImportError: + from io import StringIO from accessories import ( all_standard_terms, @@ -90,6 +95,30 @@ def child(kind): child(all_terms) +def test_env_winsize(): + """Test height and width is appropriately queried in a pty.""" + @as_subprocess + def child(): + # set the pty's virtual window size + os.environ['COLUMNS'] = '99' + os.environ['LINES'] = '11' + t = TestTerminal(stream=StringIO()) + save_init = t._init_descriptor + save_stdout = sys.__stdout__ + try: + t._init_descriptor = None + sys.__stdout__ = None + winsize = t._height_and_width() + width = t.width + height = t.height + finally: + t._init_descriptor = save_init + sys.__stdout__ = save_stdout + assert winsize.ws_col == width == 99 + assert winsize.ws_row == height == 11 + + child() + def test_winsize(many_lines, many_columns): """Test height and width is appropriately queried in a pty.""" @as_subprocess From 72ba14595d049cd66ae8bc8bc67eb780b12aff82 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 16 Mar 2014 02:47:45 -0700 Subject: [PATCH 037/459] use fixtures ! --- blessed/tests/test_length_sequence.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/blessed/tests/test_length_sequence.py b/blessed/tests/test_length_sequence.py index c3ea6052..534666d4 100644 --- a/blessed/tests/test_length_sequence.py +++ b/blessed/tests/test_length_sequence.py @@ -143,7 +143,7 @@ def child(kind, lines=25, cols=80): # set the pty's virtual window size val = struct.pack('HHHH', lines, cols, 0, 0) fcntl.ioctl(sys.__stdout__.fileno(), termios.TIOCSWINSZ, val) - t = TestTerminal() + t = TestTerminal(kind=kind) pony_msg = 'pony express, all aboard, choo, choo!' pony_len = len(pony_msg) @@ -168,7 +168,7 @@ def child(kind, lines=25, cols=80): def test_sequence_is_movement_false(all_terms): """Test parser about sequences that do not move the cursor.""" @as_subprocess - def child_mnemonics_wontmove(kind='xterm-256color'): + def child_mnemonics_wontmove(kind): from blessed.sequences import measure_length t = TestTerminal(kind=kind) assert (0 == measure_length(u'', t)) @@ -205,7 +205,7 @@ def child_mnemonics_wontmove(kind='xterm-256color'): def test_sequence_is_movement_true(all_standard_terms): """Test parsers about sequences that move the cursor.""" @as_subprocess - def child_mnemonics_willmove(kind='xterm-256color'): + def child_mnemonics_willmove(kind): from blessed.sequences import measure_length t = TestTerminal(kind=kind) # movements From 109782584940f5d68a646486376dc7c0395e6bf8 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 16 Mar 2014 02:48:02 -0700 Subject: [PATCH 038/459] skip TIOCSWINSIZE on pypy implementations --- blessed/tests/test_length_sequence.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/blessed/tests/test_length_sequence.py b/blessed/tests/test_length_sequence.py index 534666d4..8d3f8107 100644 --- a/blessed/tests/test_length_sequence.py +++ b/blessed/tests/test_length_sequence.py @@ -1,4 +1,5 @@ import itertools +import platform import termios import struct import fcntl @@ -18,6 +19,8 @@ all_terms, ) +import pytest + def test_sequence_length(all_terms): """Ensure T.length(string containing sequence) is correct.""" @@ -119,6 +122,9 @@ def child(): child() + +@pytest.mark.skipif(platform.python_implementation() == 'PyPy', + reason='PyPy fails TIOCSWINSZ') def test_winsize(many_lines, many_columns): """Test height and width is appropriately queried in a pty.""" @as_subprocess @@ -136,6 +142,8 @@ def child(lines=25, cols=80): child(lines=many_lines, cols=many_columns) +@pytest.mark.skipif(platform.python_implementation() == 'PyPy', + reason='PyPy fails TIOCSWINSZ') def test_Sequence_alignment(all_terms, many_lines): """Tests methods related to Sequence class, namely ljust, rjust, center.""" @as_subprocess From c594b879391f2f612f67264177e2b0f91c85a5de Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 16 Mar 2014 03:15:55 -0700 Subject: [PATCH 039/459] move __init__.py into terminal.py --- blessed/__init__.py | 851 +------------------------------------------ blessed/terminal.py | 855 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 860 insertions(+), 846 deletions(-) create mode 100644 blessed/terminal.py diff --git a/blessed/__init__.py b/blessed/__init__.py index fc374067..86552120 100644 --- a/blessed/__init__.py +++ b/blessed/__init__.py @@ -1,855 +1,14 @@ """A thin, practical wrapper around curses terminal capabilities.""" -# standard modules -import collections -import contextlib -import platform -import warnings -import termios -import codecs -import curses -import locale -import select -import struct -import fcntl -import time -import tty -import sys -import os - -# local imports -import sequences -import keyboard - -__all__ = ['Terminal'] - -try: - from io import UnsupportedOperation as IOUnsupportedOperation -except ImportError: - class IOUnsupportedOperation(Exception): - """A dummy exception to take the place of Python 3's - ``io.UnsupportedOperation`` in Python 2.5""" - -if ('3', '0', '0') <= platform.python_version_tuple() < ('3', '2', '2+'): +# import as _platform to avoid tab-completion with IPython (thanks @kanzure) +import platform as _platform +if ('3', '0', '0') <= _platform.python_version_tuple() < ('3', '2', '2+'): # Good till 3.2.10 # Python 3.x < 3.2.3 has a bug in which tparm() erroneously takes a string. raise ImportError('Blessings needs Python 3.2.3 or greater for Python 3 ' 'support due to http://bugs.python.org/issue10570.') -class Terminal(object): - """An abstraction around terminal capabilities - - Unlike curses, this doesn't require clearing the screen before doing - anything, and it's friendlier to use. It keeps the endless calls to - ``tigetstr()`` and ``tparm()`` out of your code, and it acts intelligently - when somebody pipes your output to a non-terminal. - - Instance attributes: - - ``stream`` - The stream the terminal outputs to. It's convenient to pass the stream - around with the terminal; it's almost always needed when the terminal - is and saves sticking lots of extra args on client functions in - practice. - """ - def __init__(self, kind=None, stream=None, force_styling=False): - """Initialize the terminal. - - If ``stream`` is not a tty, I will default to returning an empty - Unicode string for all capability values, so things like piping your - output to a file won't strew escape sequences all over the place. The - ``ls`` command sets a precedent for this: it defaults to columnar - output when being sent to a tty and one-item-per-line when not. - - :arg kind: A terminal string as taken by ``setupterm()``. Defaults to - the value of the ``TERM`` environment variable. - :arg stream: A file-like object representing the terminal. Defaults to - the original value of stdout, like ``curses.initscr()`` does. - :arg force_styling: Whether to force the emission of capabilities, even - if we don't seem to be in a terminal. This comes in handy if users - are trying to pipe your output through something like ``less -r``, - which supports terminal codes just fine but doesn't appear itself - to be a terminal. Just expose a command-line option, and set - ``force_styling`` based on it. Terminal initialization sequences - will be sent to ``stream`` if it has a file descriptor and to - ``sys.__stdout__`` otherwise. (``setupterm()`` demands to send them - somewhere, and stdout is probably where the output is ultimately - headed. If not, stderr is probably bound to the same terminal.) - - If you want to force styling to not happen, pass - ``force_styling=None``. - - """ - global _CUR_TERM - if stream is None: - stream = sys.__stdout__ - self.stream_kb = sys.__stdin__.fileno() - - try: - stream_descriptor = (stream.fileno() if hasattr(stream, 'fileno') - and callable(stream.fileno) - else None) - except IOUnsupportedOperation: - stream_descriptor = None - - self._is_a_tty = (stream_descriptor is not None and - os.isatty(stream_descriptor)) - self._does_styling = ((self.is_a_tty or force_styling) and - force_styling is not None) - - # keyboard input only valid when stream is sys.stdout - - # The desciptor to direct terminal initialization sequences to. - # sys.__stdout__ seems to always have a descriptor of 1, even if output - # is redirected. - self._init_descriptor = (sys.__stdout__.fileno() - if stream_descriptor is None - else stream_descriptor) - self._kind = kind or os.environ.get('TERM', 'unknown') - if self.does_styling: - # Make things like tigetstr() work. Explicit args make setupterm() - # work even when -s is passed to nosetests. Lean toward sending - # init sequences to the stream if it has a file descriptor, and - # send them to stdout as a fallback, since they have to go - # somewhere. - try: - curses.setupterm(self._kind, self._init_descriptor) - except curses.error: - warnings.warn('Failed to setupterm(kind=%s)' % (self._kind,)) - self._kind = None - self._does_styling = False - else: - if _CUR_TERM is None or self._kind == _CUR_TERM: - _CUR_TERM = self._kind - else: - warnings.warn( - 'A terminal of kind "%s" has been requested; due to an' - ' internal python curses bug, terminal capabilities' - ' for a terminal of kind "%s" will continue to be' - ' returned for the remainder of this process. see:' - ' https://github.com/erikrose/blessings/issues/33' % ( - self._kind, _CUR_TERM,)) - - if self.does_styling: - sequences.init_sequence_patterns(self) - - # build database of int code <=> KEY_NAME - self._keycodes = keyboard.get_keyboard_codes() - - # store attributes as: self.KEY_NAME = code - for key_code, key_name in self._keycodes.items(): - setattr(self, key_name, key_code) - - # build database of sequence <=> KEY_NAME - self._keymap = keyboard.get_keyboard_sequences(self) - - self._keyboard_buf = collections.deque() - locale.setlocale(locale.LC_ALL, '') - self._encoding = locale.getpreferredencoding() - self._keyboard_decoder = codecs.getincrementaldecoder(self._encoding)() - - self.stream = stream - - # Sugary names for commonly-used capabilities, intended to help avoid trips - # to the terminfo man page and comments in your code: - _sugar = dict( - # Don't use "on" or "bright" as an underscore-separated chunk in any of - # these (e.g. on_cology or rock_on) so we don't interfere with - # __getattr__. - save='sc', - restore='rc', - - clear_eol='el', - clear_bol='el1', - clear_eos='ed', - # 'clear' clears the whole screen. - position='cup', # deprecated - enter_fullscreen='smcup', - exit_fullscreen='rmcup', - move='cup', - move_x='hpa', - move_y='vpa', - move_left='cub1', - move_right='cuf1', - move_up='cuu1', - move_down='cud1', - - hide_cursor='civis', - normal_cursor='cnorm', - - reset_colors='op', # oc doesn't work on my OS X terminal. - - normal='sgr0', - reverse='rev', - # 'bold' is just 'bold'. Similarly... - # blink - # dim - # flash - italic='sitm', - no_italic='ritm', - shadow='sshm', - no_shadow='rshm', - standout='smso', - no_standout='rmso', - subscript='ssubm', - no_subscript='rsubm', - superscript='ssupm', - no_superscript='rsupm', - underline='smul', - no_underline='rmul') - - def __getattr__(self, attr): - """Return a terminal capability, like bold. - - For example, you can say ``term.bold`` to get the string that turns on - bold formatting and ``term.normal`` to get the string that turns it off - again. Or you can take a shortcut: ``term.bold('hi')`` bolds its - argument and sets everything to normal afterward. You can even combine - things: ``term.bold_underline_red_on_bright_green('yowzers!')``. - - For a parametrized capability like ``cup``, pass the parameters too: - ``some_term.cup(line, column)``. - - ``man terminfo`` for a complete list of capabilities. - - Return values are always Unicode. - - """ - resolution = (self._resolve_formatter(attr) if self.does_styling - else NullCallableString()) - setattr(self, attr, resolution) # Cache capability codes. - return resolution - - @property - def does_styling(self): - """Whether attempt to emit capabilities - - This is influenced by the ``is_a_tty`` property and by the - ``force_styling`` argument to the constructor. You can examine - this value to decide whether to draw progress bars or other frippery. - - """ - return self._does_styling - - @property - def is_a_tty(self): - """Whether my ``stream`` appears to be associated with a terminal""" - return self._is_a_tty - - @property - def height(self): - """T.height -> int - - The height of the terminal in characters. - - If an alternative ``stream`` is chosen, the size of that stream - is returned if it is a connected to a terminal such as a pty. - Otherwise, the size of the controlling terminal is returned. - - If neither of these streams are terminals, such as when stdout is piped - to less(1), the values of the environment variable LINES and COLS are - returned. - - None may be returned if no suitable size is discovered. - """ - return self._height_and_width().ws_row - - @property - def width(self): - """T.width -> int - - The width of the terminal in characters. - - None may be returned if no suitable size is discovered. - """ - return self._height_and_width().ws_col - - @staticmethod - def _winsize(fd): - """T._winsize -> WINSZ(ws_row, ws_col, ws_xpixel, ws_ypixel) - - The tty connected by file desriptor fd is queried for its window size, - and returned as a collections.namedtuple instance WINSZ. - - May raise exception IOError. - """ - data = fcntl.ioctl(fd, termios.TIOCGWINSZ, WINSZ._BUF) - return WINSZ(*struct.unpack(WINSZ._FMT, data)) - - def _height_and_width(self): - """Return a tuple of (terminal height, terminal width). - """ - # TODO(jquast): hey kids, even if stdout is redirected to a file, - # we can still query sys.__stdin__.fileno() for our terminal size. - # -- of course, if both are redirected, we have no use for this fd. - for descriptor in self._init_descriptor, sys.__stdout__: - try: - return self._winsize(descriptor) - except IOError: - pass - - return WINSZ(ws_row=(os.environ.get('LINES', None) is not None - and int(os.environ['LINES']) or None), - ws_col=(os.environ.get('COLUMNS', None) is not None - and int(os.environ['COLUMNS']) or None), - ws_xpixel=None, ws_ypixel=None) - - @contextlib.contextmanager - def location(self, x=None, y=None): - """Return a context manager for temporarily moving the cursor. - - Move the cursor to a certain position on entry, let you print stuff - there, then return the cursor to its original position:: - - term = Terminal() - with term.location(2, 5): - print 'Hello, world!' - for x in xrange(10): - print 'I can do it %i times!' % x - - Specify ``x`` to move to a certain column, ``y`` to move to a certain - row, both, or neither. If you specify neither, only the saving and - restoration of cursor position will happen. This can be useful if you - simply want to restore your place after doing some manual cursor - movement. - - """ - # Save position and move to the requested column, row, or both: - self.stream.write(self.save) - if x is not None and y is not None: - self.stream.write(self.move(y, x)) - elif x is not None: - self.stream.write(self.move_x(x)) - elif y is not None: - self.stream.write(self.move_y(y)) - try: - yield - finally: - # Restore original cursor position: - self.stream.write(self.restore) - - @contextlib.contextmanager - def fullscreen(self): - """Return a context manager that enters fullscreen mode while inside it - and restores normal mode on leaving.""" - self.stream.write(self.enter_fullscreen) - try: - yield - finally: - self.stream.write(self.exit_fullscreen) - - @contextlib.contextmanager - def hidden_cursor(self): - """Return a context manager that hides the cursor while inside it and - makes it visible on leaving.""" - self.stream.write(self.hide_cursor) - try: - yield - finally: - self.stream.write(self.normal_cursor) - - @property - def color(self): - """Return a capability that sets the foreground color. - - The capability is unparametrized until called and passed a number - (0-15), at which point it returns another string which represents a - specific color change. This second string can further be called to - color a piece of text and set everything back to normal afterward. - - :arg num: The number, 0-15, of the color - - """ - return (ParametrizingString(self._foreground_color, self.normal) - if self.does_styling else NullCallableString()) - - @property - def on_color(self): - """Return a capability that sets the background color. - - See ``color()``. - - """ - return (ParametrizingString(self._background_color, self.normal) - if self.does_styling else NullCallableString()) - - @property - def number_of_colors(self): - """Return the number of colors the terminal supports. - - Common values are 0, 8, 16, 88, and 256. - - Though the underlying capability returns -1 when there is no color - support, we return 0. This lets you test more Pythonically:: - - if term.number_of_colors: - ... - - We also return 0 if the terminal won't tell us how many colors it - supports, which I think is rare. - - """ - # This is actually the only remotely useful numeric capability. We - # don't name it after the underlying capability, because we deviate - # slightly from its behavior, and we might someday wish to give direct - # access to it. - # Returns -1 if no color support, -2 if no such capability. - colors = self.does_styling and curses.tigetnum('colors') or -1 - # self.__dict__['colors'] = ret # Cache it. It's not changing. - # (Doesn't work.) - return max(0, colors) - - def _resolve_formatter(self, attr): - """Resolve a sugary or plain capability name, color, or compound - formatting function name into a callable capability. - - Return a ``ParametrizingString`` or a ``FormattingString``. - - """ - if attr in COLORS: - return self._resolve_color(attr) - elif attr in COMPOUNDABLES: - # Bold, underline, or something that takes no parameters - return self._formatting_string(self._resolve_capability(attr)) - else: - formatters = split_into_formatters(attr) - if all(f in COMPOUNDABLES for f in formatters): - # It's a compound formatter, like "bold_green_on_red". Future - # optimization: combine all formatting into a single escape - # sequence. - return self._formatting_string( - u''.join(self._resolve_formatter(s) for s in formatters)) - else: - return ParametrizingString(self._resolve_capability(attr)) - - def _resolve_capability(self, atom): - """Return a terminal code for a capname or a sugary name, or an empty - Unicode. - - The return value is always Unicode, because otherwise it is clumsy - (especially in Python 3) to concatenate with real (Unicode) strings. - - """ - code = curses.tigetstr(self._sugar.get(atom, atom)) - if code: - # Decode sequences as latin1, as they are always 8-bit bytes. - return code.decode('latin1') - return u'' - - def _resolve_color(self, color): - """Resolve a color like red or on_bright_green into a callable - capability.""" - # TODO: Does curses automatically exchange red and blue and cyan and - # yellow when a terminal supports setf/setb rather than setaf/setab? - # I'll be blasted if I can find any documentation. The following - # assumes it does. - color_cap = (self._background_color if 'on_' in color else - self._foreground_color) - # curses constants go up to only 7, so add an offset to get at the - # bright colors at 8-15: - offset = 8 if 'bright_' in color else 0 - base_color = color.rsplit('_', 1)[-1] - if self.number_of_colors == 0: - return NullCallableString() - return self._formatting_string( - color_cap(getattr(curses, 'COLOR_' + base_color.upper()) + offset)) - - @property - def _foreground_color(self): - return self.setaf or self.setf +from terminal import Terminal - @property - def _background_color(self): - return self.setab or self.setb - - def _formatting_string(self, formatting): - """Return a new ``FormattingString`` which implicitly receives my - notion of "normal".""" - return FormattingString(formatting, self.normal) - - def ljust(self, text, width=None, fillchar=u' '): - """T.ljust(text, [width], [fillchar]) -> string - - Return string ``text``, left-justified by printable length ``width``. - Padding is done using the specified fill character (default is a - space). Default width is the attached terminal's width. ``text`` is - escape-sequence safe.""" - if width is None: - width = self.width - return sequences.Sequence(text, self).ljust(width, fillchar) - - def rjust(self, text, width=None, fillchar=u' '): - """T.rjust(text, [width], [fillchar]) -> string - - Return string ``text``, right-justified by printable length ``width``. - Padding is done using the specified fill character (default is a space) - Default width is the attached terminal's width. ``text`` is - escape-sequence safe.""" - if width is None: - width = self.width - return sequences.Sequence(text, self).rjust(width, fillchar) - - def center(self, text, width=None, fillchar=u' '): - """T.center(text, [width], [fillchar]) -> string - - Return string ``text``, centered by printable length ``width``. - Padding is done using the specified fill character (default is a - space). Default width is the attached terminal's width. ``text`` is - escape-sequence safe.""" - if width is None: - width = self.width - return sequences.Sequence(text, self).center(width, fillchar) - - def length(self, text): - """T.length(text) -> int - - Return printable length of string ``text``, which may contain (some - kinds) of sequences. Strings containing sequences such as 'clear', - which repositions the cursor will not give accurate results. - """ - return sequences.Sequence(text, self).length() - - def wrap(self, text, width=None, **kwargs): - """T.wrap(text, [width=None, indent=u'', ...]) -> unicode - - Wrap paragraphs containing escape sequences, ``text``, to the full - width of Terminal instance T, unless width is specified, wrapped by - the virtual printable length, irregardless of the video attribute - sequences it may contain. - - Returns a list of strings that may contain escape sequences. See - textwrap.TextWrapper class for available additional kwargs to - customize wrapping behavior. - - Note that the keyword argument ``break_long_words`` may not be set, - it is not sequence-safe. - """ - - _blw = 'break_long_words' - assert (_blw not in kwargs or not kwargs[_blw]), ( - "keyword argument, '{}' is not sequence-safe".format(_blw)) - - width = width is None and self.width or width - lines = [] - for line in text.splitlines(): - lines.extend( - (_linewrap for _linewrap in sequences.SequenceTextWrapper( - width=width, term=self, **kwargs).wrap(text)) - if line.strip() else (u'',)) - - return lines - - def kbhit(self, timeout=0): - """T.kbhit([timeout=0]) -> bool - - Returns True if a keypress has been detected on keyboard. - - When ``timeout`` is 0, this call is non-blocking(default), or blocking - indefinitely until keypress when ``None``, and blocking until keypress - or time elapsed when ``timeout`` is non-zero. - - If input is not a terminal, False is always returned. - """ - if self.keyboard_fd is None: - return False - - check_r, check_w, check_x = [self.stream_kb], [], [] - ready_r, ready_w, ready_x = select.select( - check_r, check_w, check_x, timeout) - - return check_r == ready_r - - @contextlib.contextmanager - def cbreak(self): - """Return a context manager that enters 'cbreak' mode: disabling line - buffering of keyboard input, making characters typed by the user - immediately available to the program. Also referred to as 'rare' - mode, this is the opposite of 'cooked' mode, the default for most - shells. - - In 'cbreak' mode, echo of input is also disabled: the application must - explicitly print any input received, if they so wish. - - More information can be found in the manual page for curses.h, - http://www.openbsd.org/cgi-bin/man.cgi?query=cbreak - - The python manual for curses, - http://docs.python.org/2/library/curses.html - - Note also that setcbreak sets VMIN = 1 and VTIME = 0, - http://www.unixwiz.net/techtips/termios-vmin-vtime.html - """ - assert self.is_a_tty, u'stream is not a a tty.' - if self.stream_kb is not None: - # save current terminal mode, - save_mode = termios.tcgetattr(self.stream_kb) - tty.setcbreak(self.stream_kb, termios.TCSANOW) - try: - yield - finally: - # restore prior mode, - termios.tcsetattr(self.stream_kb, termios.TCSAFLUSH, save_mode) - else: - yield - - def inkey(self, timeout=None, esc_delay=0.35): - """T.inkey(timeout=None, esc_delay=0.35) -> Keypress() - - Receive next keystroke from keyboard (stdin), blocking until a - keypress is received or ``timeout`` elapsed, if specified. - - When used without the context manager ``cbreak``, stdin remains - line-buffered, and this function will block until return is pressed. - - The value returned is an instance of ``Keystroke``, with properties - ``is_sequence``, and, when True, non-None values for ``code`` and - ``name``. The value of ``code`` may be compared against attributes - of this terminal beginning with KEY, such as KEY_ESCAPE. - - To distinguish between KEY_ESCAPE, and sequences beginning with - escape, the ``esc_delay`` specifies the amount of time after receiving - the escape character ('\x1b') to seek for application keys. - - """ - # TODO(jquast): "meta sends escape", where alt+1 would send '\x1b1', - # what do we do with that? Surely, something useful. - # comparator to term.KEY_meta('x') ? - # TODO(jquast): Ctrl characters, KEY_CTRL_[A-Z], and the rest; - # KEY_CTRL_\, KEY_CTRL_{, etc. are not legitimate - # attributes. comparator to term.KEY_ctrl('z') ? - def _timeleft(stime, timeout): - """_timeleft(stime, timeout) -> float - - Returns time-relative time remaining before ``timeout`` after time - elapsed since ``stime``. - """ - if timeout is not None: - if timeout is 0: - return 0 - return max(0, timeout - (time.time() - stime)) - - def _decode_next(): - """Read and decode next byte from stdin.""" - byte = os.read(self.stream_kb, 1) - return self._keyboard_decoder.decode(byte, final=False) - - def _resolve(text): - return keyboard.resolve_sequence(text=text, - mapper=self._keymap, - codes=self._keycodes) - - stime = time.time() - - # re-buffer previously received keystrokes, - ucs = u'' - while self._keyboard_buf: - ucs += self._keyboard_buf.pop() - - # receive all immediately available bytes - while self.kbhit(): - ucs += _decode_next() - - # decode keystroke, if any - ks = _resolve(ucs) - - # so long as the most immediately received or buffered keystroke is - # incomplete, (which may be a multibyte encoding), block until until - # one is received. - while not ks and self.kbhit(_timeleft(stime, timeout)): - ucs += _decode_next() - ks = _resolve(ucs) - - # handle escape key (KEY_ESCAPE) vs. escape sequence (which begins - # with KEY_ESCAPE, \x1b[, \x1bO, or \x1b?), up to esc_delay when - # received. This is not optimal, but causes least delay when - # (currently unhandled, and rare) "meta sends escape" is used, - # or when an unsupported sequence is sent. - - if ks.code is self.KEY_ESCAPE: - esctime = time.time() - while (ks.code is self.KEY_ESCAPE and - # XXX specially handle [?O sequence, - # which may soon become [?O{A,B,C,D}, as there - # is also an [?O in some terminal types - (len(ucs) <= 1 or ucs[1] in u'[?O') and - self.kbhit(_timeleft(esctime, esc_delay))): - ucs += _decode_next() - ks = _resolve(ucs) - - self._keyboard_buf.extendleft(ucs[len(ks):]) - return ks - - -def derivative_colors(colors): - """Return the names of valid color variants, given the base colors.""" - return set([('on_' + c) for c in colors] + - [('bright_' + c) for c in colors] + - [('on_bright_' + c) for c in colors]) - - -COLORS = set(['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', - 'white']) -COLORS.update(derivative_colors(COLORS)) -COMPOUNDABLES = (COLORS | - set(['bold', 'underline', 'reverse', 'blink', 'dim', 'italic', - 'shadow', 'standout', 'subscript', 'superscript'])) - - -class ParametrizingString(unicode): - """A Unicode string which can be called to parametrize it as a terminal - capability""" - - def __new__(cls, formatting, normal=None): - """Instantiate. - - :arg normal: If non-None, indicates that, once parametrized, this can - be used as a ``FormattingString``. The value is used as the - "normal" capability. - - """ - new = unicode.__new__(cls, formatting) - new._normal = normal - return new - - def __call__(self, *args): - try: - # Re-encode the cap, because tparm() takes a bytestring in Python - # 3. However, appear to be a plain Unicode string otherwise so - # concats work. - parametrized = curses.tparm( - self.encode('latin1'), *args).decode('latin1') - return (parametrized if self._normal is None else - FormattingString(parametrized, self._normal)) - except TypeError: - # If the first non-int (i.e. incorrect) arg was a string, suggest - # something intelligent: - if len(args) == 1 and isinstance(args[0], basestring): - raise TypeError( - 'A native or nonexistent capability template received ' - '%r when it was expecting ints. You probably misspelled a ' - 'formatting call like bright_red_on_white(...).' % args) - else: - # Somebody passed a non-string; I don't feel confident - # guessing what they were trying to do. - raise - - -class FormattingString(unicode): - """A Unicode string which can be called upon a piece of text to wrap it in - formatting""" - - def __new__(cls, formatting, normal): - new = unicode.__new__(cls, formatting) - new._normal = normal - return new - - def __call__(self, text): - """Return a new string that is ``text`` formatted with my contents. - - At the beginning of the string, I prepend the formatting that is my - contents. At the end, I append the "normal" sequence to set everything - back to defaults. The return value is always a Unicode. - - """ - return self + text + self._normal - - -class NullCallableString(unicode): - """A dummy callable Unicode to stand in for ``FormattingString`` and - ``ParametrizingString`` - - We use this when there is no tty and thus all capabilities should be blank. - - """ - def __new__(cls): - new = unicode.__new__(cls, u'') - return new - - def __call__(self, *args): - """Return a Unicode or whatever you passed in as the first arg - (hopefully a string of some kind). - - When called with an int as the first arg, return an empty Unicode. An - int is a good hint that I am a ``ParametrizingString``, as there are - only about half a dozen string-returning capabilities on OS X's - terminfo man page which take any param that's not an int, and those are - seldom if ever used on modern terminal emulators. (Most have to do with - programming function keys. Blessings' story for supporting - non-string-returning caps is undeveloped.) And any parametrized - capability in a situation where all capabilities themselves are taken - to be blank are, of course, themselves blank. - - When called with a non-int as the first arg (no no args at all), return - the first arg. I am acting as a ``FormattingString``. - - """ - if len(args) != 1 or isinstance(args[0], int): - # I am acting as a ParametrizingString. - - # tparm can take not only ints but also (at least) strings as its - # second...nth args. But we don't support callably parametrizing - # caps that take non-ints yet, so we can cheap out here. TODO: Go - # through enough of the motions in the capability resolvers to - # determine which of 2 special-purpose classes, - # NullParametrizableString or NullFormattingString, to return, and - # retire this one. - # As a NullCallableString, even when provided with a parameter, - # such as t.color(5), we must also still be callable, fe: - # >>> t.color(5)('shmoo') - # is actually simplified result of NullCallable()(), so - # turtles all the way down: we return another instance. - return NullCallableString() - return args[0] # Should we force even strs in Python 2.x to be - # unicodes? No. How would I know what encoding to use - # to convert it? - -WINSZ = collections.namedtuple('WINSZ', ( - 'ws_row', # /* rows, in characters */ - 'ws_col', # /* columns, in characters */ - 'ws_xpixel', # /* horizontal size, pixels */ - 'ws_ypixel', # /* vertical size, pixels */ -)) -#: format of termios structure -WINSZ._FMT = 'hhhh' -#: buffer of termios structure appropriate for ioctl argument -WINSZ._BUF = '\x00' * struct.calcsize(WINSZ._FMT) - - -def split_into_formatters(compound): - """Split a possibly compound format string into segments. - - >>> split_into_formatters('bold_underline_bright_blue_on_red') - ['bold', 'underline', 'bright_blue', 'on_red'] - - """ - merged_segs = [] - # These occur only as prefixes, so they can always be merged: - mergeable_prefixes = ['on', 'bright', 'on_bright'] - for s in compound.split('_'): - if merged_segs and merged_segs[-1] in mergeable_prefixes: - merged_segs[-1] += '_' + s - else: - merged_segs.append(s) - return merged_segs - -# From libcurses/doc/ncurses-intro.html (ESR, Thomas Dickey, et. al): -# -# "After the call to setupterm(), the global variable cur_term is set to -# point to the current structure of terminal capabilities. By calling -# setupterm() for each terminal, and saving and restoring cur_term, it -# is possible for a program to use two or more terminals at once." -# -# However, if you study Python's ./Modules/_cursesmodule.c, you'll find: -# -# if (!initialised_setupterm && setupterm(termstr,fd,&err) == ERR) { -# -# Python - perhaps wrongly - will not allow a re-initialisation of new -# terminals through setupterm(), so the value of cur_term cannot be changed -# once set: subsequent calls to setupterm() have no effect. -# -# Therefore, the ``kind`` of each Terminal() is, in essence, a singleton. -# This global variable reflects that, and a warning is emitted if somebody -# expects otherwise. - -_CUR_TERM = None +__all__ = ['Terminal'] diff --git a/blessed/terminal.py b/blessed/terminal.py new file mode 100644 index 00000000..fc374067 --- /dev/null +++ b/blessed/terminal.py @@ -0,0 +1,855 @@ +"""A thin, practical wrapper around curses terminal capabilities.""" + +# standard modules +import collections +import contextlib +import platform +import warnings +import termios +import codecs +import curses +import locale +import select +import struct +import fcntl +import time +import tty +import sys +import os + +# local imports +import sequences +import keyboard + +__all__ = ['Terminal'] + +try: + from io import UnsupportedOperation as IOUnsupportedOperation +except ImportError: + class IOUnsupportedOperation(Exception): + """A dummy exception to take the place of Python 3's + ``io.UnsupportedOperation`` in Python 2.5""" + +if ('3', '0', '0') <= platform.python_version_tuple() < ('3', '2', '2+'): + # Good till 3.2.10 + # Python 3.x < 3.2.3 has a bug in which tparm() erroneously takes a string. + raise ImportError('Blessings needs Python 3.2.3 or greater for Python 3 ' + 'support due to http://bugs.python.org/issue10570.') + + +class Terminal(object): + """An abstraction around terminal capabilities + + Unlike curses, this doesn't require clearing the screen before doing + anything, and it's friendlier to use. It keeps the endless calls to + ``tigetstr()`` and ``tparm()`` out of your code, and it acts intelligently + when somebody pipes your output to a non-terminal. + + Instance attributes: + + ``stream`` + The stream the terminal outputs to. It's convenient to pass the stream + around with the terminal; it's almost always needed when the terminal + is and saves sticking lots of extra args on client functions in + practice. + """ + def __init__(self, kind=None, stream=None, force_styling=False): + """Initialize the terminal. + + If ``stream`` is not a tty, I will default to returning an empty + Unicode string for all capability values, so things like piping your + output to a file won't strew escape sequences all over the place. The + ``ls`` command sets a precedent for this: it defaults to columnar + output when being sent to a tty and one-item-per-line when not. + + :arg kind: A terminal string as taken by ``setupterm()``. Defaults to + the value of the ``TERM`` environment variable. + :arg stream: A file-like object representing the terminal. Defaults to + the original value of stdout, like ``curses.initscr()`` does. + :arg force_styling: Whether to force the emission of capabilities, even + if we don't seem to be in a terminal. This comes in handy if users + are trying to pipe your output through something like ``less -r``, + which supports terminal codes just fine but doesn't appear itself + to be a terminal. Just expose a command-line option, and set + ``force_styling`` based on it. Terminal initialization sequences + will be sent to ``stream`` if it has a file descriptor and to + ``sys.__stdout__`` otherwise. (``setupterm()`` demands to send them + somewhere, and stdout is probably where the output is ultimately + headed. If not, stderr is probably bound to the same terminal.) + + If you want to force styling to not happen, pass + ``force_styling=None``. + + """ + global _CUR_TERM + if stream is None: + stream = sys.__stdout__ + self.stream_kb = sys.__stdin__.fileno() + + try: + stream_descriptor = (stream.fileno() if hasattr(stream, 'fileno') + and callable(stream.fileno) + else None) + except IOUnsupportedOperation: + stream_descriptor = None + + self._is_a_tty = (stream_descriptor is not None and + os.isatty(stream_descriptor)) + self._does_styling = ((self.is_a_tty or force_styling) and + force_styling is not None) + + # keyboard input only valid when stream is sys.stdout + + # The desciptor to direct terminal initialization sequences to. + # sys.__stdout__ seems to always have a descriptor of 1, even if output + # is redirected. + self._init_descriptor = (sys.__stdout__.fileno() + if stream_descriptor is None + else stream_descriptor) + self._kind = kind or os.environ.get('TERM', 'unknown') + if self.does_styling: + # Make things like tigetstr() work. Explicit args make setupterm() + # work even when -s is passed to nosetests. Lean toward sending + # init sequences to the stream if it has a file descriptor, and + # send them to stdout as a fallback, since they have to go + # somewhere. + try: + curses.setupterm(self._kind, self._init_descriptor) + except curses.error: + warnings.warn('Failed to setupterm(kind=%s)' % (self._kind,)) + self._kind = None + self._does_styling = False + else: + if _CUR_TERM is None or self._kind == _CUR_TERM: + _CUR_TERM = self._kind + else: + warnings.warn( + 'A terminal of kind "%s" has been requested; due to an' + ' internal python curses bug, terminal capabilities' + ' for a terminal of kind "%s" will continue to be' + ' returned for the remainder of this process. see:' + ' https://github.com/erikrose/blessings/issues/33' % ( + self._kind, _CUR_TERM,)) + + if self.does_styling: + sequences.init_sequence_patterns(self) + + # build database of int code <=> KEY_NAME + self._keycodes = keyboard.get_keyboard_codes() + + # store attributes as: self.KEY_NAME = code + for key_code, key_name in self._keycodes.items(): + setattr(self, key_name, key_code) + + # build database of sequence <=> KEY_NAME + self._keymap = keyboard.get_keyboard_sequences(self) + + self._keyboard_buf = collections.deque() + locale.setlocale(locale.LC_ALL, '') + self._encoding = locale.getpreferredencoding() + self._keyboard_decoder = codecs.getincrementaldecoder(self._encoding)() + + self.stream = stream + + # Sugary names for commonly-used capabilities, intended to help avoid trips + # to the terminfo man page and comments in your code: + _sugar = dict( + # Don't use "on" or "bright" as an underscore-separated chunk in any of + # these (e.g. on_cology or rock_on) so we don't interfere with + # __getattr__. + save='sc', + restore='rc', + + clear_eol='el', + clear_bol='el1', + clear_eos='ed', + # 'clear' clears the whole screen. + position='cup', # deprecated + enter_fullscreen='smcup', + exit_fullscreen='rmcup', + move='cup', + move_x='hpa', + move_y='vpa', + move_left='cub1', + move_right='cuf1', + move_up='cuu1', + move_down='cud1', + + hide_cursor='civis', + normal_cursor='cnorm', + + reset_colors='op', # oc doesn't work on my OS X terminal. + + normal='sgr0', + reverse='rev', + # 'bold' is just 'bold'. Similarly... + # blink + # dim + # flash + italic='sitm', + no_italic='ritm', + shadow='sshm', + no_shadow='rshm', + standout='smso', + no_standout='rmso', + subscript='ssubm', + no_subscript='rsubm', + superscript='ssupm', + no_superscript='rsupm', + underline='smul', + no_underline='rmul') + + def __getattr__(self, attr): + """Return a terminal capability, like bold. + + For example, you can say ``term.bold`` to get the string that turns on + bold formatting and ``term.normal`` to get the string that turns it off + again. Or you can take a shortcut: ``term.bold('hi')`` bolds its + argument and sets everything to normal afterward. You can even combine + things: ``term.bold_underline_red_on_bright_green('yowzers!')``. + + For a parametrized capability like ``cup``, pass the parameters too: + ``some_term.cup(line, column)``. + + ``man terminfo`` for a complete list of capabilities. + + Return values are always Unicode. + + """ + resolution = (self._resolve_formatter(attr) if self.does_styling + else NullCallableString()) + setattr(self, attr, resolution) # Cache capability codes. + return resolution + + @property + def does_styling(self): + """Whether attempt to emit capabilities + + This is influenced by the ``is_a_tty`` property and by the + ``force_styling`` argument to the constructor. You can examine + this value to decide whether to draw progress bars or other frippery. + + """ + return self._does_styling + + @property + def is_a_tty(self): + """Whether my ``stream`` appears to be associated with a terminal""" + return self._is_a_tty + + @property + def height(self): + """T.height -> int + + The height of the terminal in characters. + + If an alternative ``stream`` is chosen, the size of that stream + is returned if it is a connected to a terminal such as a pty. + Otherwise, the size of the controlling terminal is returned. + + If neither of these streams are terminals, such as when stdout is piped + to less(1), the values of the environment variable LINES and COLS are + returned. + + None may be returned if no suitable size is discovered. + """ + return self._height_and_width().ws_row + + @property + def width(self): + """T.width -> int + + The width of the terminal in characters. + + None may be returned if no suitable size is discovered. + """ + return self._height_and_width().ws_col + + @staticmethod + def _winsize(fd): + """T._winsize -> WINSZ(ws_row, ws_col, ws_xpixel, ws_ypixel) + + The tty connected by file desriptor fd is queried for its window size, + and returned as a collections.namedtuple instance WINSZ. + + May raise exception IOError. + """ + data = fcntl.ioctl(fd, termios.TIOCGWINSZ, WINSZ._BUF) + return WINSZ(*struct.unpack(WINSZ._FMT, data)) + + def _height_and_width(self): + """Return a tuple of (terminal height, terminal width). + """ + # TODO(jquast): hey kids, even if stdout is redirected to a file, + # we can still query sys.__stdin__.fileno() for our terminal size. + # -- of course, if both are redirected, we have no use for this fd. + for descriptor in self._init_descriptor, sys.__stdout__: + try: + return self._winsize(descriptor) + except IOError: + pass + + return WINSZ(ws_row=(os.environ.get('LINES', None) is not None + and int(os.environ['LINES']) or None), + ws_col=(os.environ.get('COLUMNS', None) is not None + and int(os.environ['COLUMNS']) or None), + ws_xpixel=None, ws_ypixel=None) + + @contextlib.contextmanager + def location(self, x=None, y=None): + """Return a context manager for temporarily moving the cursor. + + Move the cursor to a certain position on entry, let you print stuff + there, then return the cursor to its original position:: + + term = Terminal() + with term.location(2, 5): + print 'Hello, world!' + for x in xrange(10): + print 'I can do it %i times!' % x + + Specify ``x`` to move to a certain column, ``y`` to move to a certain + row, both, or neither. If you specify neither, only the saving and + restoration of cursor position will happen. This can be useful if you + simply want to restore your place after doing some manual cursor + movement. + + """ + # Save position and move to the requested column, row, or both: + self.stream.write(self.save) + if x is not None and y is not None: + self.stream.write(self.move(y, x)) + elif x is not None: + self.stream.write(self.move_x(x)) + elif y is not None: + self.stream.write(self.move_y(y)) + try: + yield + finally: + # Restore original cursor position: + self.stream.write(self.restore) + + @contextlib.contextmanager + def fullscreen(self): + """Return a context manager that enters fullscreen mode while inside it + and restores normal mode on leaving.""" + self.stream.write(self.enter_fullscreen) + try: + yield + finally: + self.stream.write(self.exit_fullscreen) + + @contextlib.contextmanager + def hidden_cursor(self): + """Return a context manager that hides the cursor while inside it and + makes it visible on leaving.""" + self.stream.write(self.hide_cursor) + try: + yield + finally: + self.stream.write(self.normal_cursor) + + @property + def color(self): + """Return a capability that sets the foreground color. + + The capability is unparametrized until called and passed a number + (0-15), at which point it returns another string which represents a + specific color change. This second string can further be called to + color a piece of text and set everything back to normal afterward. + + :arg num: The number, 0-15, of the color + + """ + return (ParametrizingString(self._foreground_color, self.normal) + if self.does_styling else NullCallableString()) + + @property + def on_color(self): + """Return a capability that sets the background color. + + See ``color()``. + + """ + return (ParametrizingString(self._background_color, self.normal) + if self.does_styling else NullCallableString()) + + @property + def number_of_colors(self): + """Return the number of colors the terminal supports. + + Common values are 0, 8, 16, 88, and 256. + + Though the underlying capability returns -1 when there is no color + support, we return 0. This lets you test more Pythonically:: + + if term.number_of_colors: + ... + + We also return 0 if the terminal won't tell us how many colors it + supports, which I think is rare. + + """ + # This is actually the only remotely useful numeric capability. We + # don't name it after the underlying capability, because we deviate + # slightly from its behavior, and we might someday wish to give direct + # access to it. + # Returns -1 if no color support, -2 if no such capability. + colors = self.does_styling and curses.tigetnum('colors') or -1 + # self.__dict__['colors'] = ret # Cache it. It's not changing. + # (Doesn't work.) + return max(0, colors) + + def _resolve_formatter(self, attr): + """Resolve a sugary or plain capability name, color, or compound + formatting function name into a callable capability. + + Return a ``ParametrizingString`` or a ``FormattingString``. + + """ + if attr in COLORS: + return self._resolve_color(attr) + elif attr in COMPOUNDABLES: + # Bold, underline, or something that takes no parameters + return self._formatting_string(self._resolve_capability(attr)) + else: + formatters = split_into_formatters(attr) + if all(f in COMPOUNDABLES for f in formatters): + # It's a compound formatter, like "bold_green_on_red". Future + # optimization: combine all formatting into a single escape + # sequence. + return self._formatting_string( + u''.join(self._resolve_formatter(s) for s in formatters)) + else: + return ParametrizingString(self._resolve_capability(attr)) + + def _resolve_capability(self, atom): + """Return a terminal code for a capname or a sugary name, or an empty + Unicode. + + The return value is always Unicode, because otherwise it is clumsy + (especially in Python 3) to concatenate with real (Unicode) strings. + + """ + code = curses.tigetstr(self._sugar.get(atom, atom)) + if code: + # Decode sequences as latin1, as they are always 8-bit bytes. + return code.decode('latin1') + return u'' + + def _resolve_color(self, color): + """Resolve a color like red or on_bright_green into a callable + capability.""" + # TODO: Does curses automatically exchange red and blue and cyan and + # yellow when a terminal supports setf/setb rather than setaf/setab? + # I'll be blasted if I can find any documentation. The following + # assumes it does. + color_cap = (self._background_color if 'on_' in color else + self._foreground_color) + # curses constants go up to only 7, so add an offset to get at the + # bright colors at 8-15: + offset = 8 if 'bright_' in color else 0 + base_color = color.rsplit('_', 1)[-1] + if self.number_of_colors == 0: + return NullCallableString() + return self._formatting_string( + color_cap(getattr(curses, 'COLOR_' + base_color.upper()) + offset)) + + @property + def _foreground_color(self): + return self.setaf or self.setf + + @property + def _background_color(self): + return self.setab or self.setb + + def _formatting_string(self, formatting): + """Return a new ``FormattingString`` which implicitly receives my + notion of "normal".""" + return FormattingString(formatting, self.normal) + + def ljust(self, text, width=None, fillchar=u' '): + """T.ljust(text, [width], [fillchar]) -> string + + Return string ``text``, left-justified by printable length ``width``. + Padding is done using the specified fill character (default is a + space). Default width is the attached terminal's width. ``text`` is + escape-sequence safe.""" + if width is None: + width = self.width + return sequences.Sequence(text, self).ljust(width, fillchar) + + def rjust(self, text, width=None, fillchar=u' '): + """T.rjust(text, [width], [fillchar]) -> string + + Return string ``text``, right-justified by printable length ``width``. + Padding is done using the specified fill character (default is a space) + Default width is the attached terminal's width. ``text`` is + escape-sequence safe.""" + if width is None: + width = self.width + return sequences.Sequence(text, self).rjust(width, fillchar) + + def center(self, text, width=None, fillchar=u' '): + """T.center(text, [width], [fillchar]) -> string + + Return string ``text``, centered by printable length ``width``. + Padding is done using the specified fill character (default is a + space). Default width is the attached terminal's width. ``text`` is + escape-sequence safe.""" + if width is None: + width = self.width + return sequences.Sequence(text, self).center(width, fillchar) + + def length(self, text): + """T.length(text) -> int + + Return printable length of string ``text``, which may contain (some + kinds) of sequences. Strings containing sequences such as 'clear', + which repositions the cursor will not give accurate results. + """ + return sequences.Sequence(text, self).length() + + def wrap(self, text, width=None, **kwargs): + """T.wrap(text, [width=None, indent=u'', ...]) -> unicode + + Wrap paragraphs containing escape sequences, ``text``, to the full + width of Terminal instance T, unless width is specified, wrapped by + the virtual printable length, irregardless of the video attribute + sequences it may contain. + + Returns a list of strings that may contain escape sequences. See + textwrap.TextWrapper class for available additional kwargs to + customize wrapping behavior. + + Note that the keyword argument ``break_long_words`` may not be set, + it is not sequence-safe. + """ + + _blw = 'break_long_words' + assert (_blw not in kwargs or not kwargs[_blw]), ( + "keyword argument, '{}' is not sequence-safe".format(_blw)) + + width = width is None and self.width or width + lines = [] + for line in text.splitlines(): + lines.extend( + (_linewrap for _linewrap in sequences.SequenceTextWrapper( + width=width, term=self, **kwargs).wrap(text)) + if line.strip() else (u'',)) + + return lines + + def kbhit(self, timeout=0): + """T.kbhit([timeout=0]) -> bool + + Returns True if a keypress has been detected on keyboard. + + When ``timeout`` is 0, this call is non-blocking(default), or blocking + indefinitely until keypress when ``None``, and blocking until keypress + or time elapsed when ``timeout`` is non-zero. + + If input is not a terminal, False is always returned. + """ + if self.keyboard_fd is None: + return False + + check_r, check_w, check_x = [self.stream_kb], [], [] + ready_r, ready_w, ready_x = select.select( + check_r, check_w, check_x, timeout) + + return check_r == ready_r + + @contextlib.contextmanager + def cbreak(self): + """Return a context manager that enters 'cbreak' mode: disabling line + buffering of keyboard input, making characters typed by the user + immediately available to the program. Also referred to as 'rare' + mode, this is the opposite of 'cooked' mode, the default for most + shells. + + In 'cbreak' mode, echo of input is also disabled: the application must + explicitly print any input received, if they so wish. + + More information can be found in the manual page for curses.h, + http://www.openbsd.org/cgi-bin/man.cgi?query=cbreak + + The python manual for curses, + http://docs.python.org/2/library/curses.html + + Note also that setcbreak sets VMIN = 1 and VTIME = 0, + http://www.unixwiz.net/techtips/termios-vmin-vtime.html + """ + assert self.is_a_tty, u'stream is not a a tty.' + if self.stream_kb is not None: + # save current terminal mode, + save_mode = termios.tcgetattr(self.stream_kb) + tty.setcbreak(self.stream_kb, termios.TCSANOW) + try: + yield + finally: + # restore prior mode, + termios.tcsetattr(self.stream_kb, termios.TCSAFLUSH, save_mode) + else: + yield + + def inkey(self, timeout=None, esc_delay=0.35): + """T.inkey(timeout=None, esc_delay=0.35) -> Keypress() + + Receive next keystroke from keyboard (stdin), blocking until a + keypress is received or ``timeout`` elapsed, if specified. + + When used without the context manager ``cbreak``, stdin remains + line-buffered, and this function will block until return is pressed. + + The value returned is an instance of ``Keystroke``, with properties + ``is_sequence``, and, when True, non-None values for ``code`` and + ``name``. The value of ``code`` may be compared against attributes + of this terminal beginning with KEY, such as KEY_ESCAPE. + + To distinguish between KEY_ESCAPE, and sequences beginning with + escape, the ``esc_delay`` specifies the amount of time after receiving + the escape character ('\x1b') to seek for application keys. + + """ + # TODO(jquast): "meta sends escape", where alt+1 would send '\x1b1', + # what do we do with that? Surely, something useful. + # comparator to term.KEY_meta('x') ? + # TODO(jquast): Ctrl characters, KEY_CTRL_[A-Z], and the rest; + # KEY_CTRL_\, KEY_CTRL_{, etc. are not legitimate + # attributes. comparator to term.KEY_ctrl('z') ? + def _timeleft(stime, timeout): + """_timeleft(stime, timeout) -> float + + Returns time-relative time remaining before ``timeout`` after time + elapsed since ``stime``. + """ + if timeout is not None: + if timeout is 0: + return 0 + return max(0, timeout - (time.time() - stime)) + + def _decode_next(): + """Read and decode next byte from stdin.""" + byte = os.read(self.stream_kb, 1) + return self._keyboard_decoder.decode(byte, final=False) + + def _resolve(text): + return keyboard.resolve_sequence(text=text, + mapper=self._keymap, + codes=self._keycodes) + + stime = time.time() + + # re-buffer previously received keystrokes, + ucs = u'' + while self._keyboard_buf: + ucs += self._keyboard_buf.pop() + + # receive all immediately available bytes + while self.kbhit(): + ucs += _decode_next() + + # decode keystroke, if any + ks = _resolve(ucs) + + # so long as the most immediately received or buffered keystroke is + # incomplete, (which may be a multibyte encoding), block until until + # one is received. + while not ks and self.kbhit(_timeleft(stime, timeout)): + ucs += _decode_next() + ks = _resolve(ucs) + + # handle escape key (KEY_ESCAPE) vs. escape sequence (which begins + # with KEY_ESCAPE, \x1b[, \x1bO, or \x1b?), up to esc_delay when + # received. This is not optimal, but causes least delay when + # (currently unhandled, and rare) "meta sends escape" is used, + # or when an unsupported sequence is sent. + + if ks.code is self.KEY_ESCAPE: + esctime = time.time() + while (ks.code is self.KEY_ESCAPE and + # XXX specially handle [?O sequence, + # which may soon become [?O{A,B,C,D}, as there + # is also an [?O in some terminal types + (len(ucs) <= 1 or ucs[1] in u'[?O') and + self.kbhit(_timeleft(esctime, esc_delay))): + ucs += _decode_next() + ks = _resolve(ucs) + + self._keyboard_buf.extendleft(ucs[len(ks):]) + return ks + + +def derivative_colors(colors): + """Return the names of valid color variants, given the base colors.""" + return set([('on_' + c) for c in colors] + + [('bright_' + c) for c in colors] + + [('on_bright_' + c) for c in colors]) + + +COLORS = set(['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', + 'white']) +COLORS.update(derivative_colors(COLORS)) +COMPOUNDABLES = (COLORS | + set(['bold', 'underline', 'reverse', 'blink', 'dim', 'italic', + 'shadow', 'standout', 'subscript', 'superscript'])) + + +class ParametrizingString(unicode): + """A Unicode string which can be called to parametrize it as a terminal + capability""" + + def __new__(cls, formatting, normal=None): + """Instantiate. + + :arg normal: If non-None, indicates that, once parametrized, this can + be used as a ``FormattingString``. The value is used as the + "normal" capability. + + """ + new = unicode.__new__(cls, formatting) + new._normal = normal + return new + + def __call__(self, *args): + try: + # Re-encode the cap, because tparm() takes a bytestring in Python + # 3. However, appear to be a plain Unicode string otherwise so + # concats work. + parametrized = curses.tparm( + self.encode('latin1'), *args).decode('latin1') + return (parametrized if self._normal is None else + FormattingString(parametrized, self._normal)) + except TypeError: + # If the first non-int (i.e. incorrect) arg was a string, suggest + # something intelligent: + if len(args) == 1 and isinstance(args[0], basestring): + raise TypeError( + 'A native or nonexistent capability template received ' + '%r when it was expecting ints. You probably misspelled a ' + 'formatting call like bright_red_on_white(...).' % args) + else: + # Somebody passed a non-string; I don't feel confident + # guessing what they were trying to do. + raise + + +class FormattingString(unicode): + """A Unicode string which can be called upon a piece of text to wrap it in + formatting""" + + def __new__(cls, formatting, normal): + new = unicode.__new__(cls, formatting) + new._normal = normal + return new + + def __call__(self, text): + """Return a new string that is ``text`` formatted with my contents. + + At the beginning of the string, I prepend the formatting that is my + contents. At the end, I append the "normal" sequence to set everything + back to defaults. The return value is always a Unicode. + + """ + return self + text + self._normal + + +class NullCallableString(unicode): + """A dummy callable Unicode to stand in for ``FormattingString`` and + ``ParametrizingString`` + + We use this when there is no tty and thus all capabilities should be blank. + + """ + def __new__(cls): + new = unicode.__new__(cls, u'') + return new + + def __call__(self, *args): + """Return a Unicode or whatever you passed in as the first arg + (hopefully a string of some kind). + + When called with an int as the first arg, return an empty Unicode. An + int is a good hint that I am a ``ParametrizingString``, as there are + only about half a dozen string-returning capabilities on OS X's + terminfo man page which take any param that's not an int, and those are + seldom if ever used on modern terminal emulators. (Most have to do with + programming function keys. Blessings' story for supporting + non-string-returning caps is undeveloped.) And any parametrized + capability in a situation where all capabilities themselves are taken + to be blank are, of course, themselves blank. + + When called with a non-int as the first arg (no no args at all), return + the first arg. I am acting as a ``FormattingString``. + + """ + if len(args) != 1 or isinstance(args[0], int): + # I am acting as a ParametrizingString. + + # tparm can take not only ints but also (at least) strings as its + # second...nth args. But we don't support callably parametrizing + # caps that take non-ints yet, so we can cheap out here. TODO: Go + # through enough of the motions in the capability resolvers to + # determine which of 2 special-purpose classes, + # NullParametrizableString or NullFormattingString, to return, and + # retire this one. + # As a NullCallableString, even when provided with a parameter, + # such as t.color(5), we must also still be callable, fe: + # >>> t.color(5)('shmoo') + # is actually simplified result of NullCallable()(), so + # turtles all the way down: we return another instance. + return NullCallableString() + return args[0] # Should we force even strs in Python 2.x to be + # unicodes? No. How would I know what encoding to use + # to convert it? + +WINSZ = collections.namedtuple('WINSZ', ( + 'ws_row', # /* rows, in characters */ + 'ws_col', # /* columns, in characters */ + 'ws_xpixel', # /* horizontal size, pixels */ + 'ws_ypixel', # /* vertical size, pixels */ +)) +#: format of termios structure +WINSZ._FMT = 'hhhh' +#: buffer of termios structure appropriate for ioctl argument +WINSZ._BUF = '\x00' * struct.calcsize(WINSZ._FMT) + + +def split_into_formatters(compound): + """Split a possibly compound format string into segments. + + >>> split_into_formatters('bold_underline_bright_blue_on_red') + ['bold', 'underline', 'bright_blue', 'on_red'] + + """ + merged_segs = [] + # These occur only as prefixes, so they can always be merged: + mergeable_prefixes = ['on', 'bright', 'on_bright'] + for s in compound.split('_'): + if merged_segs and merged_segs[-1] in mergeable_prefixes: + merged_segs[-1] += '_' + s + else: + merged_segs.append(s) + return merged_segs + +# From libcurses/doc/ncurses-intro.html (ESR, Thomas Dickey, et. al): +# +# "After the call to setupterm(), the global variable cur_term is set to +# point to the current structure of terminal capabilities. By calling +# setupterm() for each terminal, and saving and restoring cur_term, it +# is possible for a program to use two or more terminals at once." +# +# However, if you study Python's ./Modules/_cursesmodule.c, you'll find: +# +# if (!initialised_setupterm && setupterm(termstr,fd,&err) == ERR) { +# +# Python - perhaps wrongly - will not allow a re-initialisation of new +# terminals through setupterm(), so the value of cur_term cannot be changed +# once set: subsequent calls to setupterm() have no effect. +# +# Therefore, the ``kind`` of each Terminal() is, in essence, a singleton. +# This global variable reflects that, and a warning is emitted if somebody +# expects otherwise. + +_CUR_TERM = None From 68c9f57b792b91af79e8639c3030fa0a368b3f1f Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 16 Mar 2014 03:17:19 -0700 Subject: [PATCH 040/459] move formatters to formatters.py, add @raw, introduce partial() for resolve_keyboard, lots of various comment/docfix cleanup --- blessed/formatters.py | 206 ++++++++++++++++++++++ blessed/terminal.py | 387 ++++++++++-------------------------------- 2 files changed, 299 insertions(+), 294 deletions(-) create mode 100644 blessed/formatters.py diff --git a/blessed/formatters.py b/blessed/formatters.py new file mode 100644 index 00000000..fe4d21d7 --- /dev/null +++ b/blessed/formatters.py @@ -0,0 +1,206 @@ +import collections +import curses +import struct + + +COLORS = set(['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', + 'white']) +COMPOUNDABLES = (COLORS | + set(['bold', 'underline', 'reverse', 'blink', 'dim', 'italic', + 'shadow', 'standout', 'subscript', 'superscript'])) + + +def derivative_colors(colors): + """Return the names of valid color variants, given the base colors.""" + return set([('on_' + c) for c in colors] + + [('bright_' + c) for c in colors] + + [('on_bright_' + c) for c in colors]) + +COLORS.update(derivative_colors(COLORS)) + + +class ParametrizingString(unicode): + """A Unicode string which can be called to parametrize it as a terminal + capability""" + + def __new__(cls, attr, normal=None): + """Instantiate. + + :arg normal: If non-None, indicates that, once parametrized, this can + be used as a ``FormattingString``. The value is used as the + "normal" capability. + + """ + new = unicode.__new__(cls, attr) + new._normal = normal + return new + + def __call__(self, *args): + try: + # Re-encode the cap, because tparm() takes a bytestring in Python + # 3. However, appear to be a plain Unicode string otherwise so + # concats work. + attr = curses.tparm(self.encode('latin1'), *args).decode('latin1') + if self._normal: + return FormattingString(attr=attr, normal=self._normal) + return attr + except TypeError: + # If the first non-int (i.e. incorrect) arg was a string, suggest + # something intelligent: + if len(args) == 1 and isinstance(args[0], basestring): + raise TypeError( + 'A native or nonexistent capability template received ' + '%r when it was expecting ints. You probably misspelled a ' + 'formatting call like bright_red_on_white(...).' % args) + # Somebody passed a non-string; I don't feel confident + # guessing what they were trying to do. + raise + + +class FormattingString(unicode): + """A Unicode string which can be called using ``text``, returning a + new string, ``attr`` + ``text`` + ``normal``:: + >> style = FormattingString(term.bright_blue, term.normal) + >> style('Big Blue') + '\x1b[94mBig Blue\x1b(B\x1b[m' + """ + def __new__(cls, attr, normal): + new = unicode.__new__(cls, attr) + new._normal = normal + return new + + def __call__(self, text): + """Return string ``text``, joined by specified video attribute, + (self), and followed by reset attribute sequence (term.normal). + """ + return u''.join((self, text, self._normal)) + + +class NullCallableString(unicode): + """A dummy callable Unicode to stand in for ``FormattingString`` and + ``ParametrizingString`` for terminals that cannot perform styling. + """ + def __new__(cls): + new = unicode.__new__(cls, u'') + return new + + def __call__(self, *args): + """Return a Unicode or whatever you passed in as the first arg + (hopefully a string of some kind). + + When called with an int as the first arg, return an empty Unicode. An + int is a good hint that I am a ``ParametrizingString``, as there are + only about half a dozen string-returning capabilities listed in + terminfo(5) which accept non-int arguments, they are seldom used. + + When called with a non-int as the first arg (no no args at all), return + the first arg, acting in place of ``FormattingString`` without + any attributes. + """ + if len(args) != 1 or isinstance(args[0], int): + # I am acting as a ParametrizingString. + + # tparm can take not only ints but also (at least) strings as its + # 2nd...nth argument. But we don't support callable parameterizing + # capabilities that take non-ints yet, so we can cheap out here. + # + # TODO(erikrose): Go through enough of the motions in the + # capability resolvers to determine which of 2 special-purpose + # classes, NullParametrizableString or NullFormattingString, + # to return, and retire this one. + # + # As a NullCallableString, even when provided with a parameter, + # such as t.color(5), we must also still be callable, fe: + # >>> t.color(5)('shmoo') + # + # is actually simplified result of NullCallable()(), so + # turtles all the way down: we return another instance. + + return NullCallableString() + return args[0] # Should we force even strs in Python 2.x to be + # unicodes? No. How would I know what encoding to use + # to convert it? + +def split_compound(compound): + """Split a possibly compound format string into segments. + + >>> split_compound('bold_underline_bright_blue_on_red') + ['bold', 'underline', 'bright_blue', 'on_red'] + + """ + merged_segs = [] + # These occur only as prefixes, so they can always be merged: + mergeable_prefixes = ['on', 'bright', 'on_bright'] + for s in compound.split('_'): + if merged_segs and merged_segs[-1] in mergeable_prefixes: + merged_segs[-1] += '_' + s + else: + merged_segs.append(s) + return merged_segs + + +def resolve_capability(term, attr): + """Return a Unicode string containing terminal sequence for + capability (or term_sugar alias) ``attr`` of Terminal instance + ``term`` by querying curses.tigetstr. + + If the terminal does not have any value for the capability, an empty + Unicode string is returned. + """ + code = curses.tigetstr(term._sugar.get(attr, attr)) + if code: + # Decode sequences as latin1, as they are always 8-bit bytes. + return code.decode('latin1') + return u'' + + +def resolve_attribute(term, attr): + """Resolve a sugary or plain capability name, color, or compound + formatting function name into a *callable* unicode string + capability, ``ParametrizingString`` or ``FormattingString``. + """ + if attr in COLORS: + return resolve_color(term, attr) + + # Bold, underline, or something that takes no parameters + if attr in COMPOUNDABLES: + fmt_attr = resolve_capability(term, attr) + if fmt_attr: + return FormattingString(fmt_attr, term.normal) + else: + return NullCallableString() + + # A compound formatter, like "bold_green_on_red", recurse + # into self, joining all returned compound attribute values. + if all(fmt in COMPOUNDABLES for fmt in split_compound(attr)): + fmt_attr = u''.join(resolve_attribute(term, ucs) # RECURSIVE + for ucs in split_compound(attr)) + if fmt_attr: + return FormattingString(fmt_attr, term.normal) + else: + return NullCallableString() + + return ParametrizingString(resolve_capability(term, attr)) + + +def resolve_color(term, color): + """Resolve a color, to callable capability, valid ``color`` capabilities + are format ``red``, or ``on_right_green``. + """ + # NOTE(erikrose): Does curses automatically exchange red and blue and cyan + # and yellow when a terminal supports setf/setb rather than setaf/setab? + # I'll be blasted if I can find any documentation. The following + # assumes it does. + color_cap = (term._background_color if 'on_' in color else + term._foreground_color) + + # curses constants go up to only 7, so add an offset to get at the + # bright colors at 8-15: + offset = 8 if 'bright_' in color else 0 + base_color = color.rsplit('_', 1)[-1] + if term.number_of_colors == 0: + return NullCallableString() + + attr = 'COLOR_%s' % (base_color.upper(),) + fmt_attr = color_cap(getattr(curses, attr) + offset) + return FormattingString(fmt_attr, term.normal) diff --git a/blessed/terminal.py b/blessed/terminal.py index fc374067..0307f647 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -1,9 +1,7 @@ -"""A thin, practical wrapper around curses terminal capabilities.""" - # standard modules import collections import contextlib -import platform +import functools import warnings import termios import codecs @@ -17,12 +15,6 @@ import sys import os -# local imports -import sequences -import keyboard - -__all__ = ['Terminal'] - try: from io import UnsupportedOperation as IOUnsupportedOperation except ImportError: @@ -30,11 +22,10 @@ class IOUnsupportedOperation(Exception): """A dummy exception to take the place of Python 3's ``io.UnsupportedOperation`` in Python 2.5""" -if ('3', '0', '0') <= platform.python_version_tuple() < ('3', '2', '2+'): - # Good till 3.2.10 - # Python 3.x < 3.2.3 has a bug in which tparm() erroneously takes a string. - raise ImportError('Blessings needs Python 3.2.3 or greater for Python 3 ' - 'support due to http://bugs.python.org/issue10570.') +# local imports +import formatters +import sequences +import keyboard class Terminal(object): @@ -85,16 +76,16 @@ def __init__(self, kind=None, stream=None, force_styling=False): if stream is None: stream = sys.__stdout__ self.stream_kb = sys.__stdin__.fileno() + else: + self.stream_kb = None try: - stream_descriptor = (stream.fileno() if hasattr(stream, 'fileno') - and callable(stream.fileno) - else None) + stream_fd = (stream.fileno() if hasattr(stream, 'fileno') + and callable(stream.fileno) else None) except IOUnsupportedOperation: - stream_descriptor = None + stream_fd = None - self._is_a_tty = (stream_descriptor is not None and - os.isatty(stream_descriptor)) + self._is_a_tty = stream_fd is not None and os.isatty(stream_fd) self._does_styling = ((self.is_a_tty or force_styling) and force_styling is not None) @@ -103,9 +94,8 @@ def __init__(self, kind=None, stream=None, force_styling=False): # The desciptor to direct terminal initialization sequences to. # sys.__stdout__ seems to always have a descriptor of 1, even if output # is redirected. - self._init_descriptor = (sys.__stdout__.fileno() - if stream_descriptor is None - else stream_descriptor) + self._init_descriptor = (stream_fd is None and sys.__stdout__.fileno() + or stream_fd) self._kind = kind or os.environ.get('TERM', 'unknown') if self.does_styling: # Make things like tigetstr() work. Explicit args make setupterm() @@ -151,19 +141,14 @@ def __init__(self, kind=None, stream=None, force_styling=False): self.stream = stream - # Sugary names for commonly-used capabilities, intended to help avoid trips - # to the terminfo man page and comments in your code: + #: Sugary names for commonly-used capabilities _sugar = dict( - # Don't use "on" or "bright" as an underscore-separated chunk in any of - # these (e.g. on_cology or rock_on) so we don't interfere with - # __getattr__. save='sc', restore='rc', - + # 'clear' clears the whole screen. clear_eol='el', clear_bol='el1', clear_eos='ed', - # 'clear' clears the whole screen. position='cup', # deprecated enter_fullscreen='smcup', exit_fullscreen='rmcup', @@ -174,18 +159,11 @@ def __init__(self, kind=None, stream=None, force_styling=False): move_right='cuf1', move_up='cuu1', move_down='cud1', - hide_cursor='civis', normal_cursor='cnorm', - reset_colors='op', # oc doesn't work on my OS X terminal. - normal='sgr0', reverse='rev', - # 'bold' is just 'bold'. Similarly... - # blink - # dim - # flash italic='sitm', no_italic='ritm', shadow='sshm', @@ -200,26 +178,30 @@ def __init__(self, kind=None, stream=None, force_styling=False): no_underline='rmul') def __getattr__(self, attr): - """Return a terminal capability, like bold. + """Return a terminal capability as Unicode string. - For example, you can say ``term.bold`` to get the string that turns on - bold formatting and ``term.normal`` to get the string that turns it off - again. Or you can take a shortcut: ``term.bold('hi')`` bolds its - argument and sets everything to normal afterward. You can even combine - things: ``term.bold_underline_red_on_bright_green('yowzers!')``. + For example, ``term.bold`` is a unicode string that may be prepended + to text to set the video attribute for bold, which subsequently may + also be terminated with the pairing ``term.normal``. - For a parametrized capability like ``cup``, pass the parameters too: - ``some_term.cup(line, column)``. + This capability is also callable, so you can use ``term.bold("hi")`` + which results in the joining of (term.bold, "hi", term.normal). - ``man terminfo`` for a complete list of capabilities. - - Return values are always Unicode. + Compound formatters may also be used, for example: + ``term.bold_blink_red_on_green("merry x-mas!")``. + For a parametrized capability such as ``cup`` (cursor_address), pass + the parameters as arguments ``some_term.cup(line, column)``. See + manual page terminfo(5) for a complete list of capabilities. """ - resolution = (self._resolve_formatter(attr) if self.does_styling - else NullCallableString()) - setattr(self, attr, resolution) # Cache capability codes. - return resolution + if not self.does_styling: + return formatters.NullCallableString() + + val = formatters.resolve_attribute(self, attr) + + # Cache capability codes. + setattr(self, attr, val) + return val @property def does_styling(self): @@ -283,17 +265,17 @@ def _height_and_width(self): # TODO(jquast): hey kids, even if stdout is redirected to a file, # we can still query sys.__stdin__.fileno() for our terminal size. # -- of course, if both are redirected, we have no use for this fd. - for descriptor in self._init_descriptor, sys.__stdout__: + for fd in (self._init_descriptor, sys.__stdout__): try: - return self._winsize(descriptor) + if fd is not None: + return self._winsize(fd) except IOError: pass - return WINSZ(ws_row=(os.environ.get('LINES', None) is not None - and int(os.environ['LINES']) or None), - ws_col=(os.environ.get('COLUMNS', None) is not None - and int(os.environ['COLUMNS']) or None), - ws_xpixel=None, ws_ypixel=None) + return WINSZ(ws_row=int(os.getenv('LINES', '25')), + ws_col=int(os.getenv('COLUMNS', '80')), + ws_xpixel=None, + ws_ypixel=None) @contextlib.contextmanager def location(self, x=None, y=None): @@ -361,8 +343,10 @@ def color(self): :arg num: The number, 0-15, of the color """ - return (ParametrizingString(self._foreground_color, self.normal) - if self.does_styling else NullCallableString()) + if not self.does_styling: + return formatters.NullCallableString() + return formatters.ParametrizingString( + self._foreground_color, self.normal) @property def on_color(self): @@ -371,8 +355,10 @@ def on_color(self): See ``color()``. """ - return (ParametrizingString(self._background_color, self.normal) - if self.does_styling else NullCallableString()) + if not self.does_styling: + return formatters.NullCallableString() + return formatters.ParametrizingString( + self._background_color, self.normal) @property def number_of_colors(self): @@ -394,66 +380,10 @@ def number_of_colors(self): # don't name it after the underlying capability, because we deviate # slightly from its behavior, and we might someday wish to give direct # access to it. - # Returns -1 if no color support, -2 if no such capability. - colors = self.does_styling and curses.tigetnum('colors') or -1 - # self.__dict__['colors'] = ret # Cache it. It's not changing. - # (Doesn't work.) - return max(0, colors) - - def _resolve_formatter(self, attr): - """Resolve a sugary or plain capability name, color, or compound - formatting function name into a callable capability. - - Return a ``ParametrizingString`` or a ``FormattingString``. - - """ - if attr in COLORS: - return self._resolve_color(attr) - elif attr in COMPOUNDABLES: - # Bold, underline, or something that takes no parameters - return self._formatting_string(self._resolve_capability(attr)) - else: - formatters = split_into_formatters(attr) - if all(f in COMPOUNDABLES for f in formatters): - # It's a compound formatter, like "bold_green_on_red". Future - # optimization: combine all formatting into a single escape - # sequence. - return self._formatting_string( - u''.join(self._resolve_formatter(s) for s in formatters)) - else: - return ParametrizingString(self._resolve_capability(attr)) - - def _resolve_capability(self, atom): - """Return a terminal code for a capname or a sugary name, or an empty - Unicode. - - The return value is always Unicode, because otherwise it is clumsy - (especially in Python 3) to concatenate with real (Unicode) strings. - - """ - code = curses.tigetstr(self._sugar.get(atom, atom)) - if code: - # Decode sequences as latin1, as they are always 8-bit bytes. - return code.decode('latin1') - return u'' - - def _resolve_color(self, color): - """Resolve a color like red or on_bright_green into a callable - capability.""" - # TODO: Does curses automatically exchange red and blue and cyan and - # yellow when a terminal supports setf/setb rather than setaf/setab? - # I'll be blasted if I can find any documentation. The following - # assumes it does. - color_cap = (self._background_color if 'on_' in color else - self._foreground_color) - # curses constants go up to only 7, so add an offset to get at the - # bright colors at 8-15: - offset = 8 if 'bright_' in color else 0 - base_color = color.rsplit('_', 1)[-1] - if self.number_of_colors == 0: - return NullCallableString() - return self._formatting_string( - color_cap(getattr(curses, 'COLOR_' + base_color.upper()) + offset)) + # + # trim value to 0, as tigetnum('colors') returns -1 if no support, + # -2 if no such capability. + return max(0, self.does_styling and curses.tigetnum('colors') or -1) @property def _foreground_color(self): @@ -463,11 +393,6 @@ def _foreground_color(self): def _background_color(self): return self.setab or self.setb - def _formatting_string(self, formatting): - """Return a new ``FormattingString`` which implicitly receives my - notion of "normal".""" - return FormattingString(formatting, self.normal) - def ljust(self, text, width=None, fillchar=u' '): """T.ljust(text, [width], [fillchar]) -> string @@ -580,7 +505,7 @@ def cbreak(self): Note also that setcbreak sets VMIN = 1 and VTIME = 0, http://www.unixwiz.net/techtips/termios-vmin-vtime.html """ - assert self.is_a_tty, u'stream is not a a tty.' + assert self.is_a_tty, 'stream is not a tty.' if self.stream_kb is not None: # save current terminal mode, save_mode = termios.tcgetattr(self.stream_kb) @@ -593,6 +518,27 @@ def cbreak(self): else: yield + @contextlib.contextmanager + def raw(self): + """Return a context manager that enters 'raw' mode. Raw mode is + similar to cbreak mode, in that characters typed are immediately passed + through to the user program. The differences are that in raw mode, the + interrupt, quit, suspend, and flow control characters are all passed + through uninterpreted, instead of generating a signal. + """ + assert self.is_a_tty, 'stream is not a tty.' + if self.stream_kb is not None: + # save current terminal mode, + save_mode = termios.tcgetattr(self.stream_kb) + tty.setraw(self.stream_kb, termios.TCSANOW) + try: + yield + finally: + # restore prior mode, + termios.tcsetattr(self.stream_kb, termios.TCSAFLUSH, save_mode) + else: + yield + def inkey(self, timeout=None, esc_delay=0.35): """T.inkey(timeout=None, esc_delay=0.35) -> Keypress() @@ -634,10 +580,9 @@ def _decode_next(): byte = os.read(self.stream_kb, 1) return self._keyboard_decoder.decode(byte, final=False) - def _resolve(text): - return keyboard.resolve_sequence(text=text, - mapper=self._keymap, - codes=self._keycodes) + resolve = functools.partial(keyboard.resolve_sequence, + mapper=self._keymap, + codes=self._keycodes) stime = time.time() @@ -651,188 +596,31 @@ def _resolve(text): ucs += _decode_next() # decode keystroke, if any - ks = _resolve(ucs) + ks = resolve(text=ucs) # so long as the most immediately received or buffered keystroke is # incomplete, (which may be a multibyte encoding), block until until # one is received. while not ks and self.kbhit(_timeleft(stime, timeout)): ucs += _decode_next() - ks = _resolve(ucs) + ks = resolve(text=ucs) # handle escape key (KEY_ESCAPE) vs. escape sequence (which begins # with KEY_ESCAPE, \x1b[, \x1bO, or \x1b?), up to esc_delay when # received. This is not optimal, but causes least delay when # (currently unhandled, and rare) "meta sends escape" is used, # or when an unsupported sequence is sent. - if ks.code is self.KEY_ESCAPE: esctime = time.time() while (ks.code is self.KEY_ESCAPE and - # XXX specially handle [?O sequence, - # which may soon become [?O{A,B,C,D}, as there - # is also an [?O in some terminal types - (len(ucs) <= 1 or ucs[1] in u'[?O') and self.kbhit(_timeleft(esctime, esc_delay))): ucs += _decode_next() - ks = _resolve(ucs) + ks = resolve(text=ucs) + # buffer any remaining text received self._keyboard_buf.extendleft(ucs[len(ks):]) return ks - -def derivative_colors(colors): - """Return the names of valid color variants, given the base colors.""" - return set([('on_' + c) for c in colors] + - [('bright_' + c) for c in colors] + - [('on_bright_' + c) for c in colors]) - - -COLORS = set(['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', - 'white']) -COLORS.update(derivative_colors(COLORS)) -COMPOUNDABLES = (COLORS | - set(['bold', 'underline', 'reverse', 'blink', 'dim', 'italic', - 'shadow', 'standout', 'subscript', 'superscript'])) - - -class ParametrizingString(unicode): - """A Unicode string which can be called to parametrize it as a terminal - capability""" - - def __new__(cls, formatting, normal=None): - """Instantiate. - - :arg normal: If non-None, indicates that, once parametrized, this can - be used as a ``FormattingString``. The value is used as the - "normal" capability. - - """ - new = unicode.__new__(cls, formatting) - new._normal = normal - return new - - def __call__(self, *args): - try: - # Re-encode the cap, because tparm() takes a bytestring in Python - # 3. However, appear to be a plain Unicode string otherwise so - # concats work. - parametrized = curses.tparm( - self.encode('latin1'), *args).decode('latin1') - return (parametrized if self._normal is None else - FormattingString(parametrized, self._normal)) - except TypeError: - # If the first non-int (i.e. incorrect) arg was a string, suggest - # something intelligent: - if len(args) == 1 and isinstance(args[0], basestring): - raise TypeError( - 'A native or nonexistent capability template received ' - '%r when it was expecting ints. You probably misspelled a ' - 'formatting call like bright_red_on_white(...).' % args) - else: - # Somebody passed a non-string; I don't feel confident - # guessing what they were trying to do. - raise - - -class FormattingString(unicode): - """A Unicode string which can be called upon a piece of text to wrap it in - formatting""" - - def __new__(cls, formatting, normal): - new = unicode.__new__(cls, formatting) - new._normal = normal - return new - - def __call__(self, text): - """Return a new string that is ``text`` formatted with my contents. - - At the beginning of the string, I prepend the formatting that is my - contents. At the end, I append the "normal" sequence to set everything - back to defaults. The return value is always a Unicode. - - """ - return self + text + self._normal - - -class NullCallableString(unicode): - """A dummy callable Unicode to stand in for ``FormattingString`` and - ``ParametrizingString`` - - We use this when there is no tty and thus all capabilities should be blank. - - """ - def __new__(cls): - new = unicode.__new__(cls, u'') - return new - - def __call__(self, *args): - """Return a Unicode or whatever you passed in as the first arg - (hopefully a string of some kind). - - When called with an int as the first arg, return an empty Unicode. An - int is a good hint that I am a ``ParametrizingString``, as there are - only about half a dozen string-returning capabilities on OS X's - terminfo man page which take any param that's not an int, and those are - seldom if ever used on modern terminal emulators. (Most have to do with - programming function keys. Blessings' story for supporting - non-string-returning caps is undeveloped.) And any parametrized - capability in a situation where all capabilities themselves are taken - to be blank are, of course, themselves blank. - - When called with a non-int as the first arg (no no args at all), return - the first arg. I am acting as a ``FormattingString``. - - """ - if len(args) != 1 or isinstance(args[0], int): - # I am acting as a ParametrizingString. - - # tparm can take not only ints but also (at least) strings as its - # second...nth args. But we don't support callably parametrizing - # caps that take non-ints yet, so we can cheap out here. TODO: Go - # through enough of the motions in the capability resolvers to - # determine which of 2 special-purpose classes, - # NullParametrizableString or NullFormattingString, to return, and - # retire this one. - # As a NullCallableString, even when provided with a parameter, - # such as t.color(5), we must also still be callable, fe: - # >>> t.color(5)('shmoo') - # is actually simplified result of NullCallable()(), so - # turtles all the way down: we return another instance. - return NullCallableString() - return args[0] # Should we force even strs in Python 2.x to be - # unicodes? No. How would I know what encoding to use - # to convert it? - -WINSZ = collections.namedtuple('WINSZ', ( - 'ws_row', # /* rows, in characters */ - 'ws_col', # /* columns, in characters */ - 'ws_xpixel', # /* horizontal size, pixels */ - 'ws_ypixel', # /* vertical size, pixels */ -)) -#: format of termios structure -WINSZ._FMT = 'hhhh' -#: buffer of termios structure appropriate for ioctl argument -WINSZ._BUF = '\x00' * struct.calcsize(WINSZ._FMT) - - -def split_into_formatters(compound): - """Split a possibly compound format string into segments. - - >>> split_into_formatters('bold_underline_bright_blue_on_red') - ['bold', 'underline', 'bright_blue', 'on_red'] - - """ - merged_segs = [] - # These occur only as prefixes, so they can always be merged: - mergeable_prefixes = ['on', 'bright', 'on_bright'] - for s in compound.split('_'): - if merged_segs and merged_segs[-1] in mergeable_prefixes: - merged_segs[-1] += '_' + s - else: - merged_segs.append(s) - return merged_segs - # From libcurses/doc/ncurses-intro.html (ESR, Thomas Dickey, et. al): # # "After the call to setupterm(), the global variable cur_term is set to @@ -853,3 +641,14 @@ def split_into_formatters(compound): # expects otherwise. _CUR_TERM = None + +WINSZ = collections.namedtuple('WINSZ', ( + 'ws_row', # /* rows, in characters */ + 'ws_col', # /* columns, in characters */ + 'ws_xpixel', # /* horizontal size, pixels */ + 'ws_ypixel', # /* vertical size, pixels */ +)) +#: format of termios structure +WINSZ._FMT = 'hhhh' +#: buffer of termios structure appropriate for ioctl argument +WINSZ._BUF = '\x00' * struct.calcsize(WINSZ._FMT) From d41fc77f338915246fe8f6762cbb19c7fd960e99 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 16 Mar 2014 03:18:13 -0700 Subject: [PATCH 041/459] document @raw and keycodes reorder change log in order of features->bugs --- README.rst | 87 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 64 insertions(+), 23 deletions(-) diff --git a/README.rst b/README.rst index 46180775..5b9ae8d7 100644 --- a/README.rst +++ b/README.rst @@ -437,6 +437,14 @@ Any keypress by the user is immediately value:: # blocks until any key is pressed. sys.stdin.read(1) +raw +~~~ + +The context manager ``raw`` is the same as ``cbreak``, except interrupt (^C), +quit (^\), suspend (^Z), and flow control (^S, ^Q) characters are not trapped +by signal handlers, but instead sent directly. This is necessary if you +actually want to handle the receipt of Ctrl+C + inkey ~~~~~ @@ -482,6 +490,33 @@ Its output might appear as:: .. _`termios(4)`: www.openbsd.org/cgi-bin/man.cgi?query=termios&apropos=0&sektion=4 +codes +~~~~~ + +The return value of ``inkey`` can be inspected for property ``is_sequence``. +When ``True``, the ``code`` property (int) may be compared with any of the +following attributes available on the associated Terminal, which are equivalent +to the same available in curs_getch(3X), with the following exceptions + * use ``KEY_DELETE`` instead of ``KEY_DC`` (chr(127)) + * use ``KEY_INSERT`` instead of ``KEY_IC`` + * use ``KEY_PGUP`` instead of ``KEY_PPAGE`` + * use ``KEY_PGDOWN`` instead of ``KEY_NPAGE`` + * use ``KEY_ESCAPE`` instead of ``KEY_EXIT`` + * use ``KEY_SUP`` instead of ``KEY_SR`` (shift + up) + * use ``KEY_SDOWN`` instead of ``KEY_SF`` (shift + down) + +Additionally, use any of the following common attributes: + + * ``KEY_BACKSPACE`` (chr(8)). + * ``KEY_TAB`` (chr(9)). + * ``KEY_DOWN``, ``KEY_UP``, ``KEY_LEFT``, ``KEY_RIGHT``. + * ``KEY_SLEFT`` (shift + left). + * ``KEY_SRIGHT`` (shift + right). + * ``KEY_HOME``, ``KEY_END``. + * ``KEY_F1`` through ``KEY_F22``. + +And much more. All attributes begin with prefix ``KEY_``. + Shopping List ============= @@ -528,36 +563,40 @@ Version History =============== 1.7 - * introduced context manager ``cbreak`` which is equivalent to ``tty.cbreak``, + * Forked github project `erikrose/blessings`_ to `jquast/blessed`_, this + project was previously known as **blessings** version 1.6 and prior. + * introduced context manager ``cbreak``, which is equivalent to ``tty.cbreak``, placing the terminal in 'cooked' mode, allowing input from stdin to be read as each key is pressed (line-buffering disabled). - - * Forked github project 'erikrose/blessings' to 'jquast/blessed', this - project was previously known as 'blessings' version 1.6 and prior. - * Created ``python setup.py develop`` for developer environment. - * Converted nosetests to pytest, use ``python setup.py test``. - * introduced ``@as_subprocess`` to discover and resolve various issues. - * cannot call ``setupterm()`` more than once per process -- issue a - warning about what terminal kind subsequent calls will use. - * resolved issue ``number_of_colors`` fails when ``does_styling`` is - ``False``. resolves piping tests output to stdout. - * removed pokemon ``curses.error`` exceptions. - * warn and set ``does_styling`` set ``False`` when TERM is unset or unknown. - * allow unsupported terminal capabilities to be callable just as supported - capabilities, so that the return value of ``term.color(n)`` may be called - on terminals without color capabilities. - * attributes that should be read-only have now raise exception when - re-assigned (properties). - * introduced ``term.center()``, ``term.rjust()``, and ``term.ljust()``, - allows text containing sequences to be aligned to screen or argument - ``width``. - * introduced ``term.wrap()``, allows text containing sequences to be + * introduced ``center()``, ``rjust()``, and ``ljust()`` methods, allows text + containing sequences to be aligned to screen, or ``width`` specified. + * introduced ``wrap()``, allows text containing sequences to be word-wrapped without breaking mid-sequence and honoring their printable width. - * introduced method ``inkey()``, which will return 1 or more characters as + * introduced ``inkey()``, which will return 1 or more characters as a unicode sequence, with attributes ``.code`` and ``.name`` non-None when a multibyte sequence is received, allowing arrow keys and such to be detected. Optional value ``timeout`` allows timed polling or blocking. + * bugfix: cannot call ``setupterm()`` more than once per process -- issue a + warning about what terminal kind subsequent calls will use. + * bugfix: resolved issue where ``number_of_colors`` fails when ``does_styling`` + is ``False``. resolves issue where piping tests output to stdout would fail. + * bugfix: warn and set ``does_styling`` to ``False`` when TERM is unknown. + * bugfix: allow unsupported terminal capabilities to be callable just as + supported capabilities, so that the return value of ``term.color(n)`` may + be called on terminals without color capabilities. + * bugfix: for terminals without underline, such as vt220, + ``term.underline('xyz')`` would be ``(u'xyz' + term.normal)``, now it is only + ``u'xyz'``. + * attributes that should be read-only have now raise exception when + re-assigned (properties). + * enhancement: pypy is not a supported platform implementation. + * enhancement: removed pokemon ``curses.error`` exceptions. + * enhancement: converted nosetests to pytest, install and use ``tox`` for testing. + * enhancement: pytext fixtures, paired with a new ``@as_subprocess`` decorator + are used to test a multitude of terminal types. + * introduced ``@as_subprocess`` to discover and resolve various issues. + * deprecation: python2.5 is no longer supported (as tox does not supported). 1.6 * Add ``does_styling`` property. This takes ``force_styling`` into account @@ -636,3 +675,5 @@ Version History tootin' functionality. .. _`progress-bar-having, traceback-shortcutting, rootin', tootin' testrunner`: http://pypi.python.org/pypi/nose-progressive/ +.. _`erikrose/blessings`: https://github.com/erikrose/blessings +.. _`jquast/blessed`: https://github.com/jquast/blessed From ba933eab7bfa9f778e7e875737a09e43986d26ce Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 16 Mar 2014 03:18:23 -0700 Subject: [PATCH 042/459] switch test_keyboard.py for @raw --- bin/test_keyboard.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/bin/test_keyboard.py b/bin/test_keyboard.py index 765bef2a..113827d3 100755 --- a/bin/test_keyboard.py +++ b/bin/test_keyboard.py @@ -2,9 +2,6 @@ from blessed import Terminal import sys -# _keymap -# _keycodes - def main(): """ Displays all known key capabilities that may match the terminal. @@ -74,7 +71,7 @@ def add_score(score, pts, level): gb = build_gameboard(term) inps = [] - with term.cbreak(): + with term.raw(): inp = term.inkey(timeout=0) while inp.upper() != 'Q': if dirty: @@ -97,6 +94,7 @@ def add_score(score, pts, level): score, level = add_score(score, 100, level) inps.append(inp) + with term.cbreak(): sys.stdout.write(u''.join(( term.move(term.height), term.clear_eol, From 938f40c2711210ff20fcbb9321c1b470526f8ab7 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 16 Mar 2014 03:18:50 -0700 Subject: [PATCH 043/459] sugar rename KEY_SR/SF to KEY_SUP/SDOWN --- blessed/keyboard.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/blessed/keyboard.py b/blessed/keyboard.py index 8368fc74..d6cfb43c 100644 --- a/blessed/keyboard.py +++ b/blessed/keyboard.py @@ -81,6 +81,8 @@ def get_keyboard_codes(): * KEY_PGUP in place of KEY_PPAGE * KEY_PGDOWN in place of KEY_NPAGE * KEY_ESCAPE in place of KEY_EXIT + * KEY_SUP in place of KEY_SR + * KEY_SDOWN in place of KEY_SF """ keycodes = OrderedDict(get_curses_keycodes()) keycodes.update(CURSES_KEYCODE_OVERRIDE_MIXIN) @@ -170,6 +172,8 @@ def resolve_sequence(text, mapper, codes): ('KEY_PGUP', curses.KEY_PPAGE), ('KEY_PGDOWN', curses.KEY_NPAGE), ('KEY_ESCAPE', curses.KEY_EXIT), + ('KEY_SUP', curses.KEY_SR), + ('KEY_SDOWN', curses.KEY_SF), ) # In a perfect world, terminal emulators would always send exactly what the From 34e443d6f16c496fc965183170abb9dddd57f7c7 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 16 Mar 2014 03:19:02 -0700 Subject: [PATCH 044/459] remove python2.5 compatibility (tox) --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index b358f067..2f50dc54 100755 --- a/setup.py +++ b/setup.py @@ -87,7 +87,6 @@ 'License :: OSI Approved :: MIT License', 'Operating System :: POSIX', 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.5', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', From 0a72908161cf24cdc137816db82addfc0506be6e Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 16 Mar 2014 03:19:18 -0700 Subject: [PATCH 045/459] python 3.3 fix regarding implicit str encoding --- blessed/tests/accessories.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blessed/tests/accessories.py b/blessed/tests/accessories.py index e5eb2911..2209405e 100644 --- a/blessed/tests/accessories.py +++ b/blessed/tests/accessories.py @@ -60,7 +60,7 @@ def __call__(self, *args, **kwargs): o_err.extend([_exc.rstrip().encode('utf-8') for _exc in traceback.format_exception_only( e_type, e_value)]) - os.write(sys.__stdout__.fileno(), '\n'.join(o_err)) + os.write(sys.__stdout__.fileno(), b'\n'.join(o_err)) os.close(sys.__stdout__.fileno()) os.close(sys.__stderr__.fileno()) os.close(sys.__stdin__.fileno()) From 4b6be7c7bd673ac7d0d4fc931ea97e6fe06874c7 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 16 Mar 2014 03:22:46 -0700 Subject: [PATCH 046/459] resolve UndefinedName 'width' --- blessed/tests/test_wrap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blessed/tests/test_wrap.py b/blessed/tests/test_wrap.py index 15acbc27..370490ea 100644 --- a/blessed/tests/test_wrap.py +++ b/blessed/tests/test_wrap.py @@ -103,7 +103,7 @@ def child(kind): # ensure our colored textwrap is the same line length assert (len(internal_wrapped) == len(my_wrapped_colored)) # test subsequent_indent= - internal_wrapped = textwrap.wrap(pgraph, width, break_long_words=False, + internal_wrapped = textwrap.wrap(pgraph, WIDTH, break_long_words=False, subsequent_indent=' '*4) my_wrapped = t.wrap(pgraph, subsequent_indent=' '*4) my_wrapped_colored = t.wrap(pgraph_colored, subsequent_indent=' '*4) From 2d1ab6d913f035cbbc3e8ab4c577da538991df6c Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 16 Mar 2014 03:25:43 -0700 Subject: [PATCH 047/459] pep8 fix --- blessed/tests/test_wrap.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/blessed/tests/test_wrap.py b/blessed/tests/test_wrap.py index 370490ea..b10989ad 100644 --- a/blessed/tests/test_wrap.py +++ b/blessed/tests/test_wrap.py @@ -14,6 +14,7 @@ import pytest + @pytest.mark.skipif(platform.python_implementation() == 'PyPy', reason='PyPy fails TIOCSWINSZ') def test_SequenceWrapper(all_terms, many_columns): @@ -70,6 +71,7 @@ def child(kind, lines=25, cols=80): def test_SequenceWrapper_27(all_terms): """Test that text wrapping accounts for sequences correctly.""" WIDTH = 27 + @as_subprocess def child(kind): # build a test paragraph, along with a very colorful version From 165c8a78d08ca75bc7407b37b330fe6ed34a0d81 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 16 Mar 2014 03:27:08 -0700 Subject: [PATCH 048/459] enable pypy on travis-ci. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 786f7caa..0ed34089 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ env: - TOXENV=py26 - TOXENV=py27 - TOXENV=py33 + - TOXENV=pypy script: - tox From bff06ec9a6e1ef4a15afe5d427aaf9384465d7d7 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 16 Mar 2014 03:39:39 -0700 Subject: [PATCH 049/459] Parametrizing (sic) -> Parameterizing --- blessed/formatters.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/blessed/formatters.py b/blessed/formatters.py index fe4d21d7..92d4d34e 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -19,7 +19,7 @@ def derivative_colors(colors): COLORS.update(derivative_colors(COLORS)) -class ParametrizingString(unicode): +class ParameterizingString(unicode): """A Unicode string which can be called to parametrize it as a terminal capability""" @@ -78,7 +78,7 @@ def __call__(self, text): class NullCallableString(unicode): """A dummy callable Unicode to stand in for ``FormattingString`` and - ``ParametrizingString`` for terminals that cannot perform styling. + ``ParameterizingString`` for terminals that cannot perform styling. """ def __new__(cls): new = unicode.__new__(cls, u'') @@ -89,7 +89,7 @@ def __call__(self, *args): (hopefully a string of some kind). When called with an int as the first arg, return an empty Unicode. An - int is a good hint that I am a ``ParametrizingString``, as there are + int is a good hint that I am a ``ParameterizingString``, as there are only about half a dozen string-returning capabilities listed in terminfo(5) which accept non-int arguments, they are seldom used. @@ -98,7 +98,7 @@ def __call__(self, *args): any attributes. """ if len(args) != 1 or isinstance(args[0], int): - # I am acting as a ParametrizingString. + # I am acting as a ParameterizingString. # tparm can take not only ints but also (at least) strings as its # 2nd...nth argument. But we don't support callable parameterizing @@ -106,7 +106,7 @@ def __call__(self, *args): # # TODO(erikrose): Go through enough of the motions in the # capability resolvers to determine which of 2 special-purpose - # classes, NullParametrizableString or NullFormattingString, + # classes, NullParameterizableString or NullFormattingString, # to return, and retire this one. # # As a NullCallableString, even when provided with a parameter, @@ -157,7 +157,7 @@ def resolve_capability(term, attr): def resolve_attribute(term, attr): """Resolve a sugary or plain capability name, color, or compound formatting function name into a *callable* unicode string - capability, ``ParametrizingString`` or ``FormattingString``. + capability, ``ParameterizingString`` or ``FormattingString``. """ if attr in COLORS: return resolve_color(term, attr) @@ -180,7 +180,7 @@ def resolve_attribute(term, attr): else: return NullCallableString() - return ParametrizingString(resolve_capability(term, attr)) + return ParameterizingString(resolve_capability(term, attr)) def resolve_color(term, color): From 6b161ee50276c6b77c983a24bd694f8613c203ff Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 16 Mar 2014 03:40:01 -0700 Subject: [PATCH 050/459] cleanup changelog --- README.rst | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/README.rst b/README.rst index 5b9ae8d7..05b47d15 100644 --- a/README.rst +++ b/README.rst @@ -565,18 +565,18 @@ Version History 1.7 * Forked github project `erikrose/blessings`_ to `jquast/blessed`_, this project was previously known as **blessings** version 1.6 and prior. - * introduced context manager ``cbreak``, which is equivalent to ``tty.cbreak``, - placing the terminal in 'cooked' mode, allowing input from stdin to be read - as each key is pressed (line-buffering disabled). + * introduced context manager ``cbreak`` and ``raw``, which is equivalent + to ``tty.setcbreak`` and ``tty.setraw``, allowing input from stdin to be + read as each key is pressed. + * introduced ``inkey()``, which will return 1 or more characters as + a unicode sequence, with attributes ``.code`` and ``.name`` non-None when + a multibyte sequence is received, allowing arrow keys and such to be + detected. Optional value ``timeout`` allows timed polling or blocking. * introduced ``center()``, ``rjust()``, and ``ljust()`` methods, allows text containing sequences to be aligned to screen, or ``width`` specified. * introduced ``wrap()``, allows text containing sequences to be word-wrapped without breaking mid-sequence and honoring their printable width. - * introduced ``inkey()``, which will return 1 or more characters as - a unicode sequence, with attributes ``.code`` and ``.name`` non-None when - a multibyte sequence is received, allowing arrow keys and such to be - detected. Optional value ``timeout`` allows timed polling or blocking. * bugfix: cannot call ``setupterm()`` more than once per process -- issue a warning about what terminal kind subsequent calls will use. * bugfix: resolved issue where ``number_of_colors`` fails when ``does_styling`` @@ -585,9 +585,8 @@ Version History * bugfix: allow unsupported terminal capabilities to be callable just as supported capabilities, so that the return value of ``term.color(n)`` may be called on terminals without color capabilities. - * bugfix: for terminals without underline, such as vt220, - ``term.underline('xyz')`` would be ``(u'xyz' + term.normal)``, now it is only - ``u'xyz'``. + * bugfix: for terminals without underline, such as vt220, ``term.underline('x')`` + would be ``u'x' + term.normal``, now it is only ``u'x'``. * attributes that should be read-only have now raise exception when re-assigned (properties). * enhancement: pypy is not a supported platform implementation. @@ -612,7 +611,7 @@ Version History * Work around a tox parsing bug in its config file. * Make context managers clean up after themselves even if there's an exception. (Vitja Makarov) - * Parametrizing a capability no longer crashes when there is no tty. (Vitja + * Parameterizing a capability no longer crashes when there is no tty. (Vitja Makarov) 1.5 From de4c329ac60e2419741f99c7fc99476ca9c5ec28 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 16 Mar 2014 05:12:43 -0700 Subject: [PATCH 051/459] only test on_color & etc. for color terminals --- blessed/tests/test_sequences.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index 3cf40330..8fa240e2 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -211,10 +211,31 @@ def test_callable_numeric_colors(all_standard_terms): @as_subprocess def child(kind): t = TestTerminal(kind=kind) - assert (t.color(5)('smoo') == t.magenta + 'smoo' + t.normal) - assert (t.color(5)('smoo') == t.color(5) + 'smoo' + t.normal) - assert (t.on_color(2)('smoo') == t.on_green + 'smoo' + t.normal) - assert (t.on_color(2)('smoo') == t.on_color(2) + 'smoo' + t.normal) + if t.magenta: + assert t.color(5)('smoo') == t.magenta + 'smoo' + t.normal + else: + assert t.color(5)('smoo') == 'smoo' + + if t.on_magenta: + assert t.on_color(5)('smoo') == t.on_magenta + 'smoo' + t.normal + else: + assert t.color(5)(u'smoo') == 'smoo' + + if t.color(4): + assert t.color(4)(u'smoo') == t.color(4) + u'smoo' + t.normal + else: + assert t.color(4)(u'smoo') == 'smoo' + + if t.on_green: + assert t.on_color(2)('smoo') == t.on_green + u'smoo' + t.normal + else: + assert t.on_color(2)('smoo') == 'smoo' + + if t.on_color(6): + assert t.on_color(6)('smoo') == t.on_color(6) + u'smoo' + t.normal + else: + assert t.on_color(6)('smoo') == 'smoo' + child(all_standard_terms) From 35a12523e1bdc586b054ec36cf9365c59f889a14 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 16 Mar 2014 05:12:59 -0700 Subject: [PATCH 052/459] fixes to fixed-width test --- blessed/tests/test_wrap.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/blessed/tests/test_wrap.py b/blessed/tests/test_wrap.py index b10989ad..6cdba635 100644 --- a/blessed/tests/test_wrap.py +++ b/blessed/tests/test_wrap.py @@ -87,8 +87,8 @@ def child(kind): internal_wrapped = textwrap.wrap(pgraph, width=WIDTH, break_long_words=False) - my_wrapped = t.wrap(pgraph) - my_wrapped_colored = t.wrap(pgraph_colored) + my_wrapped = t.wrap(pgraph, width=WIDTH) + my_wrapped_colored = t.wrap(pgraph_colored, width=WIDTH) # ensure we textwrap ascii the same as python assert (internal_wrapped == my_wrapped) @@ -107,8 +107,10 @@ def child(kind): # test subsequent_indent= internal_wrapped = textwrap.wrap(pgraph, WIDTH, break_long_words=False, subsequent_indent=' '*4) - my_wrapped = t.wrap(pgraph, subsequent_indent=' '*4) - my_wrapped_colored = t.wrap(pgraph_colored, subsequent_indent=' '*4) + my_wrapped = t.wrap(pgraph, width=WIDTH, + subsequent_indent=' '*4) + my_wrapped_colored = t.wrap(pgraph_colored, width=WIDTH, + subsequent_indent=' '*4) assert (internal_wrapped == my_wrapped) assert (len(internal_wrapped) == len(my_wrapped_colored)) From 18eee9b4690006f130bb52547ba3188696c61162 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 16 Mar 2014 05:13:33 -0700 Subject: [PATCH 053/459] more Parameterizing and related fixes --- blessed/formatters.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/blessed/formatters.py b/blessed/formatters.py index 92d4d34e..ad953c54 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -23,7 +23,7 @@ class ParameterizingString(unicode): """A Unicode string which can be called to parametrize it as a terminal capability""" - def __new__(cls, attr, normal=None): + def __new__(cls, attr, normal): """Instantiate. :arg normal: If non-None, indicates that, once parametrized, this can @@ -41,9 +41,7 @@ def __call__(self, *args): # 3. However, appear to be a plain Unicode string otherwise so # concats work. attr = curses.tparm(self.encode('latin1'), *args).decode('latin1') - if self._normal: - return FormattingString(attr=attr, normal=self._normal) - return attr + return FormattingString(attr=attr, normal=self._normal) except TypeError: # If the first non-int (i.e. incorrect) arg was a string, suggest # something intelligent: @@ -51,7 +49,7 @@ def __call__(self, *args): raise TypeError( 'A native or nonexistent capability template received ' '%r when it was expecting ints. You probably misspelled a ' - 'formatting call like bright_red_on_white(...).' % args) + 'formatting call like bright_red_on_white(...).' % (args,)) # Somebody passed a non-string; I don't feel confident # guessing what they were trying to do. raise @@ -73,7 +71,9 @@ def __call__(self, text): """Return string ``text``, joined by specified video attribute, (self), and followed by reset attribute sequence (term.normal). """ - return u''.join((self, text, self._normal)) + if len(self): + return u''.join((self, text, self._normal)) + return text class NullCallableString(unicode): @@ -165,22 +165,17 @@ def resolve_attribute(term, attr): # Bold, underline, or something that takes no parameters if attr in COMPOUNDABLES: fmt_attr = resolve_capability(term, attr) - if fmt_attr: - return FormattingString(fmt_attr, term.normal) - else: - return NullCallableString() + return FormattingString(fmt_attr, term.normal) # A compound formatter, like "bold_green_on_red", recurse # into self, joining all returned compound attribute values. if all(fmt in COMPOUNDABLES for fmt in split_compound(attr)): fmt_attr = u''.join(resolve_attribute(term, ucs) # RECURSIVE for ucs in split_compound(attr)) - if fmt_attr: - return FormattingString(fmt_attr, term.normal) - else: - return NullCallableString() + return FormattingString(fmt_attr, term.normal) - return ParameterizingString(resolve_capability(term, attr)) + fmt_attr = resolve_capability(term, attr) + return ParameterizingString(fmt_attr, term.normal) def resolve_color(term, color): From 1b103c462f8fe9f695107373b12dd66f2dbf9dd6 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 16 Mar 2014 05:13:56 -0700 Subject: [PATCH 054/459] more detailed capability lookup failures --- blessed/sequences.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blessed/sequences.py b/blessed/sequences.py index c3f5d975..9e1d0c53 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -40,7 +40,7 @@ def _build_numeric_capability(term, cap, optional=False, # modify & return n to matching digit expression cap_re = cap_re.replace(str(num), r'(\d+)%s' % (opt,)) return cap_re - warnings.warn('Unknown parameter in %r, %r' % (cap, cap_re)) + warnings.warn('Unknown parameter in %r (%r, %r)' % (cap, _cap, cap_re)) return None # no such capability From b12d0845adc4c83c09cebfc8b26073e1e238d745 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 16 Mar 2014 05:14:16 -0700 Subject: [PATCH 055/459] more parameterizing spelling fixes, cache _normal --- blessed/terminal.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/blessed/terminal.py b/blessed/terminal.py index 0307f647..31cd043e 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -181,8 +181,8 @@ def __getattr__(self, attr): """Return a terminal capability as Unicode string. For example, ``term.bold`` is a unicode string that may be prepended - to text to set the video attribute for bold, which subsequently may - also be terminated with the pairing ``term.normal``. + to text to set the video attribute for bold, which should also be + terminated with the pairing ``term.normal``. This capability is also callable, so you can use ``term.bold("hi")`` which results in the joining of (term.bold, "hi", term.normal). @@ -196,11 +196,10 @@ def __getattr__(self, attr): """ if not self.does_styling: return formatters.NullCallableString() - val = formatters.resolve_attribute(self, attr) - # Cache capability codes. - setattr(self, attr, val) + if not attr in dir(self): + setattr(self, attr, val) return val @property @@ -335,7 +334,7 @@ def hidden_cursor(self): def color(self): """Return a capability that sets the foreground color. - The capability is unparametrized until called and passed a number + The capability is unparameterized until called and passed a number (0-15), at which point it returns another string which represents a specific color change. This second string can further be called to color a piece of text and set everything back to normal afterward. @@ -345,7 +344,7 @@ def color(self): """ if not self.does_styling: return formatters.NullCallableString() - return formatters.ParametrizingString( + return formatters.ParameterizingString( self._foreground_color, self.normal) @property @@ -357,9 +356,18 @@ def on_color(self): """ if not self.does_styling: return formatters.NullCallableString() - return formatters.ParametrizingString( + return formatters.ParameterizingString( self._background_color, self.normal) + @property + def normal(self): + """Return capability that resets video attribute. + """ + if '_normal' in dir(self): + return self._normal + self._normal = formatters.resolve_capability(self, 'normal') + return self._normal + @property def number_of_colors(self): """Return the number of colors the terminal supports. From 4c6193426e739f1e347de78132740d0555ced9f6 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 16 Mar 2014 05:25:56 -0700 Subject: [PATCH 056/459] avoid recursion on _normal --- blessed/terminal.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/blessed/terminal.py b/blessed/terminal.py index 31cd043e..985fb2c6 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -88,6 +88,7 @@ def __init__(self, kind=None, stream=None, force_styling=False): self._is_a_tty = stream_fd is not None and os.isatty(stream_fd) self._does_styling = ((self.is_a_tty or force_styling) and force_styling is not None) + self._normal = None # cache normal attr, preventing recursive lookups # keyboard input only valid when stream is sys.stdout @@ -198,8 +199,7 @@ def __getattr__(self, attr): return formatters.NullCallableString() val = formatters.resolve_attribute(self, attr) # Cache capability codes. - if not attr in dir(self): - setattr(self, attr, val) + setattr(self, attr, val) return val @property @@ -363,7 +363,7 @@ def on_color(self): def normal(self): """Return capability that resets video attribute. """ - if '_normal' in dir(self): + if self._normal: return self._normal self._normal = formatters.resolve_capability(self, 'normal') return self._normal From 3645980d3d5dd11dcc8d43d569d15cf1b6027c7c Mon Sep 17 00:00:00 2001 From: jquast Date: Mon, 17 Mar 2014 23:55:41 -0700 Subject: [PATCH 057/459] nibbles.bas written in blessed, how quaint. --- bin/worms.py | 124 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100755 bin/worms.py diff --git a/bin/worms.py b/bin/worms.py new file mode 100755 index 00000000..a239df1d --- /dev/null +++ b/bin/worms.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python +from __future__ import division +from collections import namedtuple +from random import randrange +from functools import partial +from blessed import Terminal + +term = Terminal() + +# a worm is made of segments, of (y, x) Locations +Location = namedtuple('Point', ('y', 'x',)) + +# a nibble is a location and a value +Nibble = namedtuple('Nibble', ('location', 'value')) + +# A direction is a bearing, +# y=0, x=-1 = move right +Direction = namedtuple('Direction', ('y', 'x',)) + +# these functions return a new Location instance, given +# the direction indicated by their name. +left_of = lambda s: Location( + y=s.y, x=max(0, s.x - 1)) +right_of = lambda s: Location( + y=s.y, x=min(term.width - 1, s.x + 1)) +below = lambda s: Location( + y=min(term.height - 1, s.y + 1), x=s.x) +above = lambda s: Location( + y=max(0, s.y - 1), x=s.x) + +# returns one of the functions that returns +# a new segment in the direction indicated +# by bearing `d'. +moved = lambda d: (left_of if d.x < 0 else + right_of if d.x else + above if d.y < 0 else + below if d.y else None) + +# returns True if segment is found in worm. +hit_any = lambda segment, worm: segment in worm + +# returns True if segments are same position +hit = lambda src, dst: src.x == dst.x and src.y == dst.y + +# return function that defines the new bearing for any matching +# keyboard code, otherwise the function for the current bearing. +next_bearing = lambda inp_code, bearing: ( + left_of if inp_code == term.KEY_LEFT else + right_of if inp_code == term.KEY_RIGHT else + below if inp_code == term.KEY_DOWN else + above if inp_code == term.KEY_UP else + moved(bearing) +) + +# return new bearing given the movement f(x). +change_bearing = lambda f_mov, segment: Direction( + f_mov(segment).y - segment.y, + f_mov(segment).x - segment.x) + +echo = partial(print, end='', flush=True) + +make_nibble = lambda value: Nibble( + location=Location(x=randrange(1, term.width - 1), + y=randrange(1, term.height - 1)), + value=value + 1) + + +def main(): + worm = [Location(x=term.width // 2, y=term.height // 2)] + worm_length = 2 + bearing = Direction(0, -1) + nibble = make_nibble(-1) + color_nibble = term.bright_red + color_worm = term.bright_yellow + color_bg = term.on_blue + echo(term.move(1, 1)) + echo(color_bg(term.clear)) + speed = 0.1 + modifier = 0.95 + + with term.hidden_cursor(), term.cbreak(): + inp = None + while inp not in (u'q', u'Q'): + + # delete the tail of the worm at worm_length + if len(worm) > worm_length: + echo(term.move(*worm.pop(0))) + echo(color_bg(u' ')) + + head = worm.pop() + if hit_any(head, worm): + break + + elif nibble.value is 0 or hit(head, nibble.location): + # eat, + value = nibble.value + worm_length += value + # create new digit, + nibble = make_nibble(value) + # unless it is within our worm .. + while hit_any(nibble.location, worm): + nibble = make_nibble(value) + # display it + echo(term.move(*nibble.location)) + echo(color_nibble('{0}'.format(nibble.value))) + # speed up, + speed = speed * modifier + + # display new worm head + echo(term.move(*head)) + echo(color_worm(u'\u2588')) + + # wait for keyboard input, which may indicate + # a new direction (up/down/left/right) + inp = term.inkey(speed) + direction = next_bearing(inp.code, bearing) + bearing = change_bearing(direction, head) + + # append the prior `head' onto the worm, then + # a new `head' for the given direction. + worm.extend([head, direction(head)]) + +if __name__ == '__main__': + main() From 563da12e0d4f0bc4de404a61fd2d3ddaf131618d Mon Sep 17 00:00:00 2001 From: jquast Date: Tue, 18 Mar 2014 03:03:47 -0700 Subject: [PATCH 058/459] nibbles.bas clone updates, sorry billy gates ! --- bin/worms.py | 125 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 81 insertions(+), 44 deletions(-) diff --git a/bin/worms.py b/bin/worms.py index a239df1d..55ffe012 100755 --- a/bin/worms.py +++ b/bin/worms.py @@ -7,50 +7,48 @@ term = Terminal() -# a worm is made of segments, of (y, x) Locations +# a worm is a list of (y, x) segments Locations Location = namedtuple('Point', ('y', 'x',)) -# a nibble is a location and a value +# a nibble is a (x,y) Location and value Nibble = namedtuple('Nibble', ('location', 'value')) -# A direction is a bearing, +# A direction is a bearing, fe. # y=0, x=-1 = move right +# y=1, x=0 = move down Direction = namedtuple('Direction', ('y', 'x',)) # these functions return a new Location instance, given # the direction indicated by their name. left_of = lambda s: Location( y=s.y, x=max(0, s.x - 1)) + right_of = lambda s: Location( y=s.y, x=min(term.width - 1, s.x + 1)) + below = lambda s: Location( y=min(term.height - 1, s.y + 1), x=s.x) + above = lambda s: Location( y=max(0, s.y - 1), x=s.x) -# returns one of the functions that returns -# a new segment in the direction indicated -# by bearing `d'. -moved = lambda d: (left_of if d.x < 0 else - right_of if d.x else - above if d.y < 0 else - below if d.y else None) - -# returns True if segment is found in worm. -hit_any = lambda segment, worm: segment in worm - -# returns True if segments are same position -hit = lambda src, dst: src.x == dst.x and src.y == dst.y +# returns a function providing the new location for the +# given `bearing' - a (y,x) difference of (src, dst). +move_given = lambda bearing: { + (0, -1): left_of, + (0, 1): right_of, + (-1, 0): above, + (1, 0): below}[(bearing.y, bearing.x)] # return function that defines the new bearing for any matching # keyboard code, otherwise the function for the current bearing. -next_bearing = lambda inp_code, bearing: ( - left_of if inp_code == term.KEY_LEFT else - right_of if inp_code == term.KEY_RIGHT else - below if inp_code == term.KEY_DOWN else - above if inp_code == term.KEY_UP else - moved(bearing) -) +next_bearing = lambda inp_code, bearing: { + term.KEY_LEFT: left_of, + term.KEY_RIGHT: right_of, + term.KEY_DOWN: below, + term.KEY_UP: above, +}.get(inp_code, move_given(bearing)) + # return new bearing given the movement f(x). change_bearing = lambda f_mov, segment: Direction( @@ -59,17 +57,48 @@ echo = partial(print, end='', flush=True) -make_nibble = lambda value: Nibble( - location=Location(x=randrange(1, term.width - 1), - y=randrange(1, term.height - 1)), - value=value + 1) +# generate a new 'nibble' (number for worm bite) +new_nibble = lambda t, v: Nibble( + # create new random (x, y) location + location=Location(x=randrange(1, t.width - 1), + y=randrange(1, t.height - 1)), + # increase given value by 1 + value=v + 1) + +# returns True if `loc' matches any (y, x) coordinates, +# within list `segments' -- such as a list composing a worm. +hit_any = lambda loc, segments: loc in segments + +# returns True if segments are same position +hit = lambda src, dst: src.x == dst.x and src.y == dst.y + +# returns a new Nibble if the current one is hit, +def next_nibble(term, nibble, head): + return (new_nibble(term, nibble.value) + if hit(head, nibble.location) else + nibble) + +# returns new worm_length if current nibble is hit, +def next_wormlength(nibble, head, worm_length): + return (worm_length + nibble.value + if hit(head, nibble.location) else + worm_length) + +def next_speed(nibble, head, speed, modifier): + return (speed * modifier + if hit(head, nibble.location) else + speed) + + +#nibble = next_nibble(nibble, head, worm_length) +#worm_length = next_wormlength(nibble, head) def main(): worm = [Location(x=term.width // 2, y=term.height // 2)] worm_length = 2 bearing = Direction(0, -1) - nibble = make_nibble(-1) + nibble = Nibble(location=worm[0], value=0) color_nibble = term.bright_red color_worm = term.bright_yellow color_bg = term.on_blue @@ -91,34 +120,42 @@ def main(): if hit_any(head, worm): break - elif nibble.value is 0 or hit(head, nibble.location): - # eat, - value = nibble.value - worm_length += value - # create new digit, - nibble = make_nibble(value) - # unless it is within our worm .. - while hit_any(nibble.location, worm): - nibble = make_nibble(value) - # display it - echo(term.move(*nibble.location)) - echo(color_nibble('{0}'.format(nibble.value))) - # speed up, - speed = speed * modifier - - # display new worm head + # check for nibble hit (new Nibble returned). + n_nibble = next_nibble(term, nibble, head) + + # new worm_length & speed, if hit. + worm_length = next_wormlength(nibble, head, worm_length) + speed = next_speed(nibble, head, speed, modifier) + + # display next nibble, if hit + if n_nibble != nibble: + echo(term.move(*n_nibble.location)) + echo(color_nibble('{0}'.format(n_nibble.value))) + + # display new worm head each turn, regardless. echo(term.move(*head)) echo(color_worm(u'\u2588')) # wait for keyboard input, which may indicate # a new direction (up/down/left/right) inp = term.inkey(speed) + + # discover new direction, given keyboard input and/or bearing direction = next_bearing(inp.code, bearing) + + # discover new bearing, given new direction compared to prev bearing = change_bearing(direction, head) # append the prior `head' onto the worm, then # a new `head' for the given direction. worm.extend([head, direction(head)]) + # re-assign new nibble, + nibble = n_nibble + + score = (worm_length - 1) * 100 + echo(u''.join((term.move(term.height - 1, 1), term.normal))) + echo(u''.join((u'\r\n', u'score: {}'.format(score), u'\r\n'))) + if __name__ == '__main__': main() From eee71d3c6158e11b9c6833b1bdf7960c6da03263 Mon Sep 17 00:00:00 2001 From: jquast Date: Tue, 18 Mar 2014 12:54:41 -0700 Subject: [PATCH 059/459] re-enable tests on_compound, accidentally missed --- blessed/tests/test_sequences.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index 8fa240e2..6ab3e33a 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -314,6 +314,8 @@ def child(kind): assert (t.on_bright_red_bold_bright_green_underline('meh') == expected_output) + child(all_standard_terms) + def test_formatting_functions_without_tty(all_standard_terms): """Test crazy-ass formatting wrappers when there's no tty.""" From c7352847b158b335b7e16a07b9e25f5207b49a9e Mon Sep 17 00:00:00 2001 From: jquast Date: Tue, 18 Mar 2014 18:06:54 -0700 Subject: [PATCH 060/459] Closes issue #6 1. make ParameterizingString descriptive and kind esp. regarding multi-arguments, change "if len == 1" test to be "if len", so that we can more kindly report to the user about their failed attempt at blue("1", "2") whoops. 2. compoundables and derivatives got out of order when we refactored these into a submodule, the red_on_black wasn't matching, because the on_black derivatives weren't calculated in the correct order. --- blessed/formatters.py | 82 ++++++++++++++++++++----------------------- blessed/terminal.py | 10 +++--- 2 files changed, 44 insertions(+), 48 deletions(-) diff --git a/blessed/formatters.py b/blessed/formatters.py index ad953c54..a07ad180 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -1,29 +1,23 @@ -import collections import curses -import struct +_derivitives = ('on', 'bright', 'on_bright',) -COLORS = set(['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', - 'white']) -COMPOUNDABLES = (COLORS | - set(['bold', 'underline', 'reverse', 'blink', 'dim', 'italic', - 'shadow', 'standout', 'subscript', 'superscript'])) +_colors = set('black red green yellow blue magenta cyan white'.split()) +_compoundables = set('bold underline reverse blink dim italic shadow ' + 'standout subscript superscript'.split()) +COLORS = set(['_'.join((derivitive, color)) + for derivitive in _derivitives + for color in _colors]) | _colors -def derivative_colors(colors): - """Return the names of valid color variants, given the base colors.""" - return set([('on_' + c) for c in colors] + - [('bright_' + c) for c in colors] + - [('on_bright_' + c) for c in colors]) - -COLORS.update(derivative_colors(COLORS)) +COMPOUNDABLES = (COLORS | _compoundables) class ParameterizingString(unicode): """A Unicode string which can be called to parametrize it as a terminal capability""" - def __new__(cls, attr, normal): + def __new__(cls, name, attr, normal): """Instantiate. :arg normal: If non-None, indicates that, once parametrized, this can @@ -32,6 +26,7 @@ def __new__(cls, attr, normal): """ new = unicode.__new__(cls, attr) + new._name = name new._normal = normal return new @@ -42,14 +37,15 @@ def __call__(self, *args): # concats work. attr = curses.tparm(self.encode('latin1'), *args).decode('latin1') return FormattingString(attr=attr, normal=self._normal) - except TypeError: + except TypeError, err: # If the first non-int (i.e. incorrect) arg was a string, suggest # something intelligent: - if len(args) == 1 and isinstance(args[0], basestring): + if len(args) and isinstance(args[0], basestring): raise TypeError( - 'A native or nonexistent capability template received ' - '%r when it was expecting ints. You probably misspelled a ' - 'formatting call like bright_red_on_white(...).' % (args,)) + "A native or nonexistent capability template, %r received" + " invalid argument %r: %s. You probably misspelled a" + " formatting call like `bright_red'" % ( + self._name, args, err)) # Somebody passed a non-string; I don't feel confident # guessing what they were trying to do. raise @@ -140,18 +136,13 @@ def split_compound(compound): def resolve_capability(term, attr): - """Return a Unicode string containing terminal sequence for - capability (or term_sugar alias) ``attr`` of Terminal instance - ``term`` by querying curses.tigetstr. - - If the terminal does not have any value for the capability, an empty - Unicode string is returned. + """Return a Unicode string for the terminal capability ``attr``, + or an empty string if not found. """ - code = curses.tigetstr(term._sugar.get(attr, attr)) - if code: - # Decode sequences as latin1, as they are always 8-bit bytes. - return code.decode('latin1') - return u'' + # Decode sequences as latin1, as they are always 8-bit bytes, so when + # b'\xff' is returned, this must be decoded to u'\xff'. + val = curses.tigetstr(term._sugar.get(attr, attr)) + return u'' if val is None else val.decode('latin1') def resolve_attribute(term, attr): @@ -159,23 +150,26 @@ def resolve_attribute(term, attr): formatting function name into a *callable* unicode string capability, ``ParameterizingString`` or ``FormattingString``. """ + # A simple color, such as `red' or `blue'. if attr in COLORS: return resolve_color(term, attr) - # Bold, underline, or something that takes no parameters + # A direct compoundable, such as `bold' or `on_red'. if attr in COMPOUNDABLES: - fmt_attr = resolve_capability(term, attr) - return FormattingString(fmt_attr, term.normal) - - # A compound formatter, like "bold_green_on_red", recurse - # into self, joining all returned compound attribute values. - if all(fmt in COMPOUNDABLES for fmt in split_compound(attr)): - fmt_attr = u''.join(resolve_attribute(term, ucs) # RECURSIVE - for ucs in split_compound(attr)) - return FormattingString(fmt_attr, term.normal) - - fmt_attr = resolve_capability(term, attr) - return ParameterizingString(fmt_attr, term.normal) + return FormattingString(resolve_capability(term, attr), + term.normal) + + # Given `bold_on_red', resolve to ('bold', 'on_red'), RECURSIVE + # call for each compounding section, joined and returned as + # a completed completed FormattingString. + formatters = split_compound(attr) + if all(fmt in COMPOUNDABLES for fmt in formatters): + resolution = (resolve_attribute(term, fmt) for fmt in formatters) + return FormattingString(u''.join(resolution), term.normal) + else: + return ParameterizingString(name=attr, + attr=resolve_capability(term, attr), + normal=term.normal) def resolve_color(term, color): diff --git a/blessed/terminal.py b/blessed/terminal.py index 985fb2c6..87ec69d9 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -344,8 +344,9 @@ def color(self): """ if not self.does_styling: return formatters.NullCallableString() - return formatters.ParameterizingString( - self._foreground_color, self.normal) + return formatters.ParameterizingString(name='color', + attr=self._foreground_color, + normal=self.normal) @property def on_color(self): @@ -356,8 +357,9 @@ def on_color(self): """ if not self.does_styling: return formatters.NullCallableString() - return formatters.ParameterizingString( - self._background_color, self.normal) + return formatters.ParameterizingString(name='on_color', + attr=self._background_color, + normal=self.normal) @property def normal(self): From ca63df7ec4018d4970c768047ef334bb15b46fef Mon Sep 17 00:00:00 2001 From: jquast Date: Tue, 18 Mar 2014 18:07:25 -0700 Subject: [PATCH 061/459] fixed test case for pypy and misspelled issue --- blessed/tests/test_sequences.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index 6ab3e33a..1226f6c4 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -357,14 +357,14 @@ def child(kind): e = sys.exc_info()[1] assert 'probably misspelled' not in e.args[0] - if platform.python_implementation() != 'PyPy': - # PyPy fails to toss an exception? - try: - t.bold_misspelled('a', 'b') # >1 string arg - assert not t.is_a_tty or False, 'Should have thrown exception' - except TypeError: - e = sys.exc_info()[1] - assert 'probably misspelled' not in e.args[0] + #if platform.python_implementation() != 'PyPy': + # PyPy fails to toss an exception? + try: + t.bold_misspelled('a', 'b') # >1 string arg + assert not t.is_a_tty or False, 'Should have thrown exception' + except TypeError: + e = sys.exc_info()[1] + assert 'probably misspelled' in e.args[0], e.args child(all_standard_terms) From 11e5c4935039a5d336f0bce21c9252bbc3cfe6bd Mon Sep 17 00:00:00 2001 From: jquast Date: Tue, 18 Mar 2014 22:16:41 -0700 Subject: [PATCH 062/459] cosmetic improvements to worm example --- bin/worms.py | 64 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/bin/worms.py b/bin/worms.py index 55ffe012..024f770e 100755 --- a/bin/worms.py +++ b/bin/worms.py @@ -3,6 +3,7 @@ from collections import namedtuple from random import randrange from functools import partial +from itertools import takewhile, count from blessed import Terminal term = Terminal() @@ -69,29 +70,18 @@ # within list `segments' -- such as a list composing a worm. hit_any = lambda loc, segments: loc in segments -# returns True if segments are same position +# returns True if segments are same position (hit detection) hit = lambda src, dst: src.x == dst.x and src.y == dst.y -# returns a new Nibble if the current one is hit, -def next_nibble(term, nibble, head): - return (new_nibble(term, nibble.value) - if hit(head, nibble.location) else - nibble) - # returns new worm_length if current nibble is hit, -def next_wormlength(nibble, head, worm_length): - return (worm_length + nibble.value - if hit(head, nibble.location) else - worm_length) - -def next_speed(nibble, head, speed, modifier): - return (speed * modifier - if hit(head, nibble.location) else - speed) - +next_wormlength = lambda nibble, head, worm_length: ( + worm_length + nibble.value if hit(head, nibble.location) + else worm_length) -#nibble = next_nibble(nibble, head, worm_length) -#worm_length = next_wormlength(nibble, head) +# returns new speed if current nibble is hit, +next_speed = lambda nibble, head, speed, modifier: ( + speed * modifier if hit(head, nibble.location) + else speed) def main(): @@ -99,15 +89,17 @@ def main(): worm_length = 2 bearing = Direction(0, -1) nibble = Nibble(location=worm[0], value=0) - color_nibble = term.bright_red - color_worm = term.bright_yellow + color_nibble = term.black_on_green + color_worm = term.bright_yellow_on_blue + color_head = term.bright_red_on_blue color_bg = term.on_blue echo(term.move(1, 1)) echo(color_bg(term.clear)) speed = 0.1 modifier = 0.95 + direction = next_bearing(None, bearing) - with term.hidden_cursor(), term.cbreak(): + with term.hidden_cursor(), term.raw(): inp = None while inp not in (u'q', u'Q'): @@ -121,20 +113,42 @@ def main(): break # check for nibble hit (new Nibble returned). - n_nibble = next_nibble(term, nibble, head) + n_nibble = (new_nibble(term, nibble.value) + if hit(head, nibble.location) else nibble) + + # ensure new nibble is regenerated outside of worm + while hit_any(n_nibble, worm): + n_nibble = next_nibble(term, nibble, head, worm) # new worm_length & speed, if hit. worm_length = next_wormlength(nibble, head, worm_length) speed = next_speed(nibble, head, speed, modifier) - # display next nibble, if hit + # display next nibble if a new one was generated, + # and erase the old one if n_nibble != nibble: echo(term.move(*n_nibble.location)) echo(color_nibble('{0}'.format(n_nibble.value))) + # erase '7' from nibble '17', using ' ' for empty space, + # or the worm body parts for a worm chunk + for offset in range(1, 1 + len(str(nibble.value)) - 1): + x = nibble.location.x + offset + y = nibble.location.y + echo(term.move(y, x)) + if hit_any((y, x), worm): + echo(color_worm(u'\u2689')) + else: + echo(color_bg(u' ')) + # display new worm head each turn, regardless. echo(term.move(*head)) - echo(color_worm(u'\u2588')) + echo(color_head(u'\u263a')) + + if worm: + # and its old head (now, a body piece) + echo(term.move(*worm[-1])) + echo(color_worm(u'\u2689')) # wait for keyboard input, which may indicate # a new direction (up/down/left/right) From 50ab66b37f382b58ad80694a331065ec0a18a1c9 Mon Sep 17 00:00:00 2001 From: jquast Date: Tue, 18 Mar 2014 22:17:36 -0700 Subject: [PATCH 063/459] re-introduce excpetion to PyPy's failure to raise excpetion Will need to look into this more, I think my local pypy using tox and travis' pypy differ in this way --- blessed/tests/test_sequences.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index 1226f6c4..341611fc 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -357,14 +357,14 @@ def child(kind): e = sys.exc_info()[1] assert 'probably misspelled' not in e.args[0] - #if platform.python_implementation() != 'PyPy': - # PyPy fails to toss an exception? - try: - t.bold_misspelled('a', 'b') # >1 string arg - assert not t.is_a_tty or False, 'Should have thrown exception' - except TypeError: - e = sys.exc_info()[1] - assert 'probably misspelled' in e.args[0], e.args + if platform.python_implementation() != 'PyPy': + # PyPy fails to toss an exception, Why?! + try: + t.bold_misspelled('a', 'b') # >1 string arg + assert not t.is_a_tty or False, 'Should have thrown exception' + except TypeError: + e = sys.exc_info()[1] + assert 'probably misspelled' in e.args[0], e.args child(all_standard_terms) From bb648679ee662eb819968c7578c659c266f2474e Mon Sep 17 00:00:00 2001 From: jquast Date: Thu, 20 Mar 2014 00:03:25 -0700 Subject: [PATCH 064/459] massive readme update. --- README.rst | 366 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 210 insertions(+), 156 deletions(-) diff --git a/README.rst b/README.rst index 05b47d15..82280b84 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ Blessed ======= -Coding with Blessed looks like this... :: +Coding with *Blessed* looks like this... :: from blessed import Terminal @@ -22,28 +22,26 @@ capabilities:: print('{t.bold}All your {t.red}bold and red base{t.normal}'.format(t=t)) + The Pitch ========= -Blessed lifts several of curses_' limiting assumptions, and it makes your -code pretty, too: +*Blessed* is a more simplified wrapper around curses_, providing : -* Use styles, color, and maybe a little positioning without necessarily +* Styles, color, and maybe a little positioning without necessarily clearing the whole screen first. * Leave more than one screenful of scrollback in the buffer after your program exits, like a well-behaved command-line application should. -* Get rid of all those noisy, C-like calls to ``tigetstr`` and ``tparm``, so - your code doesn't get crowded out by terminal bookkeeping. -* Act intelligently when somebody redirects your output to a file, omitting the - terminal control codes the user doesn't want to see (optional). +* No more C-like calls to ``tigetstr`` and ``tparm``. +* Act intelligently when somebody redirects your output to a file, omitting + all of the terminal sequences such as styling, colors, or positioning. -.. _curses: http://docs.python.org/library/curses.html Before And After ---------------- -Without Blessed, this is how you'd print some underlined text at the bottom -of the screen:: +With the built-in curses_ module, this is how you would typically +print some underlined text at the bottom of the screen:: from curses import tigetstr, setupterm, tparm from fcntl import ioctl @@ -72,8 +70,7 @@ of the screen:: normal=normal)) print(rc) # Restore cursor position. -That was long and full of incomprehensible trash! Let's try it again, this time -with Blessed:: +The same program with *Blessed* is simply:: from blessed import Terminal @@ -81,88 +78,72 @@ with Blessed:: with term.location(0, term.height - 1): print('This is', term.underline('pretty!')) -Much better. What It Provides ================ -Blessed provides just one top-level object: ``Terminal``. Instantiating a -``Terminal`` figures out whether you're on a terminal at all and, if so, does +Blessed provides just **one** top-level object: *Terminal*. Instantiating a +*Terminal* figures out whether you're on a terminal at all and, if so, does any necessary terminal setup. After that, you can proceed to ask it all sorts -of things about the terminal. Terminal terminal terminal. +of things about the terminal. + Simple Formatting ----------------- -Lots of handy formatting codes (capabilities, `terminfo(5)`_) are available -as attributes on a ``Terminal``. For example:: +Lots of handy formatting codes `terminfo(5)`_ are available as attributes +on a *Terminal* class instance. For example:: from blessed import Terminal term = Terminal() print('I am ' + term.bold + 'bold' + term.normal + '!') -Though they are strings at heart, you can also use them as callable wrappers, -which automatically ends each string with ``normal`` attributes:: +These capabilities (bold, normal) are translated to their sequences, which +when displayed simply change the video attributes. + +And, when used as a callable, automatically wraps the given string with this +sequence, and terminates it with *normal*. The same can be written as:: - print('I am', term.bold('bold') + '!') + print('I am' + term.bold('bold') + '!') -You may also use Python's string ``.format`` method:: +You may also use the *Terminal* instance as an argument for ``.format`` string +method, so that capabilities can be displayed inline for more complex strings:: - print('All your {t.red}base {t.underline}are belong to us{t.normal}' - .format(t=term)) + print('{t.red_on_yellow}Candy corn{t.normal} for everyone!'.format(t=term)) -Simple capabilities of interest include... +The basic capabilities supported by most terminals are: -* ``bold`` -* ``reverse`` -* ``blink`` -* ``normal`` (which turns off everything, even colors) +* ``bold``: Turn on 'extra bright' mode. +* ``reverse``: Switch fore and background attributes. +* ``blink``: Turn on blinking. +* ``normal``: Reset attributes to default. -Here are a few more which are less likely to work on all terminals: +The less commonly supported capabilities: -* ``dim`` -* ``underline`` -* ``no_underline`` (which turns off underlining) -* ``italic`` and ``no_italic`` -* ``shadow`` and ``no_shadow`` -* ``standout`` and ``no_standout`` -* ``subscript`` and ``no_subscript`` -* ``superscript`` and ``no_superscript`` -* ``flash`` (which flashes the screen once) +* ``dim``: Turn on 'half-bright' mode. +* ``underline`` and ``no_underline``. +* ``italic`` and ``no_italic``. +* ``shadow`` and ``no_shadow``. +* ``standout`` and ``no_standout``. +* ``subscript`` and ``no_subscript``. +* ``superscript`` and ``no_superscript``. +* ``flash``: Visual bell, which flashes the screen. Note that, while the inverse of ``underline`` is ``no_underline``, the only way to turn off ``bold`` or ``reverse`` is ``normal``, which also cancels any -custom colors. This is because there's no portable way to tell the terminal to -undo certain pieces of formatting, even at the lowest level. +custom colors. -You might also notice that the above aren't the typical incomprehensible -terminfo capability names; we alias a few of the harder-to-remember ones for -readability. However, you aren't limited to these: you can reference any -string-returning capability listed on the `terminfo(5)`_ manual page, by the name -under the **Cap-name** column: for example, ``term.rum`` (End reverse character). +Many of these are aliases, their true capability names (such as ``smul`` for +*begin underline mode*) may still be used. Any capability in the `terminfo(5)_` +manual, under column **Cap-name**, may be used as an attribute to a *Terminal* +instance. If it is not a supported capability, an empty string is returned. -.. _`terminfo(5)`: http://www.openbsd.org/cgi-bin/man.cgi?query=terminfo&apropos=0&sektion=5 Color ----- -16 colors, both foreground and background, are available as easy-to-remember -attributes:: - - from blessed import Terminal - - term = Terminal() - print(term.red + term.on_green + 'Red on green? Ick!' + term.normal) - print(term.bright_red + term.on_bright_blue + 'This is even worse!' + term.normal) - -You can also call them as wrappers, which sets everything back to normal at the -end:: - - print(term.red_on_green('Red on green? Ick!')) - print(term.yellow('I can barely see it.')) - -The available colors are... +Color terminals are capable of at least 8 basic colors. * ``black`` * ``red`` @@ -173,26 +154,57 @@ The available colors are... * ``cyan`` * ``white`` -You can set the background color instead of the foreground by prepending -``on_``, as in ``on_blue``. There is also a ``bright`` version of each color: -for example, ``on_bright_blue``. +The same colors, prefixed with ``bright_`` (synonymous with ``bold_``), +such as ``bright_blue``, providing 16 colors in total (if you count black +as a color). On most terminals, ``bright_black`` is actually a very dim +gray! + +The same colors, prefixed with ``on_`` sets the background color, some +terminals also provide an additional 8 high-intensity versions using +``on_bright``, some example compound formats:: + + from blessed import Terminal + + term = Terminal() + print(term.on_bright_blue('Blue skies!')) + print(term.bright_red_on_bright_yellow('Pepperoni Pizza!')) + There is also a numerical interface to colors, which takes an integer from -0-15:: +0-15.:: + + from blessed import Terminal + + term = Terminal() + + for n in range(16): + print(term.color(n)('Color {}'.format(n))) + +If the terminal defined by the **TERM** environment variable does not support +colors, these simply return empty strings, or the string passed as an argument +when used as a callable, without any video attributes. If the **TERM** defines +a terminal that does support colors, but actually does not, they are usually +harmless. + +Colorless terminals, such as the amber or monochrome *vt220*, do not support +colors but do support reverse video. For this reason, it may be desirable in +some applications, such as a selection bar, to simply select a foreground +color, followed by reverse video to achieve the desired background color +effect:: - term.color(5) + 'Hello' + term.normal - term.on_color(3) + 'Hello' + term.normal + from blessed import Terminal + + term = Terminal() - term.color(5)('Hello') - term.on_color(3)('Hello') + print('terminals {standout} more than others'.format( + standout=term.green_reverse('standout'))) -If some color is unsupported (for instance, if only the normal colors are -available, not the bright ones), trying to use it will, on most terminals, have -no effect: the foreground and background colors will stay as they were. You can -get fancy and do different things depending on the supported colors by checking -`number_of_colors`_. +Which appears as *bright white on green* on color terminals, or *black text +on amber or green* on monochrome terminals. You can check whether the terminal +definition used supports colors, and how many, using the ``number_of_colors`` +property, which returns any of *0* *8* or *256* for terminal types +such as *vt220*, *ansi*, and *xterm-256color*, respectively. -.. _`number_of_colors`: http://packages.python.org/blessed/#blessed.Terminal.number_of_colors Compound Formatting ------------------- @@ -203,30 +215,49 @@ all together:: from blessed import Terminal term = Terminal() - print(term.bold_underline_green_on_yellow('Woo')) -This compound notation comes in handy if you want to allow users to customize -the formatting of your app: just have them pass in a format specifier like -"bold_green" on the command line, and do a quick ``getattr(term, -that_option)('Your text')`` when you do your formatting. + print(term.bold_underline_green_on_yellow('Woo')) I'd be remiss if I didn't credit couleur_, where I probably got the idea for -all this mashing. +all this mashing. This compound notation comes in handy if you want to allow +users to customize the formatting of your app: just have them pass in a format +specifier like **bold_green** as a command line argument or configuration item:: + + #!/usr/bin/env python + import argparse + + parser = argparse.ArgumentParser( + description='displays argument as specified style') + parser.add_argument('style', type=str, help='style formatter') + parser.add_argument('text', type=str, nargs='+') + + from blessed import Terminal + + term = Terminal() + args = parser.parse_args() + + style = getattr(term, args.style) + + print(style(' '.join(args.text))) + +Saved as **tprint.py**, this could be called simply:: + + $ ./tprint.py bright_blue_reverse Blue Skies -.. _couleur: http://pypi.python.org/pypi/couleur Moving The Cursor ----------------- -When you want to move the cursor to output text at a specific spot, you have -a few choices. +When you want to move the cursor, you have a few choices, the +``location(y=None, x=None)`` context manager, ``move(y, x)``, ``move_y``, + and ``move_x`` attributes. + Moving Temporarily ~~~~~~~~~~~~~~~~~~ -Most often, moving to a screen position is only temporary. A contest manager, -``location`` is provided to move to a screen position and restore the previous -position upon exit:: +A context manager, ``location`` is provided to move the cursor to a *(x, y)* +screen position and restore the previous position upon exit:: from blessed import Terminal @@ -235,24 +266,19 @@ position upon exit:: print('Here is the bottom.') print('This is back where I came from.') -Parameters to ``location()`` are ``x`` and then ``y``, but you can also pass -just one of them, leaving the other alone. For example... :: +Parameters to ``location()`` are **optional** ``x`` and/or ``y``:: with term.location(y=10): print('We changed just the row.') -If you're doing a series of ``move`` calls (see below) and want to return the -cursor to its original position afterward, call ``location()`` with no -arguments, and it will do only the position restoring:: +When omitted, it saves the cursor position and restore it upon exit:: with term.location(): print(term.move(1, 1) + 'Hi') print(term.move(9, 9) + 'Mom') -Note that, since ``location()`` uses the terminal's built-in -position-remembering machinery, you can't usefully nest multiple calls. Use -``location()`` at the outermost spot, and use simpler things like ``move`` -inside. +*NOTE*: ``location`` may not be nested, as only one location may be saved. + Moving Permanently ~~~~~~~~~~~~~~~~~~ @@ -266,26 +292,19 @@ this:: print(term.move(10, 1) + 'Hi, mom!') ``move`` - Position the cursor elsewhere. Parameters are y coordinate, then x - coordinate. + Position the cursor, parameter in form of *(y, x)* ``move_x`` - Move the cursor to the given column. + Position the cursor at given horizontal column. ``move_y`` - Move the cursor to the given row. + Position the cursor at given vertical column. -How does all this work? These are simply more terminal capabilities, wrapped to -give them nicer names. The added wrinkle--that they take parameters--is also -given a pleasant treatment: rather than making you dig up ``tparm()`` all the -time, we simply make these capabilities into callable strings. You'd get the -raw capability strings if you were to just print them, but they're fully -parametrized if you pass params to them as if they were functions. +*NOTE*: The ``location`` method receives arguments in form of *(x, y)*, +where the ``move`` argument receives arguments in form of *(y, x)*. -Consequently, you can also reference any other string-returning capability -listed on the `terminfo man page`_ by its name under the "Cap-name" column. +This is a flaw in the original `erikrose/blessings`_ implementation, kept +for compatibility. -.. _`terminfo(5)`: http://www.openbsd.org/cgi-bin/man.cgi?query=terminfo&apropos=0&sektion=5 - One-Notch Movement ~~~~~~~~~~~~~~~~~~ @@ -297,9 +316,8 @@ cursor one character in various directions: * ``move_up`` * ``move_down`` -For example... :: - - print(term.move_up + 'Howdy!') +**NOTE**: ``move_down`` is often valued as *\\n*, which returns the +carriage to column (0). Height And Width ---------------- @@ -309,11 +327,25 @@ It's simple to get the height and width of the terminal, in characters:: from blessed import Terminal term = Terminal() - height = term.height - width = term.width + height, width = term.height, term.width + with term.location(x=term.width / 3, y=term.height / 3): + print('1/3 ways in!') + +These are always current, so a callback that refreshes the screen accordingly +as it is resized is possible:: + + import signal + from blessed import Terminal + + term = Terminal() + + def on_resize(sig, action): + print('height={t.height}, width={t.width}'.format(t=term)) + + signal.signal(signal.SIGWINCH, on_resize) + + term.inkey() -These are newly updated each time you ask for them, so they're safe to use from -SIGWINCH handlers. Clearing The Screen ------------------- @@ -333,13 +365,13 @@ Full-Screen Mode ---------------- If you've ever noticed a program, such as an editor, restores the previous -screen state (Your shell prompt) after exiting, you're seeing the +screen (such as your shell prompt) after exiting, you're seeing the ``enter_fullscreen`` and ``exit_fullscreen`` attributes in effect. ``enter_fullscreen`` Switch to alternate screen, previous screen is stored by terminal driver. ``exit_fullscreen`` - Switch back to standard screen, restoring the same termnal state. + Switch back to standard screen, restoring the same terminal state. There's also a context manager you can use as a shortcut:: @@ -356,13 +388,13 @@ Pipe Savvy If your program isn't attached to a terminal, such as piped to a program like ``less(1)`` or redirected to a file, all the capability attributes on -``Terminal`` will return empty strings. You'll get a nice-looking file without +*Terminal* will return empty strings. You'll get a nice-looking file without any formatting codes gumming up the works. If you want to override this, such as using ``less -r``, pass argument -``force_styling=True`` to the ``Terminal`` constructor. +``force_styling=True`` to the *Terminal* constructor. -In any case, there is a ``does_styling`` attribute on ``Terminal`` that lets +In any case, there is a ``does_styling`` attribute on *Terminal* that lets you see whether the terminal attached to the output stream is capable of formatting. If it is ``False``, you may refrain from drawing progress bars and other frippery and just stick to content:: @@ -417,7 +449,7 @@ You may also have noticed that special keys, such as arrow keys, actually input several byte characters, and different terminals send different strings. Finally, you may have noticed characters such as ä from ``raw_input`` are also -several byte characters in a sequence ('\xc3\xa4') that must be decoded. +several byte characters in a sequence ('\\xc3\\xa4') that must be decoded. Handling all of these possibilities can be quite difficult, but Blessed has you covered! @@ -441,7 +473,7 @@ raw ~~~ The context manager ``raw`` is the same as ``cbreak``, except interrupt (^C), -quit (^\), suspend (^Z), and flow control (^S, ^Q) characters are not trapped +quit (^\\), suspend (^Z), and flow control (^S, ^Q) characters are not trapped by signal handlers, but instead sent directly. This is necessary if you actually want to handle the receipt of Ctrl+C @@ -486,17 +518,17 @@ Its output might appear as:: got q. bye! -.. _`cbreak(3)`: www.openbsd.org/cgi-bin/man.cgi?query=cbreak&apropos=0&sektion=3 -.. _`termios(4)`: www.openbsd.org/cgi-bin/man.cgi?query=termios&apropos=0&sektion=4 +keyboard codes +~~~~~~~~~~~~~~ +The return value of the *Terminal* method ``inkey`` may be inspected for ts property +``is_sequence``. When ``True``, it means the value is a *multibyte sequence*, +representing an application key of your terminal. -codes -~~~~~ +The ``code`` property (int) may then be compared with any of the following +attributes of the *Terminal* instance, which are equivalent to the same +available in `curs_getch(3)_`, with the following exceptions: -The return value of ``inkey`` can be inspected for property ``is_sequence``. -When ``True``, the ``code`` property (int) may be compared with any of the -following attributes available on the associated Terminal, which are equivalent -to the same available in curs_getch(3X), with the following exceptions * use ``KEY_DELETE`` instead of ``KEY_DC`` (chr(127)) * use ``KEY_INSERT`` instead of ``KEY_IC`` * use ``KEY_PGUP`` instead of ``KEY_PPAGE`` @@ -515,33 +547,36 @@ Additionally, use any of the following common attributes: * ``KEY_HOME``, ``KEY_END``. * ``KEY_F1`` through ``KEY_F22``. -And much more. All attributes begin with prefix ``KEY_``. Shopping List ============= There are decades of legacy tied up in terminal interaction, so attention to detail and behavior in edge cases make a difference. Here are some ways -Blessed has your back: +*Blessed* has your back: * Uses the `terminfo(5)`_ database so it works with any terminal type * Provides up-to-the-moment terminal height and width, so you can respond to - terminal size changes (SIGWINCH signals). (Most other libraries query the + terminal size changes (*SIGWINCH* signals). (Most other libraries query the ``COLUMNS`` and ``LINES`` environment variables or the ``cols`` or ``lines`` terminal capabilities, which don't update promptly, if at all.) * Avoids making a mess if the output gets piped to a non-terminal. * Works great with standard Python string formatting. * Provides convenient access to **all** terminal capabilities. -* Outputs to any file-like object (StringIO, file), not just stdout. +* Outputs to any file-like object (*StringIO*, file), not just *stdout*. * Keeps a minimum of internal state, so you can feel free to mix and match with calls to curses or whatever other terminal libraries you like +* Safely decodes internationalization keyboard input to their unicode equivalents. +* Safely decodes multibyte sequences for application/arrow keys. +* Allows the printable length of strings containing sequences to be determined. +* Provides plenty of context managers to safely express various terminal modes, + restoring to a safe state upon exit. Blessed does not provide... * Native color support on the Windows command prompt. However, it should work - when used in concert with colorama_. + when used in concert with colorama_. Patches welcome! -.. _colorama: http://pypi.python.org/pypi/colorama/0.2.4 Bugs ==== @@ -552,6 +587,9 @@ Bugs or suggestions? Visit the `issue tracker`_. .. image:: https://secure.travis-ci.org/jquast/blessed.png +For patches, please construct a test case if possible. To test, +install and execute python package command ``tox``. + License ======= @@ -559,42 +597,51 @@ License Blessed is derived from Blessings, which is under the MIT License, and shares the same. See the LICENSE file. + Version History =============== 1.7 * Forked github project `erikrose/blessings`_ to `jquast/blessed`_, this project was previously known as **blessings** version 1.6 and prior. - * introduced context manager ``cbreak`` and ``raw``, which is equivalent + * introduced: context manager ``cbreak`` and ``raw``, which is equivalent to ``tty.setcbreak`` and ``tty.setraw``, allowing input from stdin to be read as each key is pressed. - * introduced ``inkey()``, which will return 1 or more characters as + * introduced: ``inkey()``, which will return 1 or more characters as a unicode sequence, with attributes ``.code`` and ``.name`` non-None when a multibyte sequence is received, allowing arrow keys and such to be detected. Optional value ``timeout`` allows timed polling or blocking. - * introduced ``center()``, ``rjust()``, and ``ljust()`` methods, allows text + * introduced: ``center()``, ``rjust()``, and ``ljust()`` methods, allows text containing sequences to be aligned to screen, or ``width`` specified. - * introduced ``wrap()``, allows text containing sequences to be + * introduced: ``wrap()``, allows text containing sequences to be word-wrapped without breaking mid-sequence and honoring their printable width. + * bugfix: cannot call ``setupterm()`` more than once per process -- issue a warning about what terminal kind subsequent calls will use. - * bugfix: resolved issue where ``number_of_colors`` fails when ``does_styling`` - is ``False``. resolves issue where piping tests output to stdout would fail. + * bugfix: resolved issue where ``number_of_colors`` fails when + ``does_styling`` is ``False``. resolves issue where piping tests + output would fail. * bugfix: warn and set ``does_styling`` to ``False`` when TERM is unknown. * bugfix: allow unsupported terminal capabilities to be callable just as supported capabilities, so that the return value of ``term.color(n)`` may be called on terminals without color capabilities. - * bugfix: for terminals without underline, such as vt220, ``term.underline('x')`` - would be ``u'x' + term.normal``, now it is only ``u'x'``. - * attributes that should be read-only have now raise exception when - re-assigned (properties). - * enhancement: pypy is not a supported platform implementation. + * bugfix: for terminals without underline, such as vt220, + ``term.underline('text')``. would be ``u'text' + term.normal``, now is + only ``u'text'``. + + * enhancement: move_x and move_y now work inside screen(1) or tmux(1). + * enhancement: some attributes are now properties, raise exceptions when + assigned. + * enhancement: pypy is not a supported python platform implementation. * enhancement: removed pokemon ``curses.error`` exceptions. - * enhancement: converted nosetests to pytest, install and use ``tox`` for testing. - * enhancement: pytext fixtures, paired with a new ``@as_subprocess`` decorator + * enhancement: converted nose tests to pytest, merged travis and tox. + * enhancement: pytest fixtures, paired with a new ``@as_subprocess`` + decorator are used to test a multitude of terminal types. - * introduced ``@as_subprocess`` to discover and resolve various issues. + * enhancement: test accessories ``@as_subprocess`` resolves various issues + with different terminal types that previously went untested. + * deprecation: python2.5 is no longer supported (as tox does not supported). 1.6 @@ -618,7 +665,7 @@ Version History * Add syntactic sugar and documentation for ``enter_fullscreen`` and ``exit_fullscreen``. * Add context managers ``fullscreen()`` and ``hidden_cursor()``. - * Now you can force a ``Terminal`` never to emit styles by passing + * Now you can force a *Terminal* never to emit styles by passing ``force_styling=None``. 1.4 @@ -676,3 +723,10 @@ Version History .. _`progress-bar-having, traceback-shortcutting, rootin', tootin' testrunner`: http://pypi.python.org/pypi/nose-progressive/ .. _`erikrose/blessings`: https://github.com/erikrose/blessings .. _`jquast/blessed`: https://github.com/jquast/blessed +.. _curses: http://docs.python.org/library/curses.html +.. _couleur: http://pypi.python.org/pypi/couleur +.. _`cbreak(3)`: www.openbsd.org/cgi-bin/man.cgi?query=cbreak&apropos=0&sektion=3 +.. _`curs_getch(3)`: http://www.openbsd.org/cgi-bin/man.cgi?query=curs_getch&apropos=0&sektion=3 +.. _`termios(4)`: http://www.openbsd.org/cgi-bin/man.cgi?query=termios&apropos=0&sektion=4 +.. _`terminfo(5)`: http://www.openbsd.org/cgi-bin/man.cgi?query=terminfo&apropos=0&sektion=5 +.. _colorama: http://pypi.python.org/pypi/colorama/0.2.4 From 7274694d033725b89a44d9d34b528a8e64157265 Mon Sep 17 00:00:00 2001 From: jquast Date: Thu, 20 Mar 2014 00:03:57 -0700 Subject: [PATCH 065/459] support move_x and move_y on tmux(1) or screen(1) for some reason hpa/vpa is missing, though this sequence works fine. --- blessed/sequences.py | 7 +++++++ blessed/tests/test_core.py | 15 +++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/blessed/sequences.py b/blessed/sequences.py index 9e1d0c53..0a8fab25 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -87,6 +87,13 @@ def init_sequence_patterns(term): if term._kind in _BINTERM_UNSUPPORTED: warnings.warn(_BINTERM_UNSUPPORTED_MSG) + # for some reason, 'screen' does not offer hpa and vpa, + # although they function perfectly fine ! + if term._kind == 'screen' and not self.hpa: + term.hpa = lambda col: u'\x1b[{}G'.format(col + 1) + if term._kind == 'screen' and not self.vpa: + term.vpa = lambda col: u'\x1b[{}d'.format(col + 1) + bnc = functools.partial(_build_numeric_capability, term=term) bna = functools.partial(_build_any_numeric_capability, term=term) # Build will_move, a list of terminal capabilities that have diff --git a/blessed/tests/test_core.py b/blessed/tests/test_core.py index 3af21370..73dc9ee2 100644 --- a/blessed/tests/test_core.py +++ b/blessed/tests/test_core.py @@ -34,6 +34,21 @@ def child(kind): child(all_terms) +def test_flipped_location_move(all_terms): + """``location()`` and ``move()`` receive counter-example arguments.""" + @as_subprocess + def child(kind): + buffer = StringIO() + t = TestTerminal(stream=buf, force_styling=True) + y, x = 10, 20 + with term.location(y, x) + xy_val = t.move(x, y) + yx_val = buf.getvalue()[len(term.sc):] + assert xy_val == yx_val + + child(all_terms) + + def test_null_fileno(): """Make sure ``Terminal`` works when ``fileno`` is ``None``.""" @as_subprocess From ad40093d647b4e1235e3f922f4f7398acf02ab6a Mon Sep 17 00:00:00 2001 From: jquast Date: Thu, 20 Mar 2014 00:04:48 -0700 Subject: [PATCH 066/459] support InterruptedException for SIGWINCH handlers in inkey() --- bin/on_resize.py | 12 ++++++++++++ blessed/terminal.py | 44 +++++++++++++++++++++++++++++++++++++------- 2 files changed, 49 insertions(+), 7 deletions(-) create mode 100755 bin/on_resize.py diff --git a/bin/on_resize.py b/bin/on_resize.py new file mode 100755 index 00000000..aaec9e93 --- /dev/null +++ b/bin/on_resize.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python +import signal +from blessed import Terminal + +term = Terminal() + +def on_resize(sig, action): + print('height={t.height}, width={t.width}'.format(t=term)) + +signal.signal(signal.SIGWINCH, on_resize) + +term.inkey() diff --git a/blessed/terminal.py b/blessed/terminal.py index 87ec69d9..1c96bd82 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -22,6 +22,13 @@ class IOUnsupportedOperation(Exception): """A dummy exception to take the place of Python 3's ``io.UnsupportedOperation`` in Python 2.5""" +try: + _ = InterruptedError + del _ +except NameError: + # alias py2 exception to py3 + InterruptedError = select.error + # local imports import formatters import sequences @@ -73,11 +80,12 @@ def __init__(self, kind=None, stream=None, force_styling=False): """ global _CUR_TERM + self.stream_kb = None + + # default stream is stdout, keyboard only valid as stdin with stdout. if stream is None: stream = sys.__stdout__ self.stream_kb = sys.__stdin__.fileno() - else: - self.stream_kb = None try: stream_fd = (stream.fileno() if hasattr(stream, 'fileno') @@ -90,14 +98,13 @@ def __init__(self, kind=None, stream=None, force_styling=False): force_styling is not None) self._normal = None # cache normal attr, preventing recursive lookups - # keyboard input only valid when stream is sys.stdout - - # The desciptor to direct terminal initialization sequences to. + # The descriptor to direct terminal initialization sequences to. # sys.__stdout__ seems to always have a descriptor of 1, even if output # is redirected. self._init_descriptor = (stream_fd is None and sys.__stdout__.fileno() or stream_fd) self._kind = kind or os.environ.get('TERM', 'unknown') + if self.does_styling: # Make things like tigetstr() work. Explicit args make setupterm() # work even when -s is passed to nosetests. Lean toward sending @@ -489,9 +496,32 @@ def kbhit(self, timeout=0): if self.keyboard_fd is None: return False + # Special care is taken to handle a custom SIGWINCH handler, which + # causes select() to be interrupted with errno 4 -- it is ignored, + # and a new timeout value is derived from the previous, unless timeout + # becomes negative, because signal handler has blocked beyond timeout, + # then False is returned. Otherwise, when timeout is 0, we continue to + # block indefinitely (default). + stime = time.time() check_r, check_w, check_x = [self.stream_kb], [], [] - ready_r, ready_w, ready_x = select.select( - check_r, check_w, check_x, timeout) + + while True: + try: + ready_r, ready_w, ready_x = select.select( + check_r, check_w, check_x, timeout) + except InterruptedError as exc: + if 4 == (hasattr(exc, 'errno') and exc.errno or # py2 + hasattr(exc, 'args') and exc.args[0]): # py3 + if timeout != 0: + timeout = time.time() - stime + if timeout > 0: + continue + else: + ready_r = False + break + raise + else: + break return check_r == ready_r From 71bfeff6f8e4c62eea4c8ddc8f7fe9c86ab4e337 Mon Sep 17 00:00:00 2001 From: jquast Date: Thu, 20 Mar 2014 00:05:13 -0700 Subject: [PATCH 067/459] tests for hpa/etc. for tmux/screen injection --- blessed/tests/test_sequences.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index 341611fc..bafb075b 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -165,6 +165,38 @@ def child(): child() +def test_inject_move_x_for_screen(): + """Test injection of hpa attribute for screen (issue #55).""" + @as_subprocess + def child(kind): + t = TestTerminal(kind=kind, stream=StringIO(), force_styling=True) + with t.location(x=5): + pass + expected_output = u''.join( + (unicode_cap('sc'), + unicode_parm('hpa', 5), + unicode_cap('rc'))) + assert (t.stream.getvalue() == expected_output == t.move_x(5)) + + child('screen') + + +def test_inject_move_y_for_screen(): + """Test injection of vpa attribute for screen (issue #55).""" + @as_subprocess + def child(kind): + t = TestTerminal(kind=kind, stream=StringIO(), force_styling=True) + with t.location(y=5): + pass + expected_output = u''.join( + (unicode_cap('sc'), + unicode_parm('vpa', 5), + unicode_cap('rc'))) + assert (t.stream.getvalue() == expected_output == t.move_y(5)) + + child('screen') + + def test_zero_location(): """Make sure ``location()`` pays attention to 0-valued args.""" @as_subprocess From b228563d99d5148e7dfa109d6e5664284ba22be6 Mon Sep 17 00:00:00 2001 From: jquast Date: Thu, 20 Mar 2014 00:06:41 -0700 Subject: [PATCH 068/459] resolve various issues and tests --- bin/dumb_fse.py | 59 ++++++++++++++++++++++++++++++++++++++ blessed/sequences.py | 4 +-- blessed/tests/test_core.py | 6 ++-- 3 files changed, 64 insertions(+), 5 deletions(-) create mode 100755 bin/dumb_fse.py diff --git a/bin/dumb_fse.py b/bin/dumb_fse.py new file mode 100755 index 00000000..297c60ae --- /dev/null +++ b/bin/dumb_fse.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python +# Dumb full-screen editor. It doesn't save anything but to the screen. +# +# "Why wont python let me read memory +# from screen like assembler? That's dumb." -hellbeard +from __future__ import division +import collections +import functools +from blessed import Terminal + +echo_xy = lambda cursor, text: functools.partial( + print, end='', flush=True)(cursor.term.move(cursor.y, cursor.x) + text) + +Cursor = collections.namedtuple('Point', ('y', 'x', 'term')) + +above = lambda b, n: Cursor( + max(0, b.y - n), b.x, b.term) +below = lambda b, n: Cursor( + min(b.term.height - 1, b.y + n), b.x, b.term) +right_of = lambda b, n: Cursor( + b.y, min(b.term.width - 1, b.x + n), b.term) +left_of = lambda b, n: Cursor( + b.y, max(0, b.x - n), b.term) +home = lambda b: Cursor( + b.y, 1, b.term) + +lookup_move = lambda inp_code, b: { + # arrows + b.term.KEY_LEFT: left_of(b, 1), + b.term.KEY_RIGHT: right_of(b, 1), + b.term.KEY_DOWN: below(b, 1), + b.term.KEY_UP: above(b, 1), + # shift + arrows + b.term.KEY_SLEFT: left_of(b, 10), + b.term.KEY_SRIGHT: right_of(b, 10), + b.term.KEY_SDOWN: below(b, 10), + b.term.KEY_SUP: above(b, 10), + # carriage return + b.term.KEY_ENTER: home(below(b, 1)), + b.term.KEY_HOME: home(b), +}.get(inp_code, b) + +term = Terminal() +csr = Cursor(1, 1, term) +with term.hidden_cursor(), term.raw(), term.location(), term.fullscreen(): + inp = None + while True: + echo_xy(csr, term.reverse(u' ')) + inp = term.inkey() + if inp.code == term.KEY_ESCAPE or inp == chr(3): + break + echo_xy(csr, u' ') + n_csr = lookup_move(inp.code, csr) + if n_csr != csr: + echo_xy(n_csr, u' ') + csr = n_csr + elif not inp.is_sequence: + echo_xy(csr, inp) + csr = right_of(csr, 1) diff --git a/blessed/sequences.py b/blessed/sequences.py index 0a8fab25..053a4c14 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -89,9 +89,9 @@ def init_sequence_patterns(term): # for some reason, 'screen' does not offer hpa and vpa, # although they function perfectly fine ! - if term._kind == 'screen' and not self.hpa: + if term._kind == 'screen' and not term.hpa: term.hpa = lambda col: u'\x1b[{}G'.format(col + 1) - if term._kind == 'screen' and not self.vpa: + if term._kind == 'screen' and not term.vpa: term.vpa = lambda col: u'\x1b[{}d'.format(col + 1) bnc = functools.partial(_build_numeric_capability, term=term) diff --git a/blessed/tests/test_core.py b/blessed/tests/test_core.py index 73dc9ee2..6c9ea4c1 100644 --- a/blessed/tests/test_core.py +++ b/blessed/tests/test_core.py @@ -38,12 +38,12 @@ def test_flipped_location_move(all_terms): """``location()`` and ``move()`` receive counter-example arguments.""" @as_subprocess def child(kind): - buffer = StringIO() + buf = StringIO() t = TestTerminal(stream=buf, force_styling=True) y, x = 10, 20 - with term.location(y, x) + with t.location(y, x): xy_val = t.move(x, y) - yx_val = buf.getvalue()[len(term.sc):] + yx_val = buf.getvalue()[len(t.sc):] assert xy_val == yx_val child(all_terms) From 99437a62b8db053632ee26a2d679f72460b0c26d Mon Sep 17 00:00:00 2001 From: jquast Date: Thu, 20 Mar 2014 00:36:19 -0700 Subject: [PATCH 069/459] disable hpa injection for screen, some strange issue --- README.rst | 1 - blessed/formatters.py | 1 + blessed/sequences.py | 25 +++++++++++++++---------- blessed/tests/test_sequences.py | 28 +++++++++++++--------------- 4 files changed, 29 insertions(+), 26 deletions(-) diff --git a/README.rst b/README.rst index 82280b84..f0ad45ef 100644 --- a/README.rst +++ b/README.rst @@ -630,7 +630,6 @@ Version History ``term.underline('text')``. would be ``u'text' + term.normal``, now is only ``u'text'``. - * enhancement: move_x and move_y now work inside screen(1) or tmux(1). * enhancement: some attributes are now properties, raise exceptions when assigned. * enhancement: pypy is not a supported python platform implementation. diff --git a/blessed/formatters.py b/blessed/formatters.py index a07ad180..489659af 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -117,6 +117,7 @@ def __call__(self, *args): # unicodes? No. How would I know what encoding to use # to convert it? + def split_compound(compound): """Split a possibly compound format string into segments. diff --git a/blessed/sequences.py b/blessed/sequences.py index 053a4c14..fddc329c 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -33,7 +33,8 @@ def _build_numeric_capability(term, cap, optional=False, _cap = getattr(term, cap) opt = '?' if optional else '' if _cap: - cap_re = re.escape(_cap(*((base_num,) * nparams))) + args = (base_num,) * nparams + cap_re = re.escape(_cap(*args)) for num in range(base_num-1, base_num+2): # search for matching ascii, n-1 through n+2 if str(num) in cap_re: @@ -87,15 +88,19 @@ def init_sequence_patterns(term): if term._kind in _BINTERM_UNSUPPORTED: warnings.warn(_BINTERM_UNSUPPORTED_MSG) - # for some reason, 'screen' does not offer hpa and vpa, - # although they function perfectly fine ! - if term._kind == 'screen' and not term.hpa: - term.hpa = lambda col: u'\x1b[{}G'.format(col + 1) - if term._kind == 'screen' and not term.vpa: - term.vpa = lambda col: u'\x1b[{}d'.format(col + 1) - - bnc = functools.partial(_build_numeric_capability, term=term) - bna = functools.partial(_build_any_numeric_capability, term=term) +# # for some reason, 'screen' does not offer hpa and vpa, +# # although they function perfectly fine ! +# if term._kind == 'screen' and term.hpa == u'': +# def screen_hpa(*args): +# return u'\x1b[{}G'.format(len(args) and args[0] + 1 or 1) +# term.hpa = screen_hpa +# if term._kind == 'screen' and term.vpa == u'': +# def screen_vpa(*args): +# return u'\x1b[{}d'.format(len(args) and args[0] + 1 or 1) +# term.vpa = screen_vpa + + bnc = functools.partial(_build_numeric_capability, term) + bna = functools.partial(_build_any_numeric_capability, term) # Build will_move, a list of terminal capabilities that have # indeterminate effects on the terminal cursor position. will_move_seqs = set([ diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index bafb075b..7d3887da 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -120,11 +120,11 @@ def test_merge_sequences(): assert (_merge_sequences(input_list) == output_expected) -def test_location(): +def test_location(all_standard_terms): """Make sure ``location()`` does what it claims.""" @as_subprocess def child_with_styling(): - t = TestTerminal(stream=StringIO(), force_styling=True) + t = TestTerminal(kind=kind, stream=StringIO(), force_styling=True) with t.location(3, 4): t.stream.write(u'hi') expected_output = u''.join( @@ -133,7 +133,7 @@ def child_with_styling(): u'hi', unicode_cap('rc'))) assert (t.stream.getvalue() == expected_output) - child_with_styling() + child_with_styling(all_standard_terms) @as_subprocess def child_without_styling(): @@ -149,11 +149,11 @@ def child_without_styling(): child_without_styling() -def test_horizontal_location(): +def test_horizontal_location(all_standard_terms): """Make sure we can move the cursor horizontally without changing rows.""" @as_subprocess - def child(): - t = TestTerminal(stream=StringIO(), force_styling=True) + def child(kind): + t = TestTerminal(kind=kind, stream=StringIO(), force_styling=True) with t.location(x=5): pass expected_output = u''.join( @@ -162,7 +162,7 @@ def child(): unicode_cap('rc'))) assert (t.stream.getvalue() == expected_output) - child() + child(all_standard_terms) def test_inject_move_x_for_screen(): @@ -173,10 +173,9 @@ def child(kind): with t.location(x=5): pass expected_output = u''.join( - (unicode_cap('sc'), - unicode_parm('hpa', 5), + (unicode_cap('sc'), t.hpa(5), unicode_cap('rc'))) - assert (t.stream.getvalue() == expected_output == t.move_x(5)) + assert (t.stream.getvalue() == expected_output) child('screen') @@ -189,15 +188,14 @@ def child(kind): with t.location(y=5): pass expected_output = u''.join( - (unicode_cap('sc'), - unicode_parm('vpa', 5), + (unicode_cap('sc'), t.vpa(5), unicode_cap('rc'))) - assert (t.stream.getvalue() == expected_output == t.move_y(5)) + assert (t.stream.getvalue() == expected_output) child('screen') -def test_zero_location(): +def test_zero_location(all_standard_terms): """Make sure ``location()`` pays attention to 0-valued args.""" @as_subprocess def child(): @@ -210,7 +208,7 @@ def child(): unicode_cap('rc'))) assert (t.stream.getvalue() == expected_output) - child() + child(all_standard_terms) def test_mnemonic_colors(all_standard_terms): From e737c3216a20d2ad10df22a0e902c889319e2c9a Mon Sep 17 00:00:00 2001 From: jquast Date: Thu, 20 Mar 2014 00:43:27 -0700 Subject: [PATCH 070/459] resolve tests, add bin/tprint.py --- bin/tprint.py | 19 +++++++++++++++++++ blessed/tests/accessories.py | 12 ++++++++++-- blessed/tests/test_sequences.py | 14 ++++++++------ 3 files changed, 37 insertions(+), 8 deletions(-) create mode 100755 bin/tprint.py diff --git a/bin/tprint.py b/bin/tprint.py new file mode 100755 index 00000000..f46427de --- /dev/null +++ b/bin/tprint.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python + +import argparse +from blessed import Terminal + +parser = argparse.ArgumentParser( + description='displays argument as specified style') + +parser.add_argument('style', type=str, help='style formatter') +parser.add_argument('text', type=str, nargs='+') + + +term = Terminal() +args = parser.parse_args() + +style = getattr(term, args.style) + +print(style(' '.join(args.text))) + diff --git a/blessed/tests/accessories.py b/blessed/tests/accessories.py index 2209405e..67c2dfd4 100644 --- a/blessed/tests/accessories.py +++ b/blessed/tests/accessories.py @@ -156,12 +156,20 @@ def echo_off(fd): def unicode_cap(cap): """Return the result of ``tigetstr`` except as Unicode.""" - return curses.tigetstr(cap).decode('latin1') + val = curses.tigetstr(cap) + if val: + return val.decode('latin1') + return u'' def unicode_parm(cap, *parms): """Return the result of ``tparm(tigetstr())`` except as Unicode.""" - return curses.tparm(curses.tigetstr(cap), *parms).decode('latin1') + cap = curses.tigetstr(cap) + if cap: + val = curses.tparm(cap, *parms) + if val: + return val.decode('latin1') + return u'' @pytest.fixture(params=binpacked_terminal_params) diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index 7d3887da..e2aebe23 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -120,10 +120,10 @@ def test_merge_sequences(): assert (_merge_sequences(input_list) == output_expected) -def test_location(all_standard_terms): - """Make sure ``location()`` does what it claims.""" +def test_location_with_styling(all_standard_terms): + """Make sure ``location()`` works on all terminals.""" @as_subprocess - def child_with_styling(): + def child_with_styling(kind): t = TestTerminal(kind=kind, stream=StringIO(), force_styling=True) with t.location(3, 4): t.stream.write(u'hi') @@ -135,6 +135,9 @@ def child_with_styling(): child_with_styling(all_standard_terms) + +def test_location_without_styling(): + """Make sure ``location()`` silently passes without styling.""" @as_subprocess def child_without_styling(): """No side effect for location as a context manager without styling.""" @@ -145,7 +148,6 @@ def child_without_styling(): assert t.stream.getvalue() == u'hi' - child_with_styling() child_without_styling() @@ -198,8 +200,8 @@ def child(kind): def test_zero_location(all_standard_terms): """Make sure ``location()`` pays attention to 0-valued args.""" @as_subprocess - def child(): - t = TestTerminal(stream=StringIO(), force_styling=True) + def child(kind): + t = TestTerminal(kind=kind, stream=StringIO(), force_styling=True) with t.location(0, 0): pass expected_output = u''.join( From 590e676a0616dc2976579f9d176db45032381291 Mon Sep 17 00:00:00 2001 From: jquast Date: Thu, 20 Mar 2014 10:34:18 -0700 Subject: [PATCH 071/459] remove redundant .format(t=t) doc --- README.rst | 5 ----- 1 file changed, 5 deletions(-) diff --git a/README.rst b/README.rst index f0ad45ef..fd1b5a88 100644 --- a/README.rst +++ b/README.rst @@ -17,11 +17,6 @@ Coding with *Blessed* looks like this... :: with t.cbreak(): t.inkey() -Or, for byte-level control, you can drop down and play with raw terminal -capabilities:: - - print('{t.bold}All your {t.red}bold and red base{t.normal}'.format(t=t)) - The Pitch ========= From 900e3b6065f5c228fa5dea17acbf467d504781ad Mon Sep 17 00:00:00 2001 From: jquast Date: Thu, 20 Mar 2014 10:56:40 -0700 Subject: [PATCH 072/459] more doc polishing --- README.rst | 109 ++++++++++++++++++++++++++++------------------------- 1 file changed, 58 insertions(+), 51 deletions(-) diff --git a/README.rst b/README.rst index fd1b5a88..93041d04 100644 --- a/README.rst +++ b/README.rst @@ -27,7 +27,7 @@ The Pitch clearing the whole screen first. * Leave more than one screenful of scrollback in the buffer after your program exits, like a well-behaved command-line application should. -* No more C-like calls to ``tigetstr`` and ``tparm``. +* No more C-like calls to tigetstr_ and `tparm`_. * Act intelligently when somebody redirects your output to a file, omitting all of the terminal sequences such as styling, colors, or positioning. @@ -79,8 +79,11 @@ What It Provides Blessed provides just **one** top-level object: *Terminal*. Instantiating a *Terminal* figures out whether you're on a terminal at all and, if so, does -any necessary terminal setup. After that, you can proceed to ask it all sorts -of things about the terminal. +any necessary setup. After that, you can proceed to ask it all sorts of things +about the terminal, such as its size and color support, and use its styling +to construct strings containing color and styling. Also, the special sequences +inserted with application keys (arrow and function keys) are understood and +decoded, as well as your locale-specific encoded multibyte input. Simple Formatting @@ -94,16 +97,17 @@ on a *Terminal* class instance. For example:: term = Terminal() print('I am ' + term.bold + 'bold' + term.normal + '!') -These capabilities (bold, normal) are translated to their sequences, which -when displayed simply change the video attributes. +These capabilities (*bold*, *normal*) are translated to their sequences, which +when displayed simply change the video attributes. And, when used as a callable, +automatically wraps the given string with this sequence, and terminates it with +*normal*. -And, when used as a callable, automatically wraps the given string with this -sequence, and terminates it with *normal*. The same can be written as:: +The same can be written as:: print('I am' + term.bold('bold') + '!') You may also use the *Terminal* instance as an argument for ``.format`` string -method, so that capabilities can be displayed inline for more complex strings:: +method, so that capabilities can be displayed in-line for more complex strings:: print('{t.red_on_yellow}Candy corn{t.normal} for everyone!'.format(t=term)) @@ -116,7 +120,7 @@ The basic capabilities supported by most terminals are: The less commonly supported capabilities: -* ``dim``: Turn on 'half-bright' mode. +* ``dim``: Turn on *half-bright* mode. * ``underline`` and ``no_underline``. * ``italic`` and ``no_italic``. * ``shadow`` and ``no_shadow``. @@ -125,14 +129,15 @@ The less commonly supported capabilities: * ``superscript`` and ``no_superscript``. * ``flash``: Visual bell, which flashes the screen. -Note that, while the inverse of ``underline`` is ``no_underline``, the only way -to turn off ``bold`` or ``reverse`` is ``normal``, which also cancels any -custom colors. +Note that, while the inverse of *underline* is *no_underline*, the only way +to turn off *bold* or *reverse* is *normal*, which also cancels any custom +colors. -Many of these are aliases, their true capability names (such as ``smul`` for -*begin underline mode*) may still be used. Any capability in the `terminfo(5)_` +Many of these are aliases, their true capability names (such as *smul* for +*begin underline mode*) may still be used. Any capability in the `terminfo(5)`_ manual, under column **Cap-name**, may be used as an attribute to a *Terminal* -instance. If it is not a supported capability, an empty string is returned. +instance. If it is not a supported capability, or a non-tty is used as an +output stream, an empty string is returned. Color @@ -149,14 +154,12 @@ Color terminals are capable of at least 8 basic colors. * ``cyan`` * ``white`` -The same colors, prefixed with ``bright_`` (synonymous with ``bold_``), -such as ``bright_blue``, providing 16 colors in total (if you count black -as a color). On most terminals, ``bright_black`` is actually a very dim -gray! +The same colors, prefixed with *bright_* (synonymous with *bold_*), +such as *bright_blue*, provides 16 colors in total. -The same colors, prefixed with ``on_`` sets the background color, some +The same colors, prefixed with *on_* sets the background color, some terminals also provide an additional 8 high-intensity versions using -``on_bright``, some example compound formats:: +*on_bright*, some example compound formats:: from blessed import Terminal @@ -191,7 +194,7 @@ effect:: term = Terminal() - print('terminals {standout} more than others'.format( + print('some terminals {standout} more than others'.format( standout=term.green_reverse('standout'))) Which appears as *bright white on green* on color terminals, or *black text @@ -200,6 +203,8 @@ definition used supports colors, and how many, using the ``number_of_colors`` property, which returns any of *0* *8* or *256* for terminal types such as *vt220*, *ansi*, and *xterm-256color*, respectively. +**NOTE**: On most color terminals, *bright_black* is actually a very dark +shade of gray! Compound Formatting ------------------- @@ -215,8 +220,8 @@ all together:: I'd be remiss if I didn't credit couleur_, where I probably got the idea for all this mashing. This compound notation comes in handy if you want to allow -users to customize the formatting of your app: just have them pass in a format -specifier like **bold_green** as a command line argument or configuration item:: +users to customize formatting, just allow compound formatters, like *bold_green*, +as a command line argument or configuration item:: #!/usr/bin/env python import argparse @@ -244,8 +249,8 @@ Moving The Cursor ----------------- When you want to move the cursor, you have a few choices, the -``location(y=None, x=None)`` context manager, ``move(y, x)``, ``move_y``, - and ``move_x`` attributes. +``location(y=None, x=None)`` context manager, ``move(y, x)``, ``move_y(row)``, + and ``move_x(col)`` attributes. Moving Temporarily @@ -261,7 +266,7 @@ screen position and restore the previous position upon exit:: print('Here is the bottom.') print('This is back where I came from.') -Parameters to ``location()`` are **optional** ``x`` and/or ``y``:: +Parameters to *location()* are **optional** *x* and/or *y*:: with term.location(y=10): print('We changed just the row.') @@ -272,7 +277,7 @@ When omitted, it saves the cursor position and restore it upon exit:: print(term.move(1, 1) + 'Hi') print(term.move(9, 9) + 'Mom') -*NOTE*: ``location`` may not be nested, as only one location may be saved. +*NOTE*: calls to *location* may not be nested, as only one location may be saved. Moving Permanently @@ -293,11 +298,10 @@ this:: ``move_y`` Position the cursor at given vertical column. -*NOTE*: The ``location`` method receives arguments in form of *(x, y)*, -where the ``move`` argument receives arguments in form of *(y, x)*. - -This is a flaw in the original `erikrose/blessings`_ implementation, kept -for compatibility. +*NOTE*: The *location* method receives arguments in form of *(x, y)*, +where the *move* argument receives arguments in form of *(y, x)*. This is a +flaw in the original `erikrose/blessings`_ implementation, kept for +compatibility. One-Notch Movement @@ -311,13 +315,14 @@ cursor one character in various directions: * ``move_up`` * ``move_down`` -**NOTE**: ``move_down`` is often valued as *\\n*, which returns the -carriage to column (0). +**NOTE**: *move_down* is often valued as *\\n*, which additionally returns +the carriage to column 0, depending on your terminal emulator. + Height And Width ---------------- -It's simple to get the height and width of the terminal, in characters:: +Use the *height* and *width* properties of the *Terminal* class instance:: from blessed import Terminal @@ -326,9 +331,7 @@ It's simple to get the height and width of the terminal, in characters:: with term.location(x=term.width / 3, y=term.height / 3): print('1/3 ways in!') -These are always current, so a callback that refreshes the screen accordingly -as it is resized is possible:: - +These are always current, so they may be used with a callback from SIGWINCH_ signals.:: import signal from blessed import Terminal @@ -356,12 +359,13 @@ Blessed provides syntactic sugar over some screen-clearing capabilities: ``clear_eos`` Clear to the end of screen. + Full-Screen Mode ---------------- If you've ever noticed a program, such as an editor, restores the previous screen (such as your shell prompt) after exiting, you're seeing the -``enter_fullscreen`` and ``exit_fullscreen`` attributes in effect. +*enter_fullscreen* and *exit_fullscreen* attributes in effect. ``enter_fullscreen`` Switch to alternate screen, previous screen is stored by terminal driver. @@ -382,16 +386,16 @@ Pipe Savvy ---------- If your program isn't attached to a terminal, such as piped to a program -like ``less(1)`` or redirected to a file, all the capability attributes on +like *less(1)* or redirected to a file, all the capability attributes on *Terminal* will return empty strings. You'll get a nice-looking file without any formatting codes gumming up the works. -If you want to override this, such as using ``less -r``, pass argument -``force_styling=True`` to the *Terminal* constructor. +If you want to override this, such as when piping output to ``less -r``, pass +argument ``force_styling=True`` to the *Terminal* constructor. -In any case, there is a ``does_styling`` attribute on *Terminal* that lets +In any case, there is a *does_styling* attribute on *Terminal* that lets you see whether the terminal attached to the output stream is capable of -formatting. If it is ``False``, you may refrain from drawing progress +formatting. If it is *False*, you may refrain from drawing progress bars and other frippery and just stick to content:: from blessed import Terminal @@ -407,7 +411,7 @@ Sequence Awareness Blessed may measure the printable width of strings containing sequences, providing ``.center``, ``.ljust``, and ``.rjust`` methods, using the -terminal screen's width as the default ``width`` value:: +terminal screen's width as the default *width* value:: from blessed import Terminal @@ -452,8 +456,8 @@ you covered! cbreak ~~~~~~ -The context manager ``cbreak`` can be used to enter key-at-a-time mode. -Any keypress by the user is immediately value:: +The context manager ``cbreak`` can be used to enter *key-at-a-time* mode: Any +keypress by the user is immediately consumed by read calls:: from blessed import Terminal import sys @@ -476,7 +480,7 @@ inkey ~~~~~ The method ``inkey`` resolves many issues with terminal input by returning -a unicode-derived ``Keypress`` instance. Although its return value may be +a unicode-derived *Keypress* instance. Although its return value may be printed, joined with, or compared to other unicode strings, it also provides the special attributes ``is_sequence`` (bool), ``code`` (int), and ``name`` (str):: @@ -517,10 +521,10 @@ keyboard codes ~~~~~~~~~~~~~~ The return value of the *Terminal* method ``inkey`` may be inspected for ts property -``is_sequence``. When ``True``, it means the value is a *multibyte sequence*, +*is_sequence*. When *True*, it means the value is a *multibyte sequence*, representing an application key of your terminal. -The ``code`` property (int) may then be compared with any of the following +The *code* property (int) may then be compared with any of the following attributes of the *Terminal* instance, which are equivalent to the same available in `curs_getch(3)_`, with the following exceptions: @@ -724,3 +728,6 @@ Version History .. _`termios(4)`: http://www.openbsd.org/cgi-bin/man.cgi?query=termios&apropos=0&sektion=4 .. _`terminfo(5)`: http://www.openbsd.org/cgi-bin/man.cgi?query=terminfo&apropos=0&sektion=5 .. _colorama: http://pypi.python.org/pypi/colorama/0.2.4 +.. _tigetstr: http://www.openbsd.org/cgi-bin/man.cgi?query=tigetstr&sektion=3 +.. _tparm: http://www.openbsd.org/cgi-bin/man.cgi?query=tparm&sektion=3 +.. _SIGWINCH: https://en.wikipedia.org/wiki/SIGWINCH#SIGWINCH From 91c5624ce65063796fe0fcb30cef4851875e39b2 Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 22 Mar 2014 11:21:51 -0700 Subject: [PATCH 073/459] coverage configuration and tryout coveralls.io --- .coveragerc | 8 ++++++++ .gitignore | 1 + .travis.yml | 22 ++++++++++++++++++---- README.rst | 22 +++++++++------------- tox.ini | 16 +++++++++------- 5 files changed, 45 insertions(+), 24 deletions(-) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..e97c6b4b --- /dev/null +++ b/.coveragerc @@ -0,0 +1,8 @@ +[run] +branch = True +source = blessed + +[report] +omit = */tests/* +include = '*/site-packages/blessed/*' +omit = '*/site-packages/blessed/tests/*' diff --git a/.gitignore b/.gitignore index 2e9b7023..61573956 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ build dist docs/_build htmlcov +.coveralls.yml diff --git a/.travis.yml b/.travis.yml index 0ed34089..5901b405 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,10 +6,24 @@ env: - TOXENV=py33 - TOXENV=pypy +install: + # travis wants requirements.txt; we don't, + # use only 'requires=' in setup.py, which is dynamic; + # for python versions <26, we must install ordereddict + - if [[ $TRAVIS_PYTHON_VERSION == 2.5 ]] || [[ $TRAVIS_PYTHON_VERSION == 2.6 ]]; then pip install --use-mirrors ordereddict; fi + - pip install -q --use-mirrors tox + script: + - cd .tox + - coverage erase --rcfile=$TRAVIS_BUILD_DIR/.coveragerc + - cd $TRAVIS_BUILD_DIR - tox -install: - # travis wants requirements.txt; we don't, use only 'requires=' in setup.py. - - if [[ $TRAVIS_PYTHON_VERSION == 2.5 ]] || [[ $TRAVIS_PYTHON_VERSION == 2.6 ]]; then pip install --use-mirrors ordereddict; fi - - pip install --use-mirrors tox +after_success: + - cd .tox + - coverage combine --rcfile=$TRAVIS_BUILD_DIR/.coveragerc + - coveralls --rcfile=$TRAVIS_BUILD_DIR/.coveragerc + +notifications: + email: + - contact@jeffquast.com diff --git a/README.rst b/README.rst index 93041d04..297be308 100644 --- a/README.rst +++ b/README.rst @@ -440,18 +440,13 @@ from Tao Te Ching, word-wrapped to 25 columns:: Keyboard Input -------------- -You may have noticed that the built-in python ``raw_input`` doesn't return -until the return key is pressed (line buffering). Special `termios(4)`_ routines -are required to enter Non-canonical, known in curses as `cbreak(3)_`. +The built-in python *raw_input* function does not return a value until the return +key is pressed, and is not suitable for detecting each individual keypress, much +less arrow or function keys that emit multibyte sequences. Special `termios(4)`_ +routines are required to enter Non-canonical, known in curses as `cbreak(3)_`. +These functions also receive bytes, which must be incrementally decoded to unicode. -You may also have noticed that special keys, such as arrow keys, actually -input several byte characters, and different terminals send different strings. - -Finally, you may have noticed characters such as ä from ``raw_input`` are also -several byte characters in a sequence ('\\xc3\\xa4') that must be decoded. - -Handling all of these possibilities can be quite difficult, but Blessed has -you covered! +Blessed handles all of these special keyboarding purposes! cbreak ~~~~~~ @@ -584,10 +579,11 @@ Bugs or suggestions? Visit the `issue tracker`_. .. _`issue tracker`: https://github.com/jquast/blessed/issues/ -.. image:: https://secure.travis-ci.org/jquast/blessed.png +.. image:: https://secure.travis-ci.org/jquast/blessed.png :target: https://travis-ci.org/jquast/blessed +.. image:: http://coveralls.io/repos/jquast/blessed/badge.png :target: http://coveralls.io/r/jquast/blessed For patches, please construct a test case if possible. To test, -install and execute python package command ``tox``. +install and execute python package command *tox*. License diff --git a/tox.ini b/tox.ini index 1b3de780..e4d060b0 100644 --- a/tox.ini +++ b/tox.ini @@ -5,15 +5,17 @@ envlist = py26, pypy [testenv] -changedir = {toxworkdir}/{envname} -deps = pytest +changedir = {toxworkdir} +deps = coverage + coveralls + pytest pytest-pep8 pytest-flakes pytest-cov mock -commands = py.test -x --strict --pep8 --flakes \ - --cov {envsitepackagesdir}/blessed \ - --cov-report html \ - --cov-report term \ - {envsitepackagesdir}/blessed/tests +commands = {envbindir}/coverage run -p {envbindir}/py.test \ + -x --strict --pep8 --flakes \ + --cov {envsitepackagesdir}/blessed \ + {envsitepackagesdir}/blessed/tests \ + {posargs} From 6bff7ed7cb2f92035079b01451488865f0ea6e2b Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 22 Mar 2014 11:26:06 -0700 Subject: [PATCH 074/459] try fixing up :image :target: and add :alt: --- README.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 297be308..68c4c980 100644 --- a/README.rst +++ b/README.rst @@ -579,8 +579,12 @@ Bugs or suggestions? Visit the `issue tracker`_. .. _`issue tracker`: https://github.com/jquast/blessed/issues/ -.. image:: https://secure.travis-ci.org/jquast/blessed.png :target: https://travis-ci.org/jquast/blessed -.. image:: http://coveralls.io/repos/jquast/blessed/badge.png :target: http://coveralls.io/r/jquast/blessed +.. image:: https://secure.travis-ci.org/jquast/blessed.png + :target: https://travis-ci.org/jquast/blessed + :alt: travis continous integration +.. image:: http://coveralls.io/repos/jquast/blessed/badge.png + :target: http://coveralls.io/r/jquast/blessed + :alt: coveralls code coveraage For patches, please construct a test case if possible. To test, install and execute python package command *tox*. From 3ee3c1a9355c01cf04d0edf33e7066d9342d4513 Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 22 Mar 2014 11:30:09 -0700 Subject: [PATCH 075/459] coverage + travis issue resolution --- .coveragerc | 4 ++-- .travis.yml | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.coveragerc b/.coveragerc index e97c6b4b..9672c05b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -4,5 +4,5 @@ source = blessed [report] omit = */tests/* -include = '*/site-packages/blessed/*' -omit = '*/site-packages/blessed/tests/*' +include = */site-packages/blessed/* +omit = */site-packages/blessed/tests/* diff --git a/.travis.yml b/.travis.yml index 5901b405..e5bc66e3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,12 +11,13 @@ install: # use only 'requires=' in setup.py, which is dynamic; # for python versions <26, we must install ordereddict - if [[ $TRAVIS_PYTHON_VERSION == 2.5 ]] || [[ $TRAVIS_PYTHON_VERSION == 2.6 ]]; then pip install --use-mirrors ordereddict; fi - - pip install -q --use-mirrors tox + - pip install -q --use-mirrors tox coverage script: + - mkdir -p .tox - cd .tox - coverage erase --rcfile=$TRAVIS_BUILD_DIR/.coveragerc - - cd $TRAVIS_BUILD_DIR + - cd $OLD_PWD - tox after_success: From aee0adb075b8724be9beb1d6b9b984bd3d3ba33e Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 22 Mar 2014 11:31:54 -0700 Subject: [PATCH 076/459] use $TRAVIS_BUILD_DIR, not $OLDPWD (no available?) --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e5bc66e3..1664b5b1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ script: - mkdir -p .tox - cd .tox - coverage erase --rcfile=$TRAVIS_BUILD_DIR/.coveragerc - - cd $OLD_PWD + - cd $TRAVIS_BUILD_DIR - tox after_success: From 3de7103d8200a04553e1b941e72c6685abb6904b Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 22 Mar 2014 11:34:47 -0700 Subject: [PATCH 077/459] move coveralls to travis level --- .travis.yml | 2 +- tox.ini | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1664b5b1..e52def68 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ install: # use only 'requires=' in setup.py, which is dynamic; # for python versions <26, we must install ordereddict - if [[ $TRAVIS_PYTHON_VERSION == 2.5 ]] || [[ $TRAVIS_PYTHON_VERSION == 2.6 ]]; then pip install --use-mirrors ordereddict; fi - - pip install -q --use-mirrors tox coverage + - pip install -q tox coverage coveralls script: - mkdir -p .tox diff --git a/tox.ini b/tox.ini index e4d060b0..ae3fc854 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,6 @@ envlist = py26, [testenv] changedir = {toxworkdir} deps = coverage - coveralls pytest pytest-pep8 pytest-flakes From 5fd8adc8b04ef8c2f9f55a6087a3de90eeab50b2 Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 22 Mar 2014 18:03:08 -0700 Subject: [PATCH 078/459] coverage only for py27; test packaged only for py3 --- .travis.yml | 27 +++++++++++++++------------ tox.ini | 36 ++++++++++++++++++++++++++++++++++-- 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/.travis.yml b/.travis.yml index e52def68..80a71874 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,23 +7,26 @@ env: - TOXENV=pypy install: - # travis wants requirements.txt; we don't, - # use only 'requires=' in setup.py, which is dynamic; # for python versions <26, we must install ordereddict - - if [[ $TRAVIS_PYTHON_VERSION == 2.5 ]] || [[ $TRAVIS_PYTHON_VERSION == 2.6 ]]; then pip install --use-mirrors ordereddict; fi - - pip install -q tox coverage coveralls + # mimicking the same dynamically generated 'requires=' + # in setup.py + - if [[ $TOXENV == "py25" ]] || [[ $TOXENV == "py26" ]]; then + pip install -q ordereddict + fi + + # for python version =27, cinstall and overage, coveralls. + # coverage is only measured and published for one version. + - if [[ $TOXENV == "py27" ]]; then + pip install -q coverage coveralls + fi script: - - mkdir -p .tox - - cd .tox - - coverage erase --rcfile=$TRAVIS_BUILD_DIR/.coveragerc - - cd $TRAVIS_BUILD_DIR - - tox + - tox -e $TOXENV after_success: - - cd .tox - - coverage combine --rcfile=$TRAVIS_BUILD_DIR/.coveragerc - - coveralls --rcfile=$TRAVIS_BUILD_DIR/.coveragerc + - if [[ $TOXENV == "py27" ]]; then + coveralls + fi notifications: email: diff --git a/tox.ini b/tox.ini index ae3fc854..860d0fba 100644 --- a/tox.ini +++ b/tox.ini @@ -4,8 +4,25 @@ envlist = py26, py33, pypy + [testenv] -changedir = {toxworkdir} +# for any python, run simple pytest +# with pep8 and pyflake checking +deps = pytest + pytest-pep8 + pytest-flakes + mock + +commands = {envbindir}/coverage run \ + -p \ + {envbindir}/py.test \ + -x --strict --pep8 --flakes \ + blessed/tests \ + {posargs} + + +[testenv:py27] +# for python27, measure coverage deps = coverage pytest pytest-pep8 @@ -13,7 +30,22 @@ deps = coverage pytest-cov mock -commands = {envbindir}/coverage run -p {envbindir}/py.test \ +commands = {envbindir}/coverage run \ + -p \ + {envbindir}/py.test \ + -x --strict --pep8 --flakes \ + --cov blessed \ + blessed/tests \ + {posargs} + + +[testenv:py33] +# for python3, test the version of blessed *installed*, +# and not from source. This is because we use the 2to3 tool. +changedir = {toxworkdir} +commands = {envbindir}/coverage run \ + -p --rcfile=$TRAVIS_BUILD_DIR/.coveragerc \ + {envbindir}/py.test \ -x --strict --pep8 --flakes \ --cov {envsitepackagesdir}/blessed \ {envsitepackagesdir}/blessed/tests \ From 37fc9ac29937e40ac22a7d8aed2557eac29770f7 Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 22 Mar 2014 18:05:14 -0700 Subject: [PATCH 079/459] SyntaxError, add ; to EOL for travis shellscripts --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 80a71874..c4b3faa8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,13 +11,13 @@ install: # mimicking the same dynamically generated 'requires=' # in setup.py - if [[ $TOXENV == "py25" ]] || [[ $TOXENV == "py26" ]]; then - pip install -q ordereddict + pip install -q ordereddict; fi # for python version =27, cinstall and overage, coveralls. # coverage is only measured and published for one version. - if [[ $TOXENV == "py27" ]]; then - pip install -q coverage coveralls + pip install -q coverage coveralls; fi script: From 1f19d188c8f0ff8eea67768c306372cd51e11151 Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 22 Mar 2014 18:07:15 -0700 Subject: [PATCH 080/459] add missing tox --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index c4b3faa8..06145c83 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,8 @@ env: - TOXENV=pypy install: + - pip installl -q tox + # for python versions <26, we must install ordereddict # mimicking the same dynamically generated 'requires=' # in setup.py @@ -20,6 +22,7 @@ install: pip install -q coverage coveralls; fi + script: - tox -e $TOXENV From b4e97cf030579673acd5e23d3069a89d54dbac7c Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 22 Mar 2014 18:08:39 -0700 Subject: [PATCH 081/459] ERROR: unknown command "installl" - maybe you meant "install" --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 06145c83..928a001e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ env: - TOXENV=pypy install: - - pip installl -q tox + - pip install -q tox # for python versions <26, we must install ordereddict # mimicking the same dynamically generated 'requires=' From 1a6091b2193f969e9bea3d45e049c65828924bcd Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 22 Mar 2014 18:41:29 -0700 Subject: [PATCH 082/459] more fixes; seperate of py26/27 vs. py33 tests --- .coveragerc | 4 +--- setup.py | 52 ---------------------------------------------------- tox.ini | 13 ++++--------- 3 files changed, 5 insertions(+), 64 deletions(-) diff --git a/.coveragerc b/.coveragerc index 9672c05b..eb472140 100644 --- a/.coveragerc +++ b/.coveragerc @@ -3,6 +3,4 @@ branch = True source = blessed [report] -omit = */tests/* -include = */site-packages/blessed/* -omit = */site-packages/blessed/tests/* +omit = blessed/tests/* diff --git a/setup.py b/setup.py index 2f50dc54..2f1f5bc1 100755 --- a/setup.py +++ b/setup.py @@ -13,58 +13,6 @@ elif sys.version_info >= (3,): extra.update({'use_2to3': True}) -# try: -# import setuptools -# except ImportError: -# from distribute_setup import use_setuptools -# use_setuptools() -# -# -# -#dev_requirements = ['pytest', 'pytest-cov', 'pytest-pep8', -# 'pytest-flakes', 'pytest-sugar', 'mock'] -# - -#class PyTest(test): -# -# def initialize_options(self): -# test.initialize_options(self) -# test_suite = True -# -# def finalize_options(self): -# test.finalize_options(self) -# self.test_args = ['-x', '--strict', '--pep8', '--flakes', -# '--cov', 'blessed', '--cov-report', 'html', -# '--pyargs', 'blessed.tests'] -# -# def run(self): -# import pytest -## import blessed.tests -## print ('*') -## print(blessed.tests.__file__) -## print ('*') -# raise SystemExit(pytest.main(self.test_args)) - - -#class SetupDevelop(develop): -# """Setup development environment suitable for testing.""" -# -# def finalize_options(self): -# assert os.getenv('VIRTUAL_ENV'), "Please use virtualenv." -# develop.finalize_options(self) -# -# def run(self): -# import subprocess -# reqs = dev_requirements -# reqs.extend(extra_setup['requires']) -# if extra_setup.get('use_2to3', False): -# # install in virtualenv, via 2to3 mechanism -# reqs.append(self.distribution.get_name()) -# subprocess.check_call('pip install {reqs}' -# .format(reqs=u' '.join(reqs)), -# shell=True) -# develop.run(self) - here = os.path.dirname(__file__) setup( name='blessed', diff --git a/tox.ini b/tox.ini index 860d0fba..14f8dc03 100644 --- a/tox.ini +++ b/tox.ini @@ -13,9 +13,7 @@ deps = pytest pytest-flakes mock -commands = {envbindir}/coverage run \ - -p \ - {envbindir}/py.test \ +commands = {envbindir}/py.test \ -x --strict --pep8 --flakes \ blessed/tests \ {posargs} @@ -23,6 +21,7 @@ commands = {envbindir}/coverage run \ [testenv:py27] # for python27, measure coverage +usedevelop = True deps = coverage pytest pytest-pep8 @@ -31,7 +30,6 @@ deps = coverage mock commands = {envbindir}/coverage run \ - -p \ {envbindir}/py.test \ -x --strict --pep8 --flakes \ --cov blessed \ @@ -40,13 +38,10 @@ commands = {envbindir}/coverage run \ [testenv:py33] -# for python3, test the version of blessed *installed*, +# for python3, test the version of blessed that is *installed*, # and not from source. This is because we use the 2to3 tool. changedir = {toxworkdir} -commands = {envbindir}/coverage run \ - -p --rcfile=$TRAVIS_BUILD_DIR/.coveragerc \ - {envbindir}/py.test \ +commands = {envbindir}/py.test \ -x --strict --pep8 --flakes \ - --cov {envsitepackagesdir}/blessed \ {envsitepackagesdir}/blessed/tests \ {posargs} From 9d21ba7acb2e6d7720ca10b555a727dad2ba594b Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 22 Mar 2014 18:43:59 -0700 Subject: [PATCH 083/459] resolve coveralls invocation of script --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 928a001e..4a231716 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,7 +28,7 @@ script: after_success: - if [[ $TOXENV == "py27" ]]; then - coveralls + coveralls; fi notifications: From e92e106d494556c613cc2a914b4ba3576cee1020 Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 22 Mar 2014 20:52:53 -0700 Subject: [PATCH 084/459] new test case for keyboard.get_keyboard_sequence --- blessed/tests/test_keyboard.py | 56 +++++++++++++++++++++++++++++++--- tox.ini | 2 +- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index 67540faf..3cb1f50c 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -274,7 +274,7 @@ def test_get_keyboard_codes(): def test_alternative_left_right(): - """Test alternative_left_right behavior for space/backspace.""" + """Test _alternative_left_right behavior for space/backspace.""" from blessed.keyboard import _alternative_left_right term = mock.Mock() term._cuf1 = u'' @@ -283,11 +283,11 @@ def test_alternative_left_right(): term._cuf1 = u' ' term._cub1 = u'\b' assert not bool(_alternative_left_right(term)) - term._cuf1 = u'x' - term._cub1 = u'y' + term._cuf1 = u'seq-right' + term._cub1 = u'seq-left' assert (_alternative_left_right(term) == { - u'x': curses.KEY_RIGHT, - u'y': curses.KEY_LEFT}) + u'seq-right': curses.KEY_RIGHT, + u'seq-left': curses.KEY_LEFT}) def test_cuf1_and_cub1_as_RIGHT_LEFT(all_terms): @@ -313,6 +313,7 @@ def child(kind): def test_get_keyboard_sequences_sort_order(xterms): + """ordereddict ensures sequences are ordered longest-first.""" @as_subprocess def child(): term = TestTerminal(force_styling=True) @@ -325,6 +326,51 @@ def child(): child() +def test_get_keyboard_sequence(monkeypatch): + """Test keyboard.get_keyboard_sequence. """ + + @as_subprocess + def child(monkeypatch): + import curses.has_key + import blessed.keyboard + + (KEY_SMALL, KEY_LARGE, KEY_MIXIN) = range(3) + (CAP_SMALL, CAP_LARGE) = ('cap-small cap-large'.split()) + (SEQ_SMALL, SEQ_LARGE, SEQ_MIXIN, + SEQ_ALT_CUF1, SEQ_ALT_CUB1 + ) = ('seq-small-a seq-large-abcdefg seq-mixin ' + 'seq-alt-cuf1 seq-alt-cub1_'.split()) + + # patch curses functions + monkeypatch.setattr(curses, 'tigetstr', + lambda cap: {CAP_SMALL: SEQ_SMALL, + CAP_LARGE: SEQ_LARGE}[cap]) + + monkeypatch.setattr(curses.has_key, '_capability_names', + dict(((KEY_SMALL, CAP_SMALL,), + (KEY_LARGE, CAP_LARGE,)))) + + # patch global sequence mix-in + monkeypatch.setattr(blessed.keyboard, + 'DEFAULT_SEQUENCE_MIXIN', ( + (SEQ_MIXIN, KEY_MIXIN),)) + + # patch for _alternative_left_right + term = mock.Mock() + term._cuf1 = SEQ_ALT_CUF1 + term._cub1 = SEQ_ALT_CUB1 + keymap = blessed.keyboard.get_keyboard_sequences(term) + + assert keymap.items() == [ + (SEQ_LARGE, KEY_LARGE), + (SEQ_ALT_CUB1, curses.KEY_LEFT), + (SEQ_ALT_CUF1, curses.KEY_RIGHT), + (SEQ_SMALL, KEY_SMALL), + (SEQ_MIXIN, KEY_MIXIN)] + + child(monkeypatch) + + def test_resolve_sequence(): """Test resolve_sequence for order-dependent mapping.""" from blessed.keyboard import resolve_sequence, OrderedDict diff --git a/tox.ini b/tox.ini index 14f8dc03..6f23951b 100644 --- a/tox.ini +++ b/tox.ini @@ -32,9 +32,9 @@ deps = coverage commands = {envbindir}/coverage run \ {envbindir}/py.test \ -x --strict --pep8 --flakes \ - --cov blessed \ blessed/tests \ {posargs} + coverage report [testenv:py33] From 3820c3d02487735f8c88eb61f9a93e814b28ee67 Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 22 Mar 2014 20:57:21 -0700 Subject: [PATCH 085/459] move test_get_keyboard_sequences out of child process --- blessed/tests/test_keyboard.py | 77 ++++++++++++++++------------------ 1 file changed, 36 insertions(+), 41 deletions(-) diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index 3cb1f50c..d68370ff 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -328,47 +328,42 @@ def child(): def test_get_keyboard_sequence(monkeypatch): """Test keyboard.get_keyboard_sequence. """ - - @as_subprocess - def child(monkeypatch): - import curses.has_key - import blessed.keyboard - - (KEY_SMALL, KEY_LARGE, KEY_MIXIN) = range(3) - (CAP_SMALL, CAP_LARGE) = ('cap-small cap-large'.split()) - (SEQ_SMALL, SEQ_LARGE, SEQ_MIXIN, - SEQ_ALT_CUF1, SEQ_ALT_CUB1 - ) = ('seq-small-a seq-large-abcdefg seq-mixin ' - 'seq-alt-cuf1 seq-alt-cub1_'.split()) - - # patch curses functions - monkeypatch.setattr(curses, 'tigetstr', - lambda cap: {CAP_SMALL: SEQ_SMALL, - CAP_LARGE: SEQ_LARGE}[cap]) - - monkeypatch.setattr(curses.has_key, '_capability_names', - dict(((KEY_SMALL, CAP_SMALL,), - (KEY_LARGE, CAP_LARGE,)))) - - # patch global sequence mix-in - monkeypatch.setattr(blessed.keyboard, - 'DEFAULT_SEQUENCE_MIXIN', ( - (SEQ_MIXIN, KEY_MIXIN),)) - - # patch for _alternative_left_right - term = mock.Mock() - term._cuf1 = SEQ_ALT_CUF1 - term._cub1 = SEQ_ALT_CUB1 - keymap = blessed.keyboard.get_keyboard_sequences(term) - - assert keymap.items() == [ - (SEQ_LARGE, KEY_LARGE), - (SEQ_ALT_CUB1, curses.KEY_LEFT), - (SEQ_ALT_CUF1, curses.KEY_RIGHT), - (SEQ_SMALL, KEY_SMALL), - (SEQ_MIXIN, KEY_MIXIN)] - - child(monkeypatch) + import curses.has_key + import blessed.keyboard + + (KEY_SMALL, KEY_LARGE, KEY_MIXIN) = range(3) + (CAP_SMALL, CAP_LARGE) = ('cap-small cap-large'.split()) + (SEQ_SMALL, SEQ_LARGE, SEQ_MIXIN, + SEQ_ALT_CUF1, SEQ_ALT_CUB1 + ) = ('seq-small-a seq-large-abcdefg seq-mixin ' + 'seq-alt-cuf1 seq-alt-cub1_'.split()) + + # patch curses functions + monkeypatch.setattr(curses, 'tigetstr', + lambda cap: {CAP_SMALL: SEQ_SMALL, + CAP_LARGE: SEQ_LARGE}[cap]) + + monkeypatch.setattr(curses.has_key, '_capability_names', + dict(((KEY_SMALL, CAP_SMALL,), + (KEY_LARGE, CAP_LARGE,)))) + + # patch global sequence mix-in + monkeypatch.setattr(blessed.keyboard, + 'DEFAULT_SEQUENCE_MIXIN', ( + (SEQ_MIXIN, KEY_MIXIN),)) + + # patch for _alternative_left_right + term = mock.Mock() + term._cuf1 = SEQ_ALT_CUF1 + term._cub1 = SEQ_ALT_CUB1 + keymap = blessed.keyboard.get_keyboard_sequences(term) + + assert keymap.items() == [ + (SEQ_LARGE, KEY_LARGE), + (SEQ_ALT_CUB1, curses.KEY_LEFT), + (SEQ_ALT_CUF1, curses.KEY_RIGHT), + (SEQ_SMALL, KEY_SMALL), + (SEQ_MIXIN, KEY_MIXIN)] def test_resolve_sequence(): From cf5dfae18f4e86a629da6eb9513214b9d11123fc Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 22 Mar 2014 21:10:43 -0700 Subject: [PATCH 086/459] py26 through 3.3+ encoding compatibility --- blessed/tests/test_keyboard.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index d68370ff..8a502312 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -332,11 +332,13 @@ def test_get_keyboard_sequence(monkeypatch): import blessed.keyboard (KEY_SMALL, KEY_LARGE, KEY_MIXIN) = range(3) - (CAP_SMALL, CAP_LARGE) = ('cap-small cap-large'.split()) - (SEQ_SMALL, SEQ_LARGE, SEQ_MIXIN, - SEQ_ALT_CUF1, SEQ_ALT_CUB1 - ) = ('seq-small-a seq-large-abcdefg seq-mixin ' - 'seq-alt-cuf1 seq-alt-cub1_'.split()) + (CAP_SMALL, CAP_LARGE) = 'cap-small cap-large'.split() + (SEQ_SMALL, SEQ_LARGE, SEQ_MIXIN, SEQ_ALT_CUF1, SEQ_ALT_CUB1) = ( + b'seq-small-a', + b'seq-large-abcdefg', + b'seq-mixin', + b'seq-alt-cuf1', + b'seq-alt-cub1_') # patch curses functions monkeypatch.setattr(curses, 'tigetstr', @@ -350,20 +352,20 @@ def test_get_keyboard_sequence(monkeypatch): # patch global sequence mix-in monkeypatch.setattr(blessed.keyboard, 'DEFAULT_SEQUENCE_MIXIN', ( - (SEQ_MIXIN, KEY_MIXIN),)) + (SEQ_MIXIN.decode('latin1'), KEY_MIXIN),)) # patch for _alternative_left_right term = mock.Mock() - term._cuf1 = SEQ_ALT_CUF1 - term._cub1 = SEQ_ALT_CUB1 + term._cuf1 = SEQ_ALT_CUF1.decode('latin1') + term._cub1 = SEQ_ALT_CUB1.decode('latin1') keymap = blessed.keyboard.get_keyboard_sequences(term) assert keymap.items() == [ - (SEQ_LARGE, KEY_LARGE), - (SEQ_ALT_CUB1, curses.KEY_LEFT), - (SEQ_ALT_CUF1, curses.KEY_RIGHT), - (SEQ_SMALL, KEY_SMALL), - (SEQ_MIXIN, KEY_MIXIN)] + (SEQ_LARGE.decode('latin1'), KEY_LARGE), + (SEQ_ALT_CUB1.decode('latin1'), curses.KEY_LEFT), + (SEQ_ALT_CUF1.decode('latin1'), curses.KEY_RIGHT), + (SEQ_SMALL.decode('latin1'), KEY_SMALL), + (SEQ_MIXIN.decode('latin1'), KEY_MIXIN)] def test_resolve_sequence(): From b9a023b230802f0fe246f4720bda0dfac6b2394d Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 22 Mar 2014 22:03:37 -0700 Subject: [PATCH 087/459] tests for Parametrizing and FormattingString --- blessed/tests/test_formatters.py | 84 ++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 blessed/tests/test_formatters.py diff --git a/blessed/tests/test_formatters.py b/blessed/tests/test_formatters.py new file mode 100644 index 00000000..3c852c39 --- /dev/null +++ b/blessed/tests/test_formatters.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +"""Tests string formatting functions.""" +import curses + + +def test_parameterizing_string_args(monkeypatch): + """Test ParameterizingString as a callable """ + from blessed.formatters import (ParameterizingString, + FormattingString) + + # first argument to tparm() is the sequence name, returned as-is; + # subsequent arguments are usually Integers. + tparm = lambda *args: u'~'.join( + ('%s' % (arg,) for arg in args) + ).encode('latin1') + + monkeypatch.setattr(curses, 'tparm', tparm) + + # given, + pstr = ParameterizingString(name=u'cap', attr=u'seqname', normal=u'norm') + + # excercise __new__ + assert pstr._name == u'cap' + assert pstr._normal == u'norm' + assert str(pstr) == u'seqname' + + # excercise __call__ + zero = pstr(0) + assert type(zero) is FormattingString + assert zero == u'seqname~0' + assert zero('text') == u'seqname~0textnorm' + + # excercise __call__ with multiple args + onetwo = pstr(1, 2) + assert type(onetwo) is FormattingString + assert onetwo == u'seqname~1~2' + assert onetwo('text') == u'seqname~1~2textnorm' + + +def test_parameterizing_string_type_error(monkeypatch): + """Test ParameterizingString TypeError""" + from blessed.formatters import (ParameterizingString) + + def tparm_raises_TypeError(*args): + raise TypeError('custom_err') + + monkeypatch.setattr(curses, 'tparm', tparm_raises_TypeError) + + # given, + pstr = ParameterizingString(name=u'cap', attr=u'seqname', normal=u'norm') + + # ensure TypeError when given a string raises custom exception + try: + pstr('XYZ') + assert False, "previous call should have raised TypeError" + except TypeError, err: + assert err[0] == ("A native or nonexistent capability template, " + "u'cap' received invalid argument ('XYZ',): " + "custom_err. You probably misspelled a " + "formatting call like `bright_red'") + + # ensure TypeError when given an integer raises its natural exception + try: + pstr(0) + assert False, "previous call should have raised TypeError" + except TypeError, err: + assert err[0] == "custom_err" + + +def test_formattingstring(monkeypatch): + """Test FormattingString""" + from blessed.formatters import (FormattingString) + + # given, with arg + pstr = FormattingString(attr=u'attr', normal=u'norm') + + # excercise __call__, + assert pstr._normal == u'norm' + assert str(pstr) == u'attr' + assert pstr('text') == u'attrtextnorm' + + # given, without arg + pstr = FormattingString(attr=u'', normal=u'norm') + assert pstr('text') == u'text' From 7bd90ed411c4156ed5f8f41ec618f3ac0c4d7cf3 Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 22 Mar 2014 22:07:20 -0700 Subject: [PATCH 088/459] py2x vs. py3x encoding differences --- blessed/tests/test_formatters.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/blessed/tests/test_formatters.py b/blessed/tests/test_formatters.py index 3c852c39..ccaf05d8 100644 --- a/blessed/tests/test_formatters.py +++ b/blessed/tests/test_formatters.py @@ -11,8 +11,8 @@ def test_parameterizing_string_args(monkeypatch): # first argument to tparm() is the sequence name, returned as-is; # subsequent arguments are usually Integers. tparm = lambda *args: u'~'.join( - ('%s' % (arg,) for arg in args) - ).encode('latin1') + arg.decode('latin1') if not num else '%s' % (arg,) + for num, arg in enumerate(args)).encode('latin1') monkeypatch.setattr(curses, 'tparm', tparm) @@ -54,17 +54,23 @@ def tparm_raises_TypeError(*args): pstr('XYZ') assert False, "previous call should have raised TypeError" except TypeError, err: - assert err[0] == ("A native or nonexistent capability template, " - "u'cap' received invalid argument ('XYZ',): " - "custom_err. You probably misspelled a " - "formatting call like `bright_red'") + assert (err.args[0] == ( # py3x + "A native or nonexistent capability template, " + "'cap' received invalid argument ('XYZ',): " + "custom_err. You probably misspelled a " + "formatting call like `bright_red'") or + err.args[0] == ( # py2x + "A native or nonexistent capability template, " + "u'cap' received invalid argument ('XYZ',): " + "custom_err. You probably misspelled a " + "formatting call like `bright_red'") # ensure TypeError when given an integer raises its natural exception try: pstr(0) assert False, "previous call should have raised TypeError" except TypeError, err: - assert err[0] == "custom_err" + assert err.args[0] == "custom_err" def test_formattingstring(monkeypatch): From fb156502a46fc69bcb19060402cc5fdfbd4e07b5 Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 22 Mar 2014 22:08:17 -0700 Subject: [PATCH 089/459] SyntaxErr --- blessed/tests/test_formatters.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/blessed/tests/test_formatters.py b/blessed/tests/test_formatters.py index ccaf05d8..78f161cb 100644 --- a/blessed/tests/test_formatters.py +++ b/blessed/tests/test_formatters.py @@ -59,11 +59,11 @@ def tparm_raises_TypeError(*args): "'cap' received invalid argument ('XYZ',): " "custom_err. You probably misspelled a " "formatting call like `bright_red'") or - err.args[0] == ( # py2x - "A native or nonexistent capability template, " - "u'cap' received invalid argument ('XYZ',): " - "custom_err. You probably misspelled a " - "formatting call like `bright_red'") + err.args[0] == ( + "A native or nonexistent capability template, " + "u'cap' received invalid argument ('XYZ',): " + "custom_err. You probably misspelled a " + "formatting call like `bright_red'")) # ensure TypeError when given an integer raises its natural exception try: From 2729d60c193ed284775a08be1773e06b5ceb60de Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 22 Mar 2014 22:14:28 -0700 Subject: [PATCH 090/459] add test for NullCallableString --- blessed/tests/test_formatters.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/blessed/tests/test_formatters.py b/blessed/tests/test_formatters.py index 78f161cb..5af6b6a2 100644 --- a/blessed/tests/test_formatters.py +++ b/blessed/tests/test_formatters.py @@ -19,18 +19,18 @@ def test_parameterizing_string_args(monkeypatch): # given, pstr = ParameterizingString(name=u'cap', attr=u'seqname', normal=u'norm') - # excercise __new__ + # excersize __new__ assert pstr._name == u'cap' assert pstr._normal == u'norm' assert str(pstr) == u'seqname' - # excercise __call__ + # excersize __call__ zero = pstr(0) assert type(zero) is FormattingString assert zero == u'seqname~0' assert zero('text') == u'seqname~0textnorm' - # excercise __call__ with multiple args + # excersize __call__ with multiple args onetwo = pstr(1, 2) assert type(onetwo) is FormattingString assert onetwo == u'seqname~1~2' @@ -80,7 +80,7 @@ def test_formattingstring(monkeypatch): # given, with arg pstr = FormattingString(attr=u'attr', normal=u'norm') - # excercise __call__, + # excersize __call__, assert pstr._normal == u'norm' assert str(pstr) == u'attr' assert pstr('text') == u'attrtextnorm' @@ -88,3 +88,20 @@ def test_formattingstring(monkeypatch): # given, without arg pstr = FormattingString(attr=u'', normal=u'norm') assert pstr('text') == u'text' + + +def test_nullcallablestring(monkeypatch): + """Test NullCallableString""" + from blessed.formatters import (NullCallableString) + + # given, with arg + pstr = NullCallableString() + + # excersize __call__, + assert str(pstr) == u'' + assert pstr('text') == u'text' + assert pstr('text', 1) == u'' + assert pstr('text', 'moretext') == u'' + assert pstr(99, 1) == u'' + assert pstr() == u'' + assert pstr(0) == u'' From f70c6c5f9db6e8364af92f15172a1a51f1c5e1b7 Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 22 Mar 2014 22:29:40 -0700 Subject: [PATCH 091/459] tests for split_compound and resolve_capability --- blessed/tests/test_formatters.py | 42 +++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/blessed/tests/test_formatters.py b/blessed/tests/test_formatters.py index 5af6b6a2..ab3dcf72 100644 --- a/blessed/tests/test_formatters.py +++ b/blessed/tests/test_formatters.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- """Tests string formatting functions.""" import curses +import mock def test_parameterizing_string_args(monkeypatch): - """Test ParameterizingString as a callable """ + """Test basic formatters.ParameterizingString.""" from blessed.formatters import (ParameterizingString, FormattingString) @@ -38,7 +39,7 @@ def test_parameterizing_string_args(monkeypatch): def test_parameterizing_string_type_error(monkeypatch): - """Test ParameterizingString TypeError""" + """Test formatters.ParameterizingString raising TypeError""" from blessed.formatters import (ParameterizingString) def tparm_raises_TypeError(*args): @@ -74,7 +75,7 @@ def tparm_raises_TypeError(*args): def test_formattingstring(monkeypatch): - """Test FormattingString""" + """Test formatters.FormattingString""" from blessed.formatters import (FormattingString) # given, with arg @@ -91,7 +92,7 @@ def test_formattingstring(monkeypatch): def test_nullcallablestring(monkeypatch): - """Test NullCallableString""" + """Test formatters.NullCallableString""" from blessed.formatters import (NullCallableString) # given, with arg @@ -105,3 +106,36 @@ def test_nullcallablestring(monkeypatch): assert pstr(99, 1) == u'' assert pstr() == u'' assert pstr(0) == u'' + + +def test_split_compound(): + """Test formatters.split_compound.""" + from blessed.formatters import split_compound + + assert split_compound(u'') == [u''] + assert split_compound(u'a_b_c') == [u'a', u'b', u'c'] + assert split_compound(u'a_on_b_c') == [u'a', u'on_b', u'c'] + assert split_compound(u'a_bright_b_c') == [u'a', u'bright_b', u'c'] + assert split_compound(u'a_on_bright_b_c') == [u'a', u'on_bright_b', u'c'] + + +def test_resolve_capability(monkeypatch): + """Test formatters.resolve_capability and term sugaring """ + from blessed.formatters import resolve_capability + + # given, always returns a b'seq' + tigetstr = lambda attr: ('seq-%s' % (attr,)).encode('latin1') + monkeypatch.setattr(curses, 'tigetstr', tigetstr) + term = mock.Mock() + term._sugar = dict(mnemonic='xyz') + + # excersize + assert resolve_capability(term, 'mnemonic') == u'seq-xyz' + assert resolve_capability(term, 'natural') == u'seq-natural' + + # given, always returns None + tigetstr_none = lambda attr: None + monkeypatch.setattr(curses, 'tigetstr', tigetstr_none) + + # excersize, + assert resolve_capability(term, 'natural') == u'' From d816720746f1fc3f279d8d3c7ba371e4f6c0f4dc Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 22 Mar 2014 23:31:47 -0700 Subject: [PATCH 092/459] test resolve_color, resolve_attribute also move resolve_color above resolve_attribute in formatters.py to reflect their dependency (and complexity) order. --- blessed/formatters.py | 46 +++++------ blessed/tests/test_formatters.py | 138 ++++++++++++++++++++++++++++++- 2 files changed, 158 insertions(+), 26 deletions(-) diff --git a/blessed/formatters.py b/blessed/formatters.py index 489659af..ef69c851 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -146,6 +146,29 @@ def resolve_capability(term, attr): return u'' if val is None else val.decode('latin1') +def resolve_color(term, color): + """Resolve a color, to callable capability, valid ``color`` capabilities + are format ``red``, or ``on_right_green``. + """ + # NOTE(erikrose): Does curses automatically exchange red and blue and cyan + # and yellow when a terminal supports setf/setb rather than setaf/setab? + # I'll be blasted if I can find any documentation. The following + # assumes it does. + color_cap = (term._background_color if 'on_' in color else + term._foreground_color) + + # curses constants go up to only 7, so add an offset to get at the + # bright colors at 8-15: + offset = 8 if 'bright_' in color else 0 + base_color = color.rsplit('_', 1)[-1] + if term.number_of_colors == 0: + return NullCallableString() + + attr = 'COLOR_%s' % (base_color.upper(),) + fmt_attr = color_cap(getattr(curses, attr) + offset) + return FormattingString(fmt_attr, term.normal) + + def resolve_attribute(term, attr): """Resolve a sugary or plain capability name, color, or compound formatting function name into a *callable* unicode string @@ -171,26 +194,3 @@ def resolve_attribute(term, attr): return ParameterizingString(name=attr, attr=resolve_capability(term, attr), normal=term.normal) - - -def resolve_color(term, color): - """Resolve a color, to callable capability, valid ``color`` capabilities - are format ``red``, or ``on_right_green``. - """ - # NOTE(erikrose): Does curses automatically exchange red and blue and cyan - # and yellow when a terminal supports setf/setb rather than setaf/setab? - # I'll be blasted if I can find any documentation. The following - # assumes it does. - color_cap = (term._background_color if 'on_' in color else - term._foreground_color) - - # curses constants go up to only 7, so add an offset to get at the - # bright colors at 8-15: - offset = 8 if 'bright_' in color else 0 - base_color = color.rsplit('_', 1)[-1] - if term.number_of_colors == 0: - return NullCallableString() - - attr = 'COLOR_%s' % (base_color.upper(),) - fmt_attr = color_cap(getattr(curses, attr) + offset) - return FormattingString(fmt_attr, term.normal) diff --git a/blessed/tests/test_formatters.py b/blessed/tests/test_formatters.py index ab3dcf72..a158941f 100644 --- a/blessed/tests/test_formatters.py +++ b/blessed/tests/test_formatters.py @@ -6,8 +6,7 @@ def test_parameterizing_string_args(monkeypatch): """Test basic formatters.ParameterizingString.""" - from blessed.formatters import (ParameterizingString, - FormattingString) + from blessed.formatters import ParameterizingString, FormattingString # first argument to tparm() is the sequence name, returned as-is; # subsequent arguments are usually Integers. @@ -40,7 +39,7 @@ def test_parameterizing_string_args(monkeypatch): def test_parameterizing_string_type_error(monkeypatch): """Test formatters.ParameterizingString raising TypeError""" - from blessed.formatters import (ParameterizingString) + from blessed.formatters import ParameterizingString def tparm_raises_TypeError(*args): raise TypeError('custom_err') @@ -139,3 +138,136 @@ def test_resolve_capability(monkeypatch): # excersize, assert resolve_capability(term, 'natural') == u'' + + +def test_resolve_color(monkeypatch): + """Test formatters.resolve_color.""" + from blessed.formatters import (resolve_color, + FormattingString, + NullCallableString) + + color_cap = lambda digit: 'seq-%s' % (digit,) + monkeypatch.setattr(curses, 'COLOR_RED', 1984) + + # given, terminal with color capabilities + term = mock.Mock() + term._background_color = color_cap + term._foreground_color = color_cap + term.number_of_colors = -1 + term.normal = 'seq-normal' + + # excersize, + red = resolve_color(term, 'red') + assert type(red) == FormattingString + assert red == u'seq-1984' + assert red('text') == u'seq-1984textseq-normal' + + # excersize bold, +8 + bright_red = resolve_color(term, 'bright_red') + assert type(bright_red) == FormattingString + assert bright_red == u'seq-1992' + assert bright_red('text') == u'seq-1992textseq-normal' + + # given, terminal without color + term.number_of_colors = 0 + + # excersize, + red = resolve_color(term, 'red') + assert type(red) == NullCallableString + assert red == u'' + assert red('text') == u'text' + + # excesize bold, + bright_red = resolve_color(term, 'bright_red') + assert type(bright_red) == NullCallableString + assert bright_red == u'' + assert bright_red('text') == u'text' + + +def test_resolve_attribute_as_color(monkeypatch): + """ Test simple resolve_attribte() given color name. """ + import blessed + from blessed.formatters import resolve_attribute + + resolve_color = lambda term, digit: 'seq-%s' % (digit,) + COLORS = set(['COLORX', 'COLORY']) + COMPOUNDABLES = set(['JOINT', 'COMPOUND']) + monkeypatch.setattr(blessed.formatters, 'resolve_color', resolve_color) + monkeypatch.setattr(blessed.formatters, 'COLORS', COLORS) + monkeypatch.setattr(blessed.formatters, 'COMPOUNDABLES', COMPOUNDABLES) + term = mock.Mock() + assert resolve_attribute(term, 'COLORX') == u'seq-COLORX' + + +def test_resolve_attribute_as_compoundable(monkeypatch): + """ Test simple resolve_attribte() given a compoundable. """ + import blessed + from blessed.formatters import resolve_attribute, FormattingString + + resolve_cap = lambda term, digit: 'seq-%s' % (digit,) + COMPOUNDABLES = set(['JOINT', 'COMPOUND']) + monkeypatch.setattr(blessed.formatters, 'resolve_capability', resolve_cap) + monkeypatch.setattr(blessed.formatters, 'COMPOUNDABLES', COMPOUNDABLES) + term = mock.Mock() + term.normal = 'seq-normal' + + compound = resolve_attribute(term, 'JOINT') + assert type(compound) is FormattingString + assert str(compound) == u'seq-JOINT' + assert compound('text') == u'seq-JOINTtextseq-normal' + + +def test_resolve_attribute_non_compoundables(monkeypatch): + """ Test recursive compounding of resolve_attribute(). """ + import blessed + from blessed.formatters import resolve_attribute, ParameterizingString + uncompoundables = lambda attr: ['split', 'compound'] + resolve_cap = lambda term, digit: 'seq-%s' % (digit,) + monkeypatch.setattr(blessed.formatters, 'split_compound', uncompoundables) + monkeypatch.setattr(blessed.formatters, 'resolve_capability', resolve_cap) + tparm = lambda *args: u'~'.join( + arg.decode('latin1') if not num else '%s' % (arg,) + for num, arg in enumerate(args)).encode('latin1') + monkeypatch.setattr(curses, 'tparm', tparm) + + term = mock.Mock() + term.normal = 'seq-normal' + + # given + pstr = resolve_attribute(term, 'not-a-compoundable') + assert type(pstr) == ParameterizingString + assert str(pstr) == u'seq-not-a-compoundable' + # this is like calling term.move_x(3) + assert pstr(3) == u'seq-not-a-compoundable~3' + # this is like calling term.move_x(3)('text') + assert pstr(3)('text') == u'seq-not-a-compoundable~3textseq-normal' + + +def test_resolve_attribute_recursive_compoundables(monkeypatch): + """ Test recursive compounding of resolve_attribute(). """ + import blessed + from blessed.formatters import resolve_attribute, FormattingString + + # patch, + resolve_cap = lambda term, digit: 'seq-%s' % (digit,) + monkeypatch.setattr(blessed.formatters, 'resolve_capability', resolve_cap) + tparm = lambda *args: u'~'.join( + arg.decode('latin1') if not num else '%s' % (arg,) + for num, arg in enumerate(args)).encode('latin1') + monkeypatch.setattr(curses, 'tparm', tparm) + monkeypatch.setattr(curses, 'COLOR_RED', 6502) + monkeypatch.setattr(curses, 'COLOR_BLUE', 6800) + + color_cap = lambda digit: 'seq-%s' % (digit,) + term = mock.Mock() + term._background_color = color_cap + term._foreground_color = color_cap + term.normal = 'seq-normal' + + # given, + pstr = resolve_attribute(term, 'bright_blue_on_red') + + # excersize, + assert type(pstr) == FormattingString + assert str(pstr) == 'seq-6808seq-6502' + assert pstr('text') == 'seq-6808seq-6502textseq-normal' From 7540f9666cec488203037dcd9192f5e891c1114c Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 00:31:46 -0700 Subject: [PATCH 093/459] using pytest-cov over coverage; doesnt help still trying to get coverage after pty.fork() in child, can't seem to do it. even wrote a testcase for coverage.py, seems to pass, there. --- tox.ini | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tox.ini b/tox.ini index 6f23951b..4e38fa02 100644 --- a/tox.ini +++ b/tox.ini @@ -9,11 +9,12 @@ envlist = py26, # for any python, run simple pytest # with pep8 and pyflake checking deps = pytest + pytest-xdist pytest-pep8 pytest-flakes mock -commands = {envbindir}/py.test \ +commands = {envbindir}/py.test -n 2 \ -x --strict --pep8 --flakes \ blessed/tests \ {posargs} @@ -22,18 +23,18 @@ commands = {envbindir}/py.test \ [testenv:py27] # for python27, measure coverage usedevelop = True -deps = coverage - pytest +deps = pytest + pytest-cov + pytest-xdist pytest-pep8 pytest-flakes - pytest-cov mock -commands = {envbindir}/coverage run \ - {envbindir}/py.test \ - -x --strict --pep8 --flakes \ - blessed/tests \ - {posargs} +commands = pip install -q + {envbindir}/py.test -n 2 \ + -x --strict --pep8 --flakes \ + --cov blessed \ + blessed/tests {posargs} coverage report From 7b5351a8f2fdbf2f9160d5d28115ca7ef4b31b32 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 00:40:07 -0700 Subject: [PATCH 094/459] code coverage in subprocess ! --- blessed/tests/accessories.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/blessed/tests/accessories.py b/blessed/tests/accessories.py index 67c2dfd4..3693e416 100644 --- a/blessed/tests/accessories.py +++ b/blessed/tests/accessories.py @@ -45,6 +45,7 @@ def __init__(self, func): def __call__(self, *args, **kwargs): pid, master_fd = pty.fork() if pid is self._CHILD_PID: + cov = __import__('cov_core_init').init() # child process executes function, raises exception # if failed, causing a non-zero exit code, using the # protected _exit() function of ``os``; to prevent the @@ -66,6 +67,8 @@ def __call__(self, *args, **kwargs): os.close(sys.__stdin__.fileno()) os._exit(1) else: + cov.stop() + cov.save() os._exit(0) exc_output = unicode() From c3e9ceb800689e9d0a92fe7270a03e55281246c1 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 00:43:52 -0700 Subject: [PATCH 095/459] of course, allow uncoveraged to skip --- blessed/tests/accessories.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/blessed/tests/accessories.py b/blessed/tests/accessories.py index 3693e416..658fb9a9 100644 --- a/blessed/tests/accessories.py +++ b/blessed/tests/accessories.py @@ -45,12 +45,15 @@ def __init__(self, func): def __call__(self, *args, **kwargs): pid, master_fd = pty.fork() if pid is self._CHILD_PID: - cov = __import__('cov_core_init').init() # child process executes function, raises exception # if failed, causing a non-zero exit code, using the # protected _exit() function of ``os``; to prevent the # 'SystemExit' exception from being thrown. try: + try: + cov = __import__('cov_core_init').init() + except ImportError: + cov = None self.func(*args, **kwargs) except Exception: e_type, e_value, e_tb = sys.exc_info() @@ -67,8 +70,9 @@ def __call__(self, *args, **kwargs): os.close(sys.__stdin__.fileno()) os._exit(1) else: - cov.stop() - cov.save() + if cov is not None: + cov.stop() + cov.save() os._exit(0) exc_output = unicode() From c1e746643a772efb4c43ddc507de4aad806d0e6a Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 02:20:28 -0700 Subject: [PATCH 096/459] pytest -n causing internal errors --- tox.ini | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tox.ini b/tox.ini index 4e38fa02..05c3a64a 100644 --- a/tox.ini +++ b/tox.ini @@ -9,12 +9,11 @@ envlist = py26, # for any python, run simple pytest # with pep8 and pyflake checking deps = pytest - pytest-xdist pytest-pep8 pytest-flakes mock -commands = {envbindir}/py.test -n 2 \ +commands = {envbindir}/py.test -v \ -x --strict --pep8 --flakes \ blessed/tests \ {posargs} @@ -25,13 +24,12 @@ commands = {envbindir}/py.test -n 2 \ usedevelop = True deps = pytest pytest-cov - pytest-xdist pytest-pep8 pytest-flakes mock commands = pip install -q - {envbindir}/py.test -n 2 \ + {envbindir}/py.test -v \ -x --strict --pep8 --flakes \ --cov blessed \ blessed/tests {posargs} @@ -42,7 +40,7 @@ commands = pip install -q # for python3, test the version of blessed that is *installed*, # and not from source. This is because we use the 2to3 tool. changedir = {toxworkdir} -commands = {envbindir}/py.test \ +commands = {envbindir}/py.test -v \ -x --strict --pep8 --flakes \ {envsitepackagesdir}/blessed/tests \ {posargs} From 94de2acf00ef42f17049a597772282ecc1f844b3 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 02:20:44 -0700 Subject: [PATCH 097/459] blessings -> blessed rename --- blessed/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blessed/__init__.py b/blessed/__init__.py index 86552120..adb4be82 100644 --- a/blessed/__init__.py +++ b/blessed/__init__.py @@ -5,7 +5,7 @@ if ('3', '0', '0') <= _platform.python_version_tuple() < ('3', '2', '2+'): # Good till 3.2.10 # Python 3.x < 3.2.3 has a bug in which tparm() erroneously takes a string. - raise ImportError('Blessings needs Python 3.2.3 or greater for Python 3 ' + raise ImportError('Blessed needs Python 3.2.3 or greater for Python 3 ' 'support due to http://bugs.python.org/issue10570.') From dca61ad9df71f68fb9aa0e6d6ee4e77eeef1e0d1 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 02:20:57 -0700 Subject: [PATCH 098/459] stop & save coverage even on failure --- blessed/tests/accessories.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/blessed/tests/accessories.py b/blessed/tests/accessories.py index 658fb9a9..9cfbe9d3 100644 --- a/blessed/tests/accessories.py +++ b/blessed/tests/accessories.py @@ -68,6 +68,9 @@ def __call__(self, *args, **kwargs): os.close(sys.__stdout__.fileno()) os.close(sys.__stderr__.fileno()) os.close(sys.__stdin__.fileno()) + if cov is not None: + cov.stop() + cov.save() os._exit(1) else: if cov is not None: From 23349fdc38ad544e18a0f5d44014d2ae4494ae2f Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 02:21:13 -0700 Subject: [PATCH 099/459] test ImportError, py3.2 insupport, OrderedDict --- blessed/tests/test_core.py | 51 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/blessed/tests/test_core.py b/blessed/tests/test_core.py index 6c9ea4c1..8feb5ef3 100644 --- a/blessed/tests/test_core.py +++ b/blessed/tests/test_core.py @@ -4,7 +4,11 @@ from StringIO import StringIO except ImportError: from io import StringIO + +import collections +import platform import sys +import imp from accessories import ( as_subprocess, @@ -13,6 +17,9 @@ all_terms ) +import mock +import pytest + def test_export_only_Terminal(): """Ensure only Terminal instance is exported for import * statements.""" @@ -191,3 +198,47 @@ def child(): del warnings child() + + +@pytest.mark.skipif(platform.python_implementation() == 'PyPy', + reason='PyPy freezes') +def test_missing_ordereddict_uses_module(monkeypatch): + """ordereddict module is imported when without collections.OrderedDict.""" + import blessed.keyboard + + if hasattr(collections, 'OrderedDict'): + monkeypatch.delattr('collections.OrderedDict') + + try: + imp.reload(blessed.keyboard) + assert platform.python_version_tuple() < ('2', '7') # reached by py2.6 + except ImportError, err: + assert err.args[0] in ("No module named ordereddict", # py2 + "No module named 'ordereddict'") # py3 + sys.modules['ordereddict'] = mock.Mock() + sys.modules['ordereddict'].OrderedDict = -1 + imp.reload(blessed.keyboard) + assert blessed.keyboard.OrderedDict == -1 + del sys.modules['ordereddict'] + monkeypatch.undo() + imp.reload(blessed.keyboard) + + +@pytest.mark.skipif(platform.python_implementation() == 'PyPy', + reason='PyPy freezes') +def test_python3_2_raises_exception(monkeypatch): + """ordereddict module is imported when without collections.OrderedDict.""" + import blessed + + monkeypatch.setattr('platform.python_version_tuple', + lambda: ('3', '2', '2')) + + try: + imp.reload(blessed) + assert False, 'Exception should have been raised' + except ImportError, err: + assert err.args[0] == ( + 'Blessed needs Python 3.2.3 or greater for Python 3 ' + 'support due to http://bugs.python.org/issue10570.') + monkeypatch.undo() + imp.reload(blessed) From 42fedd4cac5b8f7d0a7ce42b60d312032d965925 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 02:21:38 -0700 Subject: [PATCH 100/459] test various textwrap options --- blessed/tests/test_wrap.py | 55 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/blessed/tests/test_wrap.py b/blessed/tests/test_wrap.py index 6cdba635..0577ec88 100644 --- a/blessed/tests/test_wrap.py +++ b/blessed/tests/test_wrap.py @@ -117,3 +117,58 @@ def child(kind): assert (len(internal_wrapped[-1]) == t.length(my_wrapped_colored[-1])) child(kind=all_terms) + + +def test_SequenceWrapper_drop_whitespace_subsequent_indent(): + """Test that text wrapping matches internal extra options.""" + WIDTH = 10 + + @as_subprocess + def child(): + # build a test paragraph, along with a very colorful version + t = TestTerminal() + pgraph = u''.join( + ('a', 'ab', 'abc', 'abcd', 'abcde', 'abcdef', 'abcdefgh', + 'abcdefghi', 'abcdefghij', 'abcdefghijk', 'abcdefghijkl', + 'abcdefghijklm', 'abcdefghijklmn', 'abcdefghijklmno',) * 4) + + pgraph_colored = u''.join([ + t.color(n % 7) + t.bold + ch + for n, ch in enumerate(pgraph)]) + + internal_wrapped = textwrap.wrap(pgraph, width=WIDTH, + break_long_words=False, + drop_whitespace=True, + subsequent_indent=4) + my_wrapped = t.wrap(pgraph, width=WIDTH) + my_wrapped_colored = t.wrap(pgraph_colored, width=WIDTH, + drop_whitespace=True, + subsequent_indent=4) + + # ensure we textwrap ascii the same as python + assert (internal_wrapped == my_wrapped) + + # ensure our first and last line wraps at its ends + first_l = internal_wrapped[0] + last_l = internal_wrapped[-1] + my_first_l = my_wrapped_colored[0] + my_last_l = my_wrapped_colored[-1] + assert (len(first_l) == t.length(my_first_l)) + assert (len(last_l) == t.length(my_last_l)) + assert (len(internal_wrapped[-1]) == t.length(my_wrapped_colored[-1])) + + # ensure our colored textwrap is the same line length + assert (len(internal_wrapped) == len(my_wrapped_colored)) + # test subsequent_indent= + internal_wrapped = textwrap.wrap(pgraph, WIDTH, break_long_words=False, + subsequent_indent=' '*4) + my_wrapped = t.wrap(pgraph, width=WIDTH, + subsequent_indent=' '*4) + my_wrapped_colored = t.wrap(pgraph_colored, width=WIDTH, + subsequent_indent=' '*4) + + assert (internal_wrapped == my_wrapped) + assert (len(internal_wrapped) == len(my_wrapped_colored)) + assert (len(internal_wrapped[-1]) == t.length(my_wrapped_colored[-1])) + + child() From 5e2bdb91eba84069992091a472394470cec7c014 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 02:21:52 -0700 Subject: [PATCH 101/459] test \r -- strange, why are some, but not all, length 9 ?? --- blessed/tests/test_length_sequence.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/blessed/tests/test_length_sequence.py b/blessed/tests/test_length_sequence.py index 8d3f8107..9a5e8c4b 100644 --- a/blessed/tests/test_length_sequence.py +++ b/blessed/tests/test_length_sequence.py @@ -75,7 +75,9 @@ def child(kind): # accounted for as a "length", as # will result in a printed column length of 12 (even # though columns 2-11 are non-destructive space - assert (t.length('\b') == -1) + assert (t.length(u'\b') == -1) + # XXX why are some terminals width of 9 here ?? + assert (t.length(u'\t') in (8, 9)) assert (t.length(t.move_left) == -1) if t.cub: assert (t.length(t.cub(10)) == -10) From 5a9bfa75f24955bf48a5ba5b756ff2bef690c9da Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 12:14:26 -0700 Subject: [PATCH 102/459] pep8/pyflakes compliance --- bin/dumb_fse.py | 2 +- bin/on_resize.py | 1 + ...test_keyboard.py => test_keyboard_keys.py} | 1 + bin/tprint.py | 1 - bin/worms.py | 6 +-- docs/conf.py | 44 ++++++++++--------- fabfile.py | 5 ++- 7 files changed, 32 insertions(+), 28 deletions(-) rename bin/{test_keyboard.py => test_keyboard_keys.py} (99%) diff --git a/bin/dumb_fse.py b/bin/dumb_fse.py index 297c60ae..462e3d15 100755 --- a/bin/dumb_fse.py +++ b/bin/dumb_fse.py @@ -3,7 +3,7 @@ # # "Why wont python let me read memory # from screen like assembler? That's dumb." -hellbeard -from __future__ import division +from __future__ import division, print_function import collections import functools from blessed import Terminal diff --git a/bin/on_resize.py b/bin/on_resize.py index aaec9e93..d7260c12 100755 --- a/bin/on_resize.py +++ b/bin/on_resize.py @@ -4,6 +4,7 @@ term = Terminal() + def on_resize(sig, action): print('height={t.height}, width={t.width}'.format(t=term)) diff --git a/bin/test_keyboard.py b/bin/test_keyboard_keys.py similarity index 99% rename from bin/test_keyboard.py rename to bin/test_keyboard_keys.py index 113827d3..d751af21 100755 --- a/bin/test_keyboard.py +++ b/bin/test_keyboard_keys.py @@ -2,6 +2,7 @@ from blessed import Terminal import sys + def main(): """ Displays all known key capabilities that may match the terminal. diff --git a/bin/tprint.py b/bin/tprint.py index f46427de..e85f5807 100755 --- a/bin/tprint.py +++ b/bin/tprint.py @@ -16,4 +16,3 @@ style = getattr(term, args.style) print(style(' '.join(args.text))) - diff --git a/bin/worms.py b/bin/worms.py index 024f770e..2afdeaad 100755 --- a/bin/worms.py +++ b/bin/worms.py @@ -1,9 +1,8 @@ #!/usr/bin/env python -from __future__ import division +from __future__ import division, print_function from collections import namedtuple from random import randrange from functools import partial -from itertools import takewhile, count from blessed import Terminal term = Terminal() @@ -118,7 +117,7 @@ def main(): # ensure new nibble is regenerated outside of worm while hit_any(n_nibble, worm): - n_nibble = next_nibble(term, nibble, head, worm) + n_nibble = new_nibble(term, nibble, head, worm) # new worm_length & speed, if hit. worm_length = next_wormlength(nibble, head, worm_length) @@ -140,7 +139,6 @@ def main(): else: echo(color_bg(u' ')) - # display new worm head each turn, regardless. echo(term.move(*head)) echo(color_head(u'\u263a')) diff --git a/docs/conf.py b/docs/conf.py index e9ed9ebf..34a409f9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- # -# blessings documentation build configuration file, created by +# blessed documentation build configuration file, created by # sphinx-quickstart on Thu Mar 31 13:40:27 2011. # -# This file is execfile()d with the current directory set to its containing dir. +# This file is execfile()d with the current directory set to its +# containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. @@ -11,22 +12,21 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os - -import blessings +import blessed +#import sys, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) -# -- General configuration ----------------------------------------------------- +# -- General configuration ---------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] # Add any paths that contain templates here, relative to this directory. @@ -42,8 +42,8 @@ master_doc = 'index' # General information about the project. -project = u'Blessings' -copyright = u'2011, Erik Rose' +project = u'Blessed' +copyright = u'2011 Erik Rose, 2014 Jeff Quast' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -68,7 +68,8 @@ # directories to ignore when looking for source files. exclude_patterns = ['_build'] -# The reST default role (used for this markup: `text`) to use for all documents. +# The reST default role (used for this markup: `text`) to use for all +# documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. @@ -89,7 +90,7 @@ #modindex_common_prefix = [] -# -- Options for HTML output --------------------------------------------------- +# -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. @@ -166,10 +167,10 @@ #html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'blessingsdoc' +htmlhelp_basename = 'blesseddoc' -# -- Options for LaTeX output -------------------------------------------------- +# -- Options for LaTeX output ------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' @@ -178,10 +179,11 @@ #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). +# (source start file, target name, title, author, documentclass +# [howto/manual]). latex_documents = [ - ('index', 'blessings.tex', u'Blessings Documentation', - u'Erik Rose', 'manual'), + ('index', 'blessed.tex', u'Blessed Documentation', + u'Jeff Quast', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -208,11 +210,13 @@ #latex_domain_indices = True -# -- Options for manual page output -------------------------------------------- +# -- Options for manual page output ------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'Blessings', u'Blessings Documentation', - [u'Erik Rose'], 1) + ('index', 'Blessed', u'Blessed Documentation', + [u'Jeff Quast'], 1) ] + +del blessed # imported but unused diff --git a/fabfile.py b/fabfile.py index af98c0d8..27b418e5 100644 --- a/fabfile.py +++ b/fabfile.py @@ -14,8 +14,9 @@ ROOT = abspath(dirname(__file__)) -environ['PYTHONPATH'] = (((environ['PYTHONPATH'] + ':') if - environ.get('PYTHONPATH') else '') + ROOT) +environ['PYTHONPATH'] = (((environ['PYTHONPATH'] + ':') + if environ.get('PYTHONPATH') + else '') + ROOT) def doc(kind='html'): From c09d8396f7d8e6ef74f5e66ea1ea335916c6d5e3 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 12:14:53 -0700 Subject: [PATCH 103/459] reset warnings filter after use --- blessed/tests/test_core.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/blessed/tests/test_core.py b/blessed/tests/test_core.py index 8feb5ef3..95ffdbc6 100644 --- a/blessed/tests/test_core.py +++ b/blessed/tests/test_core.py @@ -160,8 +160,6 @@ def child(): try: # a second instantiation raises UserWarning term = TestTerminal(kind="vt220", force_styling=True) - assert not term.is_a_tty or False, 'Should have thrown exception' - except UserWarning: err = sys.exc_info()[1] assert (err.args[0].startswith( @@ -169,8 +167,10 @@ def child(): ), err.args[0] assert ('a terminal of kind "xterm-256color" will ' 'continue to be returned' in err.args[0]), err.args[0] - finally: - del warnings + else: + # unless term is not a tty and setupterm() is not called + assert not term.is_a_tty or False, 'Should have thrown exception' + warnings.resetwarnings() child() @@ -188,14 +188,14 @@ def child(): try: term = TestTerminal(kind='unknown', force_styling=True) - assert not term.is_a_tty and not term.does_styling, ( - 'Should have thrown exception') - assert (term.number_of_colors == 0) except UserWarning: err = sys.exc_info()[1] assert err.args[0] == 'Failed to setupterm(kind=unknown)' - finally: - del warnings + else: + assert not term.is_a_tty and not term.does_styling, ( + 'Should have thrown exception') + assert (term.number_of_colors == 0) + warnings.resetwarnings() child() @@ -211,7 +211,6 @@ def test_missing_ordereddict_uses_module(monkeypatch): try: imp.reload(blessed.keyboard) - assert platform.python_version_tuple() < ('2', '7') # reached by py2.6 except ImportError, err: assert err.args[0] in ("No module named ordereddict", # py2 "No module named 'ordereddict'") # py3 @@ -222,6 +221,8 @@ def test_missing_ordereddict_uses_module(monkeypatch): del sys.modules['ordereddict'] monkeypatch.undo() imp.reload(blessed.keyboard) + else: + assert platform.python_version_tuple() < ('2', '7') # reached by py2.6 @pytest.mark.skipif(platform.python_implementation() == 'PyPy', @@ -235,10 +236,11 @@ def test_python3_2_raises_exception(monkeypatch): try: imp.reload(blessed) - assert False, 'Exception should have been raised' except ImportError, err: assert err.args[0] == ( 'Blessed needs Python 3.2.3 or greater for Python 3 ' 'support due to http://bugs.python.org/issue10570.') monkeypatch.undo() imp.reload(blessed) + else: + assert False, 'Exception should have been raised' From 3520a4a093e7653da4cdf2f2d29fffe0c12cd2e3 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 12:15:13 -0700 Subject: [PATCH 104/459] remove UnusedImport from setup.py --- setup.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 2f1f5bc1..487baefb 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -from setuptools import setup, find_packages, Command -from setuptools.command.develop import develop -from setuptools.command.test import test +from setuptools import setup import sys import os From 4c709af08c83c8570018a8c91526e87b019baf6f Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 12:16:10 -0700 Subject: [PATCH 105/459] whitespace nit --- tox.ini | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/tox.ini b/tox.ini index 05c3a64a..f34d8652 100644 --- a/tox.ini +++ b/tox.ini @@ -14,9 +14,8 @@ deps = pytest mock commands = {envbindir}/py.test -v \ - -x --strict --pep8 --flakes \ - blessed/tests \ - {posargs} + -x --strict --pep8 --flakes \ + blessed/tests {posargs} [testenv:py27] @@ -28,11 +27,9 @@ deps = pytest pytest-flakes mock -commands = pip install -q - {envbindir}/py.test -v \ - -x --strict --pep8 --flakes \ - --cov blessed \ - blessed/tests {posargs} +commands = {envbindir}/py.test -v \ + -x --strict --pep8 --flakes \ + --cov blessed {posargs} coverage report @@ -41,6 +38,5 @@ commands = pip install -q # and not from source. This is because we use the 2to3 tool. changedir = {toxworkdir} commands = {envbindir}/py.test -v \ - -x --strict --pep8 --flakes \ - {envsitepackagesdir}/blessed/tests \ - {posargs} + -x --strict --pep8 --flakes \ + {envsitepackagesdir}/blessed/tests {posargs} From e23c5f4352a807da2a59440aec5678977d518233 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 12:16:36 -0700 Subject: [PATCH 106/459] unittest binpacked terminal warning --- blessed/tests/test_sequences.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index e2aebe23..362b2566 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -20,6 +20,7 @@ ) import pytest +import mock def test_capability(): @@ -106,12 +107,30 @@ def child(kind): ), err else: assert 'warnings should have been emitted.' - finally: - del warnings + warnings.resetwarnings() child(unsupported_sequence_terminals) +def test_unit_binpacked_unittest(unsupported_sequence_terminals): + """Unit Test known binary-packed terminals emit a warning (travis-safe).""" + import warnings + from blessed.sequences import (_BINTERM_UNSUPPORTED_MSG, + init_sequence_patterns) + warnings.filterwarnings("error", category=UserWarning) + term = mock.Mock() + term._kind = unsupported_sequence_terminals + + try: + init_sequence_patterns(term) + except UserWarning: + err = sys.exc_info()[1] + assert err.args[0] == _BINTERM_UNSUPPORTED_MSG + else: + assert False, 'Previous stmt should have raised exception.' + warnings.resetwarnings() + + def test_merge_sequences(): """Test sequences are filtered and ordered longest-first.""" from blessed.sequences import _merge_sequences From d1452edca9c6dff2681947f54f909f1f890f8268 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 12:21:01 -0700 Subject: [PATCH 107/459] introduce .strip() and .strip_seqs() the former is better used as a replacement for .strip() in text wrapping functions, so that spaces containing sequences are equivalent to u'' for "empty line" or "drop whitespace" evaluation. very simply, T.length(S) == len(T.strip_seqs()) --- blessed/sequences.py | 59 ++++++++--- blessed/terminal.py | 17 +++ blessed/tests/test_length_sequence.py | 6 +- blessed/tests/test_wrap.py | 146 ++++++++++++-------------- 4 files changed, 133 insertions(+), 95 deletions(-) diff --git a/blessed/sequences.py b/blessed/sequences.py index fddc329c..f0b6ec48 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -292,6 +292,9 @@ def _wrap_chunks(self, chunks): if self.width <= 0 or not isinstance(self.width, int): raise ValueError("invalid width %r(%s) (must be integer > 0)" % ( self.width, type(self.width))) + term = self.term + drop_whitespace = not hasattr(self, 'drop_whitespace' + ) or self.drop_whitespace chunks.reverse() while chunks: cur_line = [] @@ -301,22 +304,20 @@ def _wrap_chunks(self, chunks): else: indent = self.initial_indent width = self.width - len(indent) - if (not hasattr(self, 'drop_whitespace') - or self.drop_whitespace) and ( - chunks[-1].strip() == '' and lines): + if drop_whitespace and ( + Sequence(chunks[-1], term).strip() == '' and lines): del chunks[-1] while chunks: - chunk_len = Sequence(chunks[-1], self.term).length() + chunk_len = Sequence(chunks[-1], term).length() if cur_len + chunk_len <= width: cur_line.append(chunks.pop()) cur_len += chunk_len else: break - if chunks and Sequence(chunks[-1], self.term).length() > width: + if chunks and Sequence(chunks[-1], term).length() > width: self._handle_long_word(chunks, cur_line, cur_len, width) - if (not hasattr(self, 'drop_whitespace') - or self.drop_whitespace) and ( - cur_line and cur_line[-1].strip() == ''): + if drop_whitespace and ( + cur_line and Sequence(cur_line[-1], term).strip() == ''): del cur_line[-1] if cur_line: lines.append(indent + u''.join(cur_line)) @@ -353,29 +354,55 @@ def length(self): (escape) sequences. Although accounted for, strings containing sequences such as 'clear' will not give accurate returns, it is considered un-lengthy (length of 0). + + Strings contaning term.left or '\b' will cause "overstrike", but + a length less than 0 is not ever returned. So '_\b+' is a length of + 1 ('+'), but '\b' is simply a length of 0. + """ + # TODO(jquast): Should we implement the terminal printable + # width of 'East Asian Fullwidth' and 'East Asian Wide' characters, + # which can take 2 cells, see http://www.unicode.org/reports/tr11/ + # and http://www.gossamer-threads.com/lists/python/bugs/972834 + return len(self.strip_seqs()) + + def strip(self): + """ S.strip() -> str + + Strips sequences and whitespaces of ``S`` and returns. + """ + return self.strip_seqs().strip() + + def strip_seqs(self): + """ S.strip_seqs() -> str + + Return a string without sequences for a string that contains + (most types) of (escape) sequences for the Terminal with which + they were created. """ # nxt: points to first character beyond current escape sequence. # width: currently estimated display length. - nxt = width = 0 + outp = u'' + nxt = 0 for idx in range(0, unicode.__len__(self)): # account for width of sequences that contain padding (a sort of # SGR-equivalent cheat for the python equivalent of ' '*N, for # very large values of N that may xmit fewer bytes than many raw # spaces. It should be noted, however, that this is a # non-destructive space. - width += horizontal_distance(self[idx:], self._term) + width = horizontal_distance(self[idx:], self._term) + if width > 0: + outp += u' ' * horizontal_distance(self[idx:], self._term) + elif width < 0: + # \b causes the previous character to be trimmed + outp = outp[:width] if idx == nxt: # point beyond this sequence nxt = idx + measure_length(self[idx:], self._term) if nxt <= idx: - # TODO: - # 'East Asian Fullwidth' and 'East Asian Wide' characters - # can take 2 cells, see http://www.unicode.org/reports/tr11/ - # and http://www.gossamer-threads.com/lists/python/bugs/972834 - width += 1 + outp += self[idx] # point beyond next sequence, if any, otherwise next character nxt = idx + measure_length(self[idx:], self._term) + 1 - return width + return outp def measure_length(ucs, term): diff --git a/blessed/terminal.py b/blessed/terminal.py index 1c96bd82..dc9be982 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -452,6 +452,23 @@ def length(self, text): """ return sequences.Sequence(text, self).length() + def strip(self, text): + """T.strip(text) -> int + + Return string ``text`` stripped of its whitespace *and* sequences. + Text containing backspace or term.left will "overstrike", so that + the string u"_\\b" or u"__\\b\\b=" becomes u"x", not u"=" (as would + actually be printed). + """ + return sequences.Sequence(text, self).strip() + + def strip_seqs(self, text): + """T.strip_seqs(text) -> int + + Return string ``text`` stripped only of its sequences. + """ + return sequences.Sequence(text, self).strip_seqs() + def wrap(self, text, width=None, **kwargs): """T.wrap(text, [width=None, indent=u'', ...]) -> unicode diff --git a/blessed/tests/test_length_sequence.py b/blessed/tests/test_length_sequence.py index 9a5e8c4b..13f2ed65 100644 --- a/blessed/tests/test_length_sequence.py +++ b/blessed/tests/test_length_sequence.py @@ -75,12 +75,12 @@ def child(kind): # accounted for as a "length", as # will result in a printed column length of 12 (even # though columns 2-11 are non-destructive space - assert (t.length(u'\b') == -1) + assert (t.length(u'x\b') == 0) # XXX why are some terminals width of 9 here ?? assert (t.length(u'\t') in (8, 9)) - assert (t.length(t.move_left) == -1) + assert (t.length(u'_' + t.move_left) == 0) if t.cub: - assert (t.length(t.cub(10)) == -10) + assert (t.length((u'_' * 10) + t.cub(10)) == 0) assert (t.length(t.move_right) == 1) if t.cuf: assert (t.length(t.cuf(10)) == 10) diff --git a/blessed/tests/test_wrap.py b/blessed/tests/test_wrap.py index 0577ec88..26a1dfb3 100644 --- a/blessed/tests/test_wrap.py +++ b/blessed/tests/test_wrap.py @@ -15,30 +15,52 @@ import pytest -@pytest.mark.skipif(platform.python_implementation() == 'PyPy', - reason='PyPy fails TIOCSWINSZ') -def test_SequenceWrapper(all_terms, many_columns): - """Test that text wrapping accounts for sequences correctly.""" +def test_SequenceWrapper_invalid_width(): + """Test exception thrown from invalid width""" + WIDTH = 'XXX' + @as_subprocess - def child(kind, lines=25, cols=80): - # set the pty's virtual window size - val = struct.pack('HHHH', lines, cols, 0, 0) - fcntl.ioctl(sys.__stdout__.fileno(), termios.TIOCSWINSZ, val) + def child(): + t = TestTerminal() + try: + my_wrapped = t.wrap(u'', WIDTH) + except ValueError, err: + assert err.args[0] == ( + "invalid width %r(%s) (must be integer > 0)" % ( + WIDTH, type(WIDTH))) + else: + assert False, 'Previous stmt should have raised exception.' + del my_wrapped # assigned but never used + +def test_SequenceWrapper_drop_whitespace_subsequent_indent(): + """Test that text wrapping matches internal extra options.""" + WIDTH = 10 + + @as_subprocess + def child(): # build a test paragraph, along with a very colorful version - t = TestTerminal(kind=kind) - pgraph = u''.join( + t = TestTerminal() + pgraph = u' '.join( ('a', 'ab', 'abc', 'abcd', 'abcde', 'abcdef', 'abcdefgh', 'abcdefghi', 'abcdefghij', 'abcdefghijk', 'abcdefghijkl', - 'abcdefghijklm', 'abcdefghijklmn', 'abcdefghijklmno',) * 4) + 'abcdefghijklm', 'abcdefghijklmn', 'abcdefghijklmno ',) + * 4) + pgraph_colored = u''.join([ - t.color(n % 7) + t.bold + ch + t.color(n % 7) + t.bold + ch if ch != ' ' else ' ' for n, ch in enumerate(pgraph)]) - internal_wrapped = textwrap.wrap(pgraph, t.width, - break_long_words=False) - my_wrapped = t.wrap(pgraph) - my_wrapped_colored = t.wrap(pgraph_colored) + internal_wrapped = textwrap.wrap(pgraph, width=WIDTH, + break_long_words=False, + drop_whitespace=True, + subsequent_indent=u' '*3) + my_wrapped = t.wrap(pgraph, width=WIDTH, + drop_whitespace=True, + subsequent_indent=u' '*3) + my_wrapped_colored = t.wrap(pgraph_colored, width=WIDTH, + drop_whitespace=True, + subsequent_indent=u' '*3) # ensure we textwrap ascii the same as python assert (internal_wrapped == my_wrapped) @@ -49,46 +71,41 @@ def child(kind, lines=25, cols=80): my_first_l = my_wrapped_colored[0] my_last_l = my_wrapped_colored[-1] assert (len(first_l) == t.length(my_first_l)) - assert (len(last_l) == t.length(my_last_l)) + assert (len(last_l) == t.length(my_last_l)), (internal_wrapped, + my_wrapped_colored) assert (len(internal_wrapped[-1]) == t.length(my_wrapped_colored[-1])) # ensure our colored textwrap is the same line length assert (len(internal_wrapped) == len(my_wrapped_colored)) - # test subsequent_indent= - internal_wrapped = textwrap.wrap(pgraph, t.width, - break_long_words=False, - subsequent_indent=' '*4) - my_wrapped = t.wrap(pgraph, subsequent_indent=' '*4) - my_wrapped_colored = t.wrap(pgraph_colored, subsequent_indent=' '*4) - assert (internal_wrapped == my_wrapped) - assert (len(internal_wrapped) == len(my_wrapped_colored)) - assert (len(internal_wrapped[-1]) == t.length(my_wrapped_colored[-1])) - - child(kind=all_terms, lines=25, cols=many_columns) + child() -def test_SequenceWrapper_27(all_terms): +@pytest.mark.skipif(platform.python_implementation() == 'PyPy', + reason='PyPy fails TIOCSWINSZ') +def test_SequenceWrapper(all_terms, many_columns): """Test that text wrapping accounts for sequences correctly.""" - WIDTH = 27 - @as_subprocess - def child(kind): + def child(kind, lines=25, cols=80): + + # set the pty's virtual window size + val = struct.pack('HHHH', lines, cols, 0, 0) + fcntl.ioctl(sys.__stdout__.fileno(), termios.TIOCSWINSZ, val) + # build a test paragraph, along with a very colorful version t = TestTerminal(kind=kind) - pgraph = u''.join( + pgraph = u' '.join( ('a', 'ab', 'abc', 'abcd', 'abcde', 'abcdef', 'abcdefgh', 'abcdefghi', 'abcdefghij', 'abcdefghijk', 'abcdefghijkl', 'abcdefghijklm', 'abcdefghijklmn', 'abcdefghijklmno',) * 4) - pgraph_colored = u''.join([ t.color(n % 7) + t.bold + ch for n, ch in enumerate(pgraph)]) - internal_wrapped = textwrap.wrap(pgraph, width=WIDTH, + internal_wrapped = textwrap.wrap(pgraph, t.width, break_long_words=False) - my_wrapped = t.wrap(pgraph, width=WIDTH) - my_wrapped_colored = t.wrap(pgraph_colored, width=WIDTH) + my_wrapped = t.wrap(pgraph) + my_wrapped_colored = t.wrap(pgraph_colored) # ensure we textwrap ascii the same as python assert (internal_wrapped == my_wrapped) @@ -102,35 +119,22 @@ def child(kind): assert (len(last_l) == t.length(my_last_l)) assert (len(internal_wrapped[-1]) == t.length(my_wrapped_colored[-1])) - # ensure our colored textwrap is the same line length - assert (len(internal_wrapped) == len(my_wrapped_colored)) - # test subsequent_indent= - internal_wrapped = textwrap.wrap(pgraph, WIDTH, break_long_words=False, - subsequent_indent=' '*4) - my_wrapped = t.wrap(pgraph, width=WIDTH, - subsequent_indent=' '*4) - my_wrapped_colored = t.wrap(pgraph_colored, width=WIDTH, - subsequent_indent=' '*4) - - assert (internal_wrapped == my_wrapped) - assert (len(internal_wrapped) == len(my_wrapped_colored)) - assert (len(internal_wrapped[-1]) == t.length(my_wrapped_colored[-1])) - - child(kind=all_terms) + child(kind=all_terms, lines=25, cols=many_columns) -def test_SequenceWrapper_drop_whitespace_subsequent_indent(): - """Test that text wrapping matches internal extra options.""" - WIDTH = 10 +def test_SequenceWrapper_27(all_terms): + """Test that text wrapping accounts for sequences correctly.""" + WIDTH = 27 @as_subprocess - def child(): + def child(kind): # build a test paragraph, along with a very colorful version - t = TestTerminal() - pgraph = u''.join( + t = TestTerminal(kind=kind) + pgraph = u' '.join( ('a', 'ab', 'abc', 'abcd', 'abcde', 'abcdef', 'abcdefgh', 'abcdefghi', 'abcdefghij', 'abcdefghijk', 'abcdefghijkl', - 'abcdefghijklm', 'abcdefghijklmn', 'abcdefghijklmno',) * 4) + 'abcdefghijklm', 'abcdefghijklmn', 'abcdefghijklmno ',) + * 8) pgraph_colored = u''.join([ t.color(n % 7) + t.bold + ch @@ -138,12 +142,13 @@ def child(): internal_wrapped = textwrap.wrap(pgraph, width=WIDTH, break_long_words=False, - drop_whitespace=True, - subsequent_indent=4) - my_wrapped = t.wrap(pgraph, width=WIDTH) + drop_whitespace=False) + my_wrapped = t.wrap(pgraph, width=WIDTH, + break_long_words=False, + drop_whitespace=False) my_wrapped_colored = t.wrap(pgraph_colored, width=WIDTH, - drop_whitespace=True, - subsequent_indent=4) + break_long_words=False, + drop_whitespace=False) # ensure we textwrap ascii the same as python assert (internal_wrapped == my_wrapped) @@ -159,16 +164,5 @@ def child(): # ensure our colored textwrap is the same line length assert (len(internal_wrapped) == len(my_wrapped_colored)) - # test subsequent_indent= - internal_wrapped = textwrap.wrap(pgraph, WIDTH, break_long_words=False, - subsequent_indent=' '*4) - my_wrapped = t.wrap(pgraph, width=WIDTH, - subsequent_indent=' '*4) - my_wrapped_colored = t.wrap(pgraph_colored, width=WIDTH, - subsequent_indent=' '*4) - assert (internal_wrapped == my_wrapped) - assert (len(internal_wrapped) == len(my_wrapped_colored)) - assert (len(internal_wrapped[-1]) == t.length(my_wrapped_colored[-1])) - - child() + child(kind=all_terms) From e91cefc773e89767431afa6484db5d665f9a2824 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 12:48:57 -0700 Subject: [PATCH 108/459] bring sequences.py to 100% coverage --- blessed/tests/test_sequences.py | 44 +++++++++++++++++++++++++++++++++ blessed/tests/test_wrap.py | 8 +++--- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index 362b2566..ad9f63f1 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -435,3 +435,47 @@ def child(kind): assert (t.clear('x') == 'x') child(all_standard_terms) + + +def test_bnc_parameter_emits_warning(): + """A fake capability without target digits emits a warning.""" + import warnings + from blessed.sequences import _build_numeric_capability + + # given, + warnings.filterwarnings("error", category=UserWarning) + term = mock.Mock() + fake_cap = lambda *args: u'NO-DIGIT' + term.fake_cap = fake_cap + + # excersize, + try: + _build_numeric_capability(term, 'fake_cap', base_num=1984) + except UserWarning: + err = sys.exc_info()[1] + assert err.args[0].startswith('Unknown parameter in ') + else: + assert False, 'Previous stmt should have raised exception.' + warnings.resetwarnings() + + +def test_bna_parameter_emits_warning(): + """A fake capability without any digits emits a warning.""" + import warnings + from blessed.sequences import _build_any_numeric_capability + + # given, + warnings.filterwarnings("error", category=UserWarning) + term = mock.Mock() + fake_cap = lambda *args: 'NO-DIGIT' + term.fake_cap = fake_cap + + # excersize, + try: + _build_any_numeric_capability(term, 'fake_cap') + except UserWarning: + err = sys.exc_info()[1] + assert err.args[0].startswith('Missing numerics in ') + else: + assert False, 'Previous stmt should have raised exception.' + warnings.resetwarnings() diff --git a/blessed/tests/test_wrap.py b/blessed/tests/test_wrap.py index 26a1dfb3..3d18f1d2 100644 --- a/blessed/tests/test_wrap.py +++ b/blessed/tests/test_wrap.py @@ -17,20 +17,22 @@ def test_SequenceWrapper_invalid_width(): """Test exception thrown from invalid width""" - WIDTH = 'XXX' + WIDTH = -3 @as_subprocess def child(): t = TestTerminal() try: - my_wrapped = t.wrap(u'', WIDTH) + my_wrapped = t.wrap(u'------- -------------', WIDTH) except ValueError, err: assert err.args[0] == ( "invalid width %r(%s) (must be integer > 0)" % ( WIDTH, type(WIDTH))) else: assert False, 'Previous stmt should have raised exception.' - del my_wrapped # assigned but never used + del my_wrapped # assigned but never used + + child() def test_SequenceWrapper_drop_whitespace_subsequent_indent(): From 0496c9e83ee553e2059b595bc1366046207696f2 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 13:58:14 -0700 Subject: [PATCH 109/459] increasing test coverage of blessed.terminal --- blessed/terminal.py | 30 +++--- blessed/tests/test_core.py | 135 +++++++++++++++++++++++--- blessed/tests/test_keyboard.py | 56 +++++++++-- blessed/tests/test_length_sequence.py | 48 +++++++++ 4 files changed, 233 insertions(+), 36 deletions(-) diff --git a/blessed/terminal.py b/blessed/terminal.py index dc9be982..b80738f7 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -80,12 +80,12 @@ def __init__(self, kind=None, stream=None, force_styling=False): """ global _CUR_TERM - self.stream_kb = None + self.keyboard_fd = None # default stream is stdout, keyboard only valid as stdin with stdout. - if stream is None: + if stream is None or stream == sys.__stdout__: stream = sys.__stdout__ - self.stream_kb = sys.__stdin__.fileno() + self.keyboard_fd = sys.__stdin__.fileno() try: stream_fd = (stream.fileno() if hasattr(stream, 'fileno') @@ -520,7 +520,7 @@ def kbhit(self, timeout=0): # then False is returned. Otherwise, when timeout is 0, we continue to # block indefinitely (default). stime = time.time() - check_r, check_w, check_x = [self.stream_kb], [], [] + check_r, check_w, check_x = [self.keyboard_fd], [], [] while True: try: @@ -563,15 +563,17 @@ def cbreak(self): http://www.unixwiz.net/techtips/termios-vmin-vtime.html """ assert self.is_a_tty, 'stream is not a tty.' - if self.stream_kb is not None: + if self.keyboard_fd is not None: # save current terminal mode, - save_mode = termios.tcgetattr(self.stream_kb) - tty.setcbreak(self.stream_kb, termios.TCSANOW) + save_mode = termios.tcgetattr(self.keyboard_fd) + tty.setcbreak(self.keyboard_fd, termios.TCSANOW) try: yield finally: # restore prior mode, - termios.tcsetattr(self.stream_kb, termios.TCSAFLUSH, save_mode) + termios.tcsetattr(self.keyboard_fd, + termios.TCSAFLUSH, + save_mode) else: yield @@ -584,15 +586,17 @@ def raw(self): through uninterpreted, instead of generating a signal. """ assert self.is_a_tty, 'stream is not a tty.' - if self.stream_kb is not None: + if self.keyboard_fd is not None: # save current terminal mode, - save_mode = termios.tcgetattr(self.stream_kb) - tty.setraw(self.stream_kb, termios.TCSANOW) + save_mode = termios.tcgetattr(self.keyboard_fd) + tty.setraw(self.keyboard_fd, termios.TCSANOW) try: yield finally: # restore prior mode, - termios.tcsetattr(self.stream_kb, termios.TCSAFLUSH, save_mode) + termios.tcsetattr(self.keyboard_fd, + termios.TCSAFLUSH, + save_mode) else: yield @@ -634,7 +638,7 @@ def _timeleft(stime, timeout): def _decode_next(): """Read and decode next byte from stdin.""" - byte = os.read(self.stream_kb, 1) + byte = os.read(self.keyboard_fd, 1) return self._keyboard_decoder.decode(byte, final=False) resolve = functools.partial(keyboard.resolve_sequence, diff --git a/blessed/tests/test_core.py b/blessed/tests/test_core.py index 95ffdbc6..92511414 100644 --- a/blessed/tests/test_core.py +++ b/blessed/tests/test_core.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Core blessed Terminal() tests.""" +"Core blessed Terminal() tests." try: from StringIO import StringIO except ImportError: @@ -9,6 +9,7 @@ import platform import sys import imp +import os from accessories import ( as_subprocess, @@ -22,13 +23,13 @@ def test_export_only_Terminal(): - """Ensure only Terminal instance is exported for import * statements.""" + "Ensure only Terminal instance is exported for import * statements." import blessed assert blessed.__all__ == ['Terminal'] def test_null_location(all_terms): - """Make sure ``location()`` with no args just does position restoration.""" + "Make sure ``location()`` with no args just does position restoration." @as_subprocess def child(kind): t = TestTerminal(stream=StringIO(), force_styling=True) @@ -42,7 +43,7 @@ def child(kind): def test_flipped_location_move(all_terms): - """``location()`` and ``move()`` receive counter-example arguments.""" + "``location()`` and ``move()`` receive counter-example arguments." @as_subprocess def child(kind): buf = StringIO() @@ -57,7 +58,7 @@ def child(kind): def test_null_fileno(): - """Make sure ``Terminal`` works when ``fileno`` is ``None``.""" + "Make sure ``Terminal`` works when ``fileno`` is ``None``." @as_subprocess def child(): # This simulates piping output to another program. @@ -70,7 +71,7 @@ def child(): def test_number_of_colors_without_tty(): - """``number_of_colors`` should return 0 when there's no tty.""" + "``number_of_colors`` should return 0 when there's no tty." @as_subprocess def child_256_nostyle(): t = TestTerminal(stream=StringIO()) @@ -100,7 +101,7 @@ def child_0_forcestyle(): def test_number_of_colors_with_tty(): - """``number_of_colors`` should work.""" + "test ``number_of_colors`` 0, 8, and 256." @as_subprocess def child_256(): t = TestTerminal() @@ -122,7 +123,7 @@ def child_0(): def test_init_descriptor_always_initted(all_terms): - """Test height and width with non-tty Terminals.""" + "Test height and width with non-tty Terminals." @as_subprocess def child(kind): t = TestTerminal(kind=kind, stream=StringIO()) @@ -136,7 +137,7 @@ def child(kind): def test_force_styling_none(all_terms): - """If ``force_styling=None`` is used, don't ever do styling.""" + "If ``force_styling=None`` is used, don't ever do styling." @as_subprocess def child(kind): t = TestTerminal(kind=kind, force_styling=None) @@ -148,7 +149,7 @@ def child(kind): def test_setupterm_singleton_issue33(): - """A warning is emitted if a new terminal ``kind`` is used per process.""" + "A warning is emitted if a new terminal ``kind`` is used per process." @as_subprocess def child(): import warnings @@ -176,7 +177,7 @@ def child(): def test_setupterm_invalid_issue39(): - """A warning is emitted if TERM is invalid.""" + "A warning is emitted if TERM is invalid." # https://bugzilla.mozilla.org/show_bug.cgi?id=878089 # if TERM is unset, defaults to 'unknown', which should @@ -194,7 +195,26 @@ def child(): else: assert not term.is_a_tty and not term.does_styling, ( 'Should have thrown exception') - assert (term.number_of_colors == 0) + warnings.resetwarnings() + + child() + + +def test_setupterm_invalid_has_no_styling(): + "An unknown TERM type does not perform styling." + # https://bugzilla.mozilla.org/show_bug.cgi?id=878089 + + # if TERM is unset, defaults to 'unknown', which should + # fail to lookup and emit a warning, only. + @as_subprocess + def child(): + import warnings + warnings.filterwarnings("ignore", category=UserWarning) + + term = TestTerminal(kind='unknown', force_styling=True) + assert term._kind is None + assert term.does_styling is False + assert term.number_of_colors == 0 warnings.resetwarnings() child() @@ -203,7 +223,7 @@ def child(): @pytest.mark.skipif(platform.python_implementation() == 'PyPy', reason='PyPy freezes') def test_missing_ordereddict_uses_module(monkeypatch): - """ordereddict module is imported when without collections.OrderedDict.""" + "ordereddict module is imported when without collections.OrderedDict." import blessed.keyboard if hasattr(collections, 'OrderedDict'): @@ -228,7 +248,7 @@ def test_missing_ordereddict_uses_module(monkeypatch): @pytest.mark.skipif(platform.python_implementation() == 'PyPy', reason='PyPy freezes') def test_python3_2_raises_exception(monkeypatch): - """ordereddict module is imported when without collections.OrderedDict.""" + "Test python version 3.0 through 3.2 raises an exception." import blessed monkeypatch.setattr('platform.python_version_tuple', @@ -244,3 +264,90 @@ def test_python3_2_raises_exception(monkeypatch): imp.reload(blessed) else: assert False, 'Exception should have been raised' + + +def test_IOUnsupportedOperation_dummy(monkeypatch): + "Ensure dummy exception is used when io is without UnsupportedOperation." + import blessed.terminal + import io + if hasattr(io, 'UnsupportedOperation'): + monkeypatch.delattr('io.UnsupportedOperation') + + imp.reload(blessed.terminal) + assert blessed.terminal.IOUnsupportedOperation.__doc__.startswith( + "A dummy exception to take the place of") + monkeypatch.undo() + imp.reload(blessed.terminal) + + +def test_without_dunder(): + "Ensure dunder does not remain in module (py2x InterruptedError test." + import blessed.terminal + assert '_' not in dir(blessed.terminal) + + +def test_IOUnsupportedOperation(): + "Ensure stream that throws IOUnsupportedOperation results in non-tty." + @as_subprocess + def child(): + import blessed.terminal + + def side_effect(): + raise blessed.terminal.IOUnsupportedOperation + + mock_stream = mock.Mock() + mock_stream.fileno = side_effect + + term = TestTerminal(stream=mock_stream) + assert term.stream == mock_stream + assert term.does_styling is False + assert term.is_a_tty is False + assert term.number_of_colors is 0 + + child() + + +def test_winsize_IOError_returns_environ(): + """When _winsize raises IOError, defaults from os.environ given.""" + @as_subprocess + def child(): + def side_effect(fd): + raise IOError + + term = TestTerminal() + term._winsize = side_effect + os.environ['COLUMNS'] = '1984' + os.environ['LINES'] = '1888' + assert term._height_and_width() == (1888, 1984, None, None) + + child() + + +def test_yield_fullscreen(all_terms): + "Ensure ``fullscreen()`` writes enter_fullscreen and exit_fullscreen." + @as_subprocess + def child(kind): + t = TestTerminal(stream=StringIO(), force_styling=True) + t.enter_fullscreen = u'BEGIN' + t.exit_fullscreen = u'END' + with t.fullscreen(): + pass + expected_output = u''.join((t.enter_fullscreen, t.exit_fullscreen)) + assert (t.stream.getvalue() == expected_output) + + child(all_terms) + + +def test_yield_hidden_cursor(all_terms): + "Ensure ``hidden_cursor()`` writes hide_cursor and normal_cursor." + @as_subprocess + def child(kind): + t = TestTerminal(stream=StringIO(), force_styling=True) + t.hide_cursor = u'BEGIN' + t.normal_cursor = u'END' + with t.hidden_cursor(): + pass + expected_output = u''.join((t.hide_cursor, t.normal_cursor)) + assert (t.stream.getvalue() == expected_output) + + child(all_terms) diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index 8a502312..60d52078 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """Tests for keyboard support.""" +import StringIO import curses import time import math @@ -11,6 +12,7 @@ read_until_eof, read_until_semaphore, SEND_SEMAPHORE, + RECV_SEMAPHORE, as_subprocess, TestTerminal, SEMAPHORE, @@ -22,7 +24,18 @@ import mock -def test_inkey_0s_noinput(): +def test_kbhit_no_kb(): + """kbhit() always immediately returns False without a keyboard.""" + @as_subprocess + def child(): + term = TestTerminal(stream=StringIO.StringIO()) + stime = time.time() + assert term.kbhit(timeout=2.5) is False + assert (math.floor(time.time() - stime) == 0.0) + child() + + +def test_inkey_0s_cbreak_noinput(): """0-second inkey without input; '' should be returned.""" @as_subprocess def child(): @@ -35,7 +48,7 @@ def child(): child() -def test_inkey_1s_noinput(): +def test_inkey_1s_cbreak_noinput(): """1-second inkey without input; '' should be returned after ~1 second.""" @as_subprocess def child(): @@ -48,7 +61,7 @@ def child(): child() -def test_inkey_0s_input(): +def test_inkey_0s_cbreak_input(): """0-second inkey with input; Keypress should be immediately returned.""" pid, master_fd = pty.fork() if pid is 0: @@ -74,7 +87,7 @@ def test_inkey_0s_input(): assert (math.floor(time.time() - stime) == 0.0) -def test_inkey_0s_multibyte_utf8(): +def test_inkey_0s_cbreak_multibyte_utf8(): """0-second inkey with multibyte utf-8 input; should decode immediately.""" # utf-8 bytes represent "latin capital letter upsilon". pid, master_fd = pty.fork() @@ -99,7 +112,32 @@ def test_inkey_0s_multibyte_utf8(): assert (math.floor(time.time() - stime) == 0.0) -def test_inkey_0s_sequence(): +def test_inkey_0s_raw_ctrl_c(): + """0-second inkey with raw allows receiving ^C.""" + pid, master_fd = pty.fork() + if pid is 0: # child + term = TestTerminal() + read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) + with term.raw(): + os.write(sys.__stdout__.fileno(), RECV_SEMAPHORE) + inp = term.inkey(timeout=0) + os.write(sys.__stdout__.fileno(), inp.encode('latin1')) + os._exit(0) + + with echo_off(master_fd): + os.write(master_fd, SEND_SEMAPHORE) + # ensure child is in raw mode before sending ^C, + read_until_semaphore(master_fd) + os.write(master_fd, u'\x03'.encode('latin1')) + stime = time.time() + output = read_until_eof(master_fd) + pid, status = os.waitpid(pid, 0) + assert (output == u'\x03') + assert (os.WEXITSTATUS(status) == 0) + assert (math.floor(time.time() - stime) == 0.0) + + +def test_inkey_0s_cbreak_sequence(): """0-second inkey with multibyte sequence; should decode immediately.""" pid, master_fd = pty.fork() if pid is 0: # child @@ -122,7 +160,7 @@ def test_inkey_0s_sequence(): assert (math.floor(time.time() - stime) == 0.0) -def test_inkey_1s_input(): +def test_inkey_1s_cbreak_input(): """1-second inkey w/multibyte sequence; should return after ~1 second.""" pid, master_fd = pty.fork() if pid is 0: # child @@ -147,7 +185,7 @@ def test_inkey_1s_input(): assert (math.floor(time.time() - stime) == 1.0) -def test_esc_delay_035(): +def test_esc_delay_cbreak_035(): """esc_delay will cause a single ESC (\\x1b) to delay for 0.35.""" pid, master_fd = pty.fork() if pid is 0: # child @@ -175,7 +213,7 @@ def test_esc_delay_035(): assert 35 <= int(duration_ms) <= 45, duration_ms -def test_esc_delay_135(): +def test_esc_delay_cbreak_135(): """esc_delay=1.35 will cause a single ESC (\\x1b) to delay for 1.35.""" pid, master_fd = pty.fork() if pid is 0: # child @@ -203,7 +241,7 @@ def test_esc_delay_135(): assert 135 <= int(duration_ms) <= 145, int(duration_ms) -def test_esc_delay_timout_0(): +def test_esc_delay_cbreak_timout_0(): """esc_delay still in effect with timeout of 0 ("nonblocking").""" pid, master_fd = pty.fork() if pid is 0: # child diff --git a/blessed/tests/test_length_sequence.py b/blessed/tests/test_length_sequence.py index 13f2ed65..593d6685 100644 --- a/blessed/tests/test_length_sequence.py +++ b/blessed/tests/test_length_sequence.py @@ -41,47 +41,94 @@ def child(kind): assert (t.length(t.bold('x')) == 1) assert (t.length(t.bold_red) == 0) assert (t.length(t.bold_red('x')) == 1) + assert (t.strip(t.bold) == u'') + assert (t.strip(t.bold(' x ')) == u'x') + assert (t.strip(t.bold_red) == u'') + assert (t.strip(t.bold_red(' x ')) == u'x') + assert (t.strip_seqs(t.bold) == u'') + assert (t.strip_seqs(t.bold(' x ')) == u' x ') + assert (t.strip_seqs(t.bold_red) == u'') + assert (t.strip_seqs(t.bold_red(' x ')) == u' x ') + if t.underline: assert (t.length(t.underline) == 0) assert (t.length(t.underline('x')) == 1) assert (t.length(t.underline_red) == 0) assert (t.length(t.underline_red('x')) == 1) + assert (t.strip(t.underline) == u'') + assert (t.strip(t.underline(' x ')) == u'x') + assert (t.strip(t.underline_red) == u'') + assert (t.strip(t.underline_red(' x ')) == u'x') + assert (t.strip_seqs(t.underline) == u'') + assert (t.strip_seqs(t.underline(' x ')) == u' x ') + assert (t.strip_seqs(t.underline_red) == u'') + assert (t.strip_seqs(t.underline_red(' x ')) == u' x ') + if t.reverse: assert (t.length(t.reverse) == 0) assert (t.length(t.reverse('x')) == 1) assert (t.length(t.reverse_red) == 0) assert (t.length(t.reverse_red('x')) == 1) + assert (t.strip(t.reverse) == u'') + assert (t.strip(t.reverse(' x ')) == u'x') + assert (t.strip(t.reverse_red) == u'') + assert (t.strip(t.reverse_red(' x ')) == u'x') + assert (t.strip_seqs(t.reverse) == u'') + assert (t.strip_seqs(t.reverse(' x ')) == u' x ') + assert (t.strip_seqs(t.reverse_red) == u'') + assert (t.strip_seqs(t.reverse_red(' x ')) == u' x ') + if t.blink: assert (t.length(t.blink) == 0) assert (t.length(t.blink('x')) == 1) assert (t.length(t.blink_red) == 0) assert (t.length(t.blink_red('x')) == 1) + assert (t.strip(t.blink) == u'') + assert (t.strip(t.blink(' x ')) == u'x') + assert (t.strip(t.blink_red) == u'') + assert (t.strip(t.blink_red(' x ')) == u'x') + assert (t.strip_seqs(t.blink) == u'') + assert (t.strip_seqs(t.blink(' x ')) == u' x ') + assert (t.strip_seqs(t.blink_red) == u'') + assert (t.strip_seqs(t.blink_red(' x ')) == u' x ') + if t.home: assert (t.length(t.home) == 0) + assert (t.strip(t.home) == u'') if t.clear_eol: assert (t.length(t.clear_eol) == 0) + assert (t.strip(t.clear_eol) == u'') if t.enter_fullscreen: assert (t.length(t.enter_fullscreen) == 0) + assert (t.strip(t.enter_fullscreen) == u'') if t.exit_fullscreen: assert (t.length(t.exit_fullscreen) == 0) + assert (t.strip(t.exit_fullscreen) == u'') # horizontally, we decide move_down and move_up are 0, assert (t.length(t.move_down) == 0) assert (t.length(t.move_down(2)) == 0) assert (t.length(t.move_up) == 0) assert (t.length(t.move_up(2)) == 0) + # other things aren't so simple, somewhat edge cases, # moving backwards and forwards horizontally must be # accounted for as a "length", as # will result in a printed column length of 12 (even # though columns 2-11 are non-destructive space assert (t.length(u'x\b') == 0) + assert (t.strip(u'x\b') == u'') + # XXX why are some terminals width of 9 here ?? assert (t.length(u'\t') in (8, 9)) + assert (t.strip(u'\t') == u'') assert (t.length(u'_' + t.move_left) == 0) + if t.cub: assert (t.length((u'_' * 10) + t.cub(10)) == 0) + assert (t.length(t.move_right) == 1) + if t.cuf: assert (t.length(t.cuf(10)) == 10) @@ -90,6 +137,7 @@ def child(kind): assert (t.length(t.cuu(10)) == 0) assert (t.length(t.move_down) == 0) assert (t.length(t.cud(10)) == 0) + # this is how manpages perform underlining, this is done # with the 'overstrike' capability of teletypes, and aparently # less(1), '123' -> '1\b_2\b_3\b_' From de987f7106dc9e88f31b813c4c7ebe220cbf62f2 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 14:32:00 -0700 Subject: [PATCH 110/459] allow non-ttys to call kbhit/cbreak/raw along with related testcases and more keyboard coverage --- blessed/terminal.py | 2 -- blessed/tests/test_keyboard.py | 65 ++++++++++++++++++++++++++++++++-- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/blessed/terminal.py b/blessed/terminal.py index b80738f7..fdf40853 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -562,7 +562,6 @@ def cbreak(self): Note also that setcbreak sets VMIN = 1 and VTIME = 0, http://www.unixwiz.net/techtips/termios-vmin-vtime.html """ - assert self.is_a_tty, 'stream is not a tty.' if self.keyboard_fd is not None: # save current terminal mode, save_mode = termios.tcgetattr(self.keyboard_fd) @@ -585,7 +584,6 @@ def raw(self): interrupt, quit, suspend, and flow control characters are all passed through uninterpreted, instead of generating a signal. """ - assert self.is_a_tty, 'stream is not a tty.' if self.keyboard_fd is not None: # save current terminal mode, save_mode = termios.tcgetattr(self.keyboard_fd) diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index 60d52078..55bb6a67 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -1,9 +1,11 @@ # -*- coding: utf-8 -*- """Tests for keyboard support.""" +import tempfile import StringIO import curses import time import math +import tty import pty import sys import os @@ -24,13 +26,37 @@ import mock +def test_cbreak_no_kb(): + """cbreak() should not call tty.setcbreak() without keyboard""" + @as_subprocess + def child(): + with tempfile.NamedTemporaryFile() as stream: + term = TestTerminal(stream=stream) + with mock.patch("tty.setcbreak") as mock_setcbreak: + with term.cbreak(): + assert not mock_setcbreak.called + child() + + +def test_raw_no_kb(): + """raw() should not call tty.setraw() without keyboard""" + @as_subprocess + def child(): + with tempfile.NamedTemporaryFile() as stream: + term = TestTerminal(stream=stream) + with mock.patch("tty.setraw") as mock_setraw: + with term.raw(): + assert not mock_setraw.called + child() + + def test_kbhit_no_kb(): """kbhit() always immediately returns False without a keyboard.""" @as_subprocess def child(): term = TestTerminal(stream=StringIO.StringIO()) stime = time.time() - assert term.kbhit(timeout=2.5) is False + assert term.kbhit(timeout=1.5) is False assert (math.floor(time.time() - stime) == 0.0) child() @@ -87,6 +113,41 @@ def test_inkey_0s_cbreak_input(): assert (math.floor(time.time() - stime) == 0.0) +def test_inkey_cbreak_input_slowly(): + """0-second inkey with input; Keypress should be immediately returned.""" + pid, master_fd = pty.fork() + if pid is 0: + # child pauses, writes semaphore and begins awaiting input + term = TestTerminal() + read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.cbreak(): + while True: + inp = term.inkey(timeout=0.5) + os.write(sys.__stdout__.fileno(), inp.encode('utf-8')) + if inp == 'X': + break + os._exit(0) + + with echo_off(master_fd): + os.write(master_fd, SEND_SEMAPHORE) + os.write(master_fd, u'a'.encode('ascii')) + time.sleep(0.1) + os.write(master_fd, u'b'.encode('ascii')) + time.sleep(0.1) + os.write(master_fd, u'c'.encode('ascii')) + time.sleep(0.1) + os.write(master_fd, u'X'.encode('ascii')) + read_until_semaphore(master_fd) + stime = time.time() + output = read_until_eof(master_fd) + + pid, status = os.waitpid(pid, 0) + assert (output == u'abcX') + assert (os.WEXITSTATUS(status) == 0) + assert (math.floor(time.time() - stime) == 0.0) + + def test_inkey_0s_cbreak_multibyte_utf8(): """0-second inkey with multibyte utf-8 input; should decode immediately.""" # utf-8 bytes represent "latin capital letter upsilon". @@ -132,7 +193,7 @@ def test_inkey_0s_raw_ctrl_c(): stime = time.time() output = read_until_eof(master_fd) pid, status = os.waitpid(pid, 0) - assert (output == u'\x03') + assert (output == u'\x03'), repr(output) assert (os.WEXITSTATUS(status) == 0) assert (math.floor(time.time() - stime) == 0.0) From 4810c200cac8306f353ae1f5ace981c99a4b5799 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 14:38:37 -0700 Subject: [PATCH 111/459] travis workaround; will it throw exit code 1, now? --- blessed/tests/test_keyboard.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index 55bb6a67..02fad51f 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -193,7 +193,11 @@ def test_inkey_0s_raw_ctrl_c(): stime = time.time() output = read_until_eof(master_fd) pid, status = os.waitpid(pid, 0) - assert (output == u'\x03'), repr(output) + if os.environ.get('TRAVIS', None) is not None: + # XXX for some reason, setraw has no effect travis-ci + assert (output == u''), repr(output) + else: + assert (output == u'\x03'), repr(output) assert (os.WEXITSTATUS(status) == 0) assert (math.floor(time.time() - stime) == 0.0) From 310b2029c7b8fc8eefbf904847787a7541328ada Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 14:51:15 -0700 Subject: [PATCH 112/459] additional coverage for manual pty.fork() --- blessed/tests/test_keyboard.py | 67 +++++++++++++++++++++++++++++++++- tox.ini | 2 +- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index 02fad51f..b7f4fb9d 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -91,6 +91,10 @@ def test_inkey_0s_cbreak_input(): """0-second inkey with input; Keypress should be immediately returned.""" pid, master_fd = pty.fork() if pid is 0: + try: + cov = __import__('cov_core_init').init() + except ImportError: + cov = None # child pauses, writes semaphore and begins awaiting input term = TestTerminal() read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) @@ -98,6 +102,9 @@ def test_inkey_0s_cbreak_input(): with term.cbreak(): inp = term.inkey(timeout=0) os.write(sys.__stdout__.fileno(), inp.encode('utf-8')) + if cov is not None: + cov.stop() + cov.save() os._exit(0) with echo_off(master_fd): @@ -117,6 +124,10 @@ def test_inkey_cbreak_input_slowly(): """0-second inkey with input; Keypress should be immediately returned.""" pid, master_fd = pty.fork() if pid is 0: + try: + cov = __import__('cov_core_init').init() + except ImportError: + cov = None # child pauses, writes semaphore and begins awaiting input term = TestTerminal() read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) @@ -127,6 +138,9 @@ def test_inkey_cbreak_input_slowly(): os.write(sys.__stdout__.fileno(), inp.encode('utf-8')) if inp == 'X': break + if cov is not None: + cov.stop() + cov.save() os._exit(0) with echo_off(master_fd): @@ -135,7 +149,7 @@ def test_inkey_cbreak_input_slowly(): time.sleep(0.1) os.write(master_fd, u'b'.encode('ascii')) time.sleep(0.1) - os.write(master_fd, u'c'.encode('ascii')) + os.write(master_fd, u'cdefgh'.encode('ascii')) time.sleep(0.1) os.write(master_fd, u'X'.encode('ascii')) read_until_semaphore(master_fd) @@ -143,7 +157,7 @@ def test_inkey_cbreak_input_slowly(): output = read_until_eof(master_fd) pid, status = os.waitpid(pid, 0) - assert (output == u'abcX') + assert (output == u'abcdefghX') assert (os.WEXITSTATUS(status) == 0) assert (math.floor(time.time() - stime) == 0.0) @@ -153,12 +167,19 @@ def test_inkey_0s_cbreak_multibyte_utf8(): # utf-8 bytes represent "latin capital letter upsilon". pid, master_fd = pty.fork() if pid is 0: # child + try: + cov = __import__('cov_core_init').init() + except ImportError: + cov = None term = TestTerminal() read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) os.write(sys.__stdout__.fileno(), SEMAPHORE) with term.cbreak(): inp = term.inkey(timeout=0) os.write(sys.__stdout__.fileno(), inp.encode('utf-8')) + if cov is not None: + cov.stop() + cov.save() os._exit(0) with echo_off(master_fd): @@ -177,12 +198,19 @@ def test_inkey_0s_raw_ctrl_c(): """0-second inkey with raw allows receiving ^C.""" pid, master_fd = pty.fork() if pid is 0: # child + try: + cov = __import__('cov_core_init').init() + except ImportError: + cov = None term = TestTerminal() read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) with term.raw(): os.write(sys.__stdout__.fileno(), RECV_SEMAPHORE) inp = term.inkey(timeout=0) os.write(sys.__stdout__.fileno(), inp.encode('latin1')) + if cov is not None: + cov.stop() + cov.save() os._exit(0) with echo_off(master_fd): @@ -206,12 +234,19 @@ def test_inkey_0s_cbreak_sequence(): """0-second inkey with multibyte sequence; should decode immediately.""" pid, master_fd = pty.fork() if pid is 0: # child + try: + cov = __import__('cov_core_init').init() + except ImportError: + cov = None term = TestTerminal() os.write(sys.__stdout__.fileno(), SEMAPHORE) with term.cbreak(): inp = term.inkey(timeout=0) os.write(sys.__stdout__.fileno(), inp.name.encode('ascii')) sys.stdout.flush() + if cov is not None: + cov.stop() + cov.save() os._exit(0) with echo_off(master_fd): @@ -229,12 +264,19 @@ def test_inkey_1s_cbreak_input(): """1-second inkey w/multibyte sequence; should return after ~1 second.""" pid, master_fd = pty.fork() if pid is 0: # child + try: + cov = __import__('cov_core_init').init() + except ImportError: + cov = None term = TestTerminal() os.write(sys.__stdout__.fileno(), SEMAPHORE) with term.cbreak(): inp = term.inkey(timeout=3) os.write(sys.__stdout__.fileno(), inp.name.encode('utf-8')) sys.stdout.flush() + if cov is not None: + cov.stop() + cov.save() os._exit(0) with echo_off(master_fd): @@ -254,6 +296,10 @@ def test_esc_delay_cbreak_035(): """esc_delay will cause a single ESC (\\x1b) to delay for 0.35.""" pid, master_fd = pty.fork() if pid is 0: # child + try: + cov = __import__('cov_core_init').init() + except ImportError: + cov = None term = TestTerminal() os.write(sys.__stdout__.fileno(), SEMAPHORE) with term.cbreak(): @@ -263,6 +309,9 @@ def test_esc_delay_cbreak_035(): os.write(sys.__stdout__.fileno(), ( '%s %i' % (inp.name, measured_time,)).encode('ascii')) sys.stdout.flush() + if cov is not None: + cov.stop() + cov.save() os._exit(0) with echo_off(master_fd): @@ -282,6 +331,10 @@ def test_esc_delay_cbreak_135(): """esc_delay=1.35 will cause a single ESC (\\x1b) to delay for 1.35.""" pid, master_fd = pty.fork() if pid is 0: # child + try: + cov = __import__('cov_core_init').init() + except ImportError: + cov = None term = TestTerminal() os.write(sys.__stdout__.fileno(), SEMAPHORE) with term.cbreak(): @@ -291,6 +344,9 @@ def test_esc_delay_cbreak_135(): os.write(sys.__stdout__.fileno(), ( '%s %i' % (inp.name, measured_time,)).encode('ascii')) sys.stdout.flush() + if cov is not None: + cov.stop() + cov.save() os._exit(0) with echo_off(master_fd): @@ -310,6 +366,10 @@ def test_esc_delay_cbreak_timout_0(): """esc_delay still in effect with timeout of 0 ("nonblocking").""" pid, master_fd = pty.fork() if pid is 0: # child + try: + cov = __import__('cov_core_init').init() + except ImportError: + cov = None term = TestTerminal() os.write(sys.__stdout__.fileno(), SEMAPHORE) with term.cbreak(): @@ -319,6 +379,9 @@ def test_esc_delay_cbreak_timout_0(): os.write(sys.__stdout__.fileno(), ( '%s %i' % (inp.name, measured_time,)).encode('ascii')) sys.stdout.flush() + if cov is not None: + cov.stop() + cov.save() os._exit(0) with echo_off(master_fd): diff --git a/tox.ini b/tox.ini index f34d8652..848294ca 100644 --- a/tox.ini +++ b/tox.ini @@ -30,7 +30,7 @@ deps = pytest commands = {envbindir}/py.test -v \ -x --strict --pep8 --flakes \ --cov blessed {posargs} - coverage report + coverage report -m [testenv:py33] From 9cdf91dc16b89ccc5920ed27942b55372c90a97e Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 14:55:27 -0700 Subject: [PATCH 113/459] dealing with travis-ci setraw and remove noop () --- blessed/tests/test_keyboard.py | 61 ++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index b7f4fb9d..60e7eb3b 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -115,9 +115,9 @@ def test_inkey_0s_cbreak_input(): output = read_until_eof(master_fd) pid, status = os.waitpid(pid, 0) - assert (output == u'x') - assert (os.WEXITSTATUS(status) == 0) - assert (math.floor(time.time() - stime) == 0.0) + assert output == u'x' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 0.0 def test_inkey_cbreak_input_slowly(): @@ -157,9 +157,9 @@ def test_inkey_cbreak_input_slowly(): output = read_until_eof(master_fd) pid, status = os.waitpid(pid, 0) - assert (output == u'abcdefghX') - assert (os.WEXITSTATUS(status) == 0) - assert (math.floor(time.time() - stime) == 0.0) + assert output == u'abcdefghX' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 0.0 def test_inkey_0s_cbreak_multibyte_utf8(): @@ -189,9 +189,9 @@ def test_inkey_0s_cbreak_multibyte_utf8(): stime = time.time() output = read_until_eof(master_fd) pid, status = os.waitpid(pid, 0) - assert (output == u'Ʊ') - assert (os.WEXITSTATUS(status) == 0) - assert (math.floor(time.time() - stime) == 0.0) + assert output == u'Ʊ' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 0.0 def test_inkey_0s_raw_ctrl_c(): @@ -222,12 +222,15 @@ def test_inkey_0s_raw_ctrl_c(): output = read_until_eof(master_fd) pid, status = os.waitpid(pid, 0) if os.environ.get('TRAVIS', None) is not None: - # XXX for some reason, setraw has no effect travis-ci - assert (output == u''), repr(output) + # For some reason, setraw has no effect travis-ci, + # is still accepts ^C, when causes system exit on + # py27, but exit 0 on py27 and p33 -- strangely, huh? + assert output == u'', repr(output) + assert os.WEXITSTATUS(status) in (0, 2) else: - assert (output == u'\x03'), repr(output) - assert (os.WEXITSTATUS(status) == 0) - assert (math.floor(time.time() - stime) == 0.0) + assert output == u'\x03', repr(output) + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 0.0 def test_inkey_0s_cbreak_sequence(): @@ -255,9 +258,9 @@ def test_inkey_0s_cbreak_sequence(): stime = time.time() output = read_until_eof(master_fd) pid, status = os.waitpid(pid, 0) - assert (output == u'KEY_LEFT') - assert (os.WEXITSTATUS(status) == 0) - assert (math.floor(time.time() - stime) == 0.0) + assert output == u'KEY_LEFT' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 0.0 def test_inkey_1s_cbreak_input(): @@ -287,9 +290,9 @@ def test_inkey_1s_cbreak_input(): output = read_until_eof(master_fd) pid, status = os.waitpid(pid, 0) - assert (output == u'KEY_RIGHT') - assert (os.WEXITSTATUS(status) == 0) - assert (math.floor(time.time() - stime) == 1.0) + assert output == u'KEY_RIGHT' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 1.0 def test_esc_delay_cbreak_035(): @@ -321,9 +324,9 @@ def test_esc_delay_cbreak_035(): key_name, duration_ms = read_until_eof(master_fd).split() pid, status = os.waitpid(pid, 0) - assert (key_name == u'KEY_ESCAPE') - assert (os.WEXITSTATUS(status) == 0) - assert (math.floor(time.time() - stime) == 0.0) + assert key_name == u'KEY_ESCAPE' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 0.0 assert 35 <= int(duration_ms) <= 45, duration_ms @@ -356,9 +359,9 @@ def test_esc_delay_cbreak_135(): key_name, duration_ms = read_until_eof(master_fd).split() pid, status = os.waitpid(pid, 0) - assert (key_name == u'KEY_ESCAPE') - assert (os.WEXITSTATUS(status) == 0) - assert (math.floor(time.time() - stime) == 1.0) + assert key_name == u'KEY_ESCAPE' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 1.0 assert 135 <= int(duration_ms) <= 145, int(duration_ms) @@ -391,9 +394,9 @@ def test_esc_delay_cbreak_timout_0(): key_name, duration_ms = read_until_eof(master_fd).split() pid, status = os.waitpid(pid, 0) - assert (key_name == u'KEY_ESCAPE') - assert (os.WEXITSTATUS(status) == 0) - assert (math.floor(time.time() - stime) == 0.0) + assert key_name == u'KEY_ESCAPE' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 0.0 assert 35 <= int(duration_ms) <= 45, int(duration_ms) From 0b4eb54013381d81ac36a155b9828a035052bbd8 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 16:32:29 -0700 Subject: [PATCH 114/459] correct timeout measurement of interrupted select --- bin/on_resize.py | 2 +- blessed/terminal.py | 35 ++++++++++------------ blessed/tests/test_keyboard.py | 55 ++++++++++++++++++++++++++++++++-- 3 files changed, 69 insertions(+), 23 deletions(-) diff --git a/bin/on_resize.py b/bin/on_resize.py index d7260c12..bce18beb 100755 --- a/bin/on_resize.py +++ b/bin/on_resize.py @@ -10,4 +10,4 @@ def on_resize(sig, action): signal.signal(signal.SIGWINCH, on_resize) -term.inkey() +term.inkey(1) diff --git a/blessed/terminal.py b/blessed/terminal.py index fdf40853..936ba023 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -510,37 +510,32 @@ def kbhit(self, timeout=0): If input is not a terminal, False is always returned. """ - if self.keyboard_fd is None: - return False - # Special care is taken to handle a custom SIGWINCH handler, which - # causes select() to be interrupted with errno 4 -- it is ignored, - # and a new timeout value is derived from the previous, unless timeout - # becomes negative, because signal handler has blocked beyond timeout, - # then False is returned. Otherwise, when timeout is 0, we continue to - # block indefinitely (default). + # causes select() to be interrupted with errno 4 (EAGAIN) -- + # it is ignored, and a new timeout value is derived from the previous, + # unless timeout becomes negative, because signal handler has blocked + # beyond timeout, then False is returned. Otherwise, when timeout is 0, + # we continue to block indefinitely (default). stime = time.time() - check_r, check_w, check_x = [self.keyboard_fd], [], [] + check_w, check_x = [], [] + check_r = [self.keyboard_fd] if self.keyboard_fd is not None else [] while True: try: ready_r, ready_w, ready_x = select.select( check_r, check_w, check_x, timeout) except InterruptedError as exc: - if 4 == (hasattr(exc, 'errno') and exc.errno or # py2 - hasattr(exc, 'args') and exc.args[0]): # py3 - if timeout != 0: - timeout = time.time() - stime - if timeout > 0: - continue - else: - ready_r = False - break - raise + if timeout != 0: + # subtract time already elapsed, + timeout -= time.time() - stime + if timeout > 0: + continue + ready_r = False + break else: break - return check_r == ready_r + return False if self.keyboard_fd is None else check_r == ready_r @contextlib.contextmanager def cbreak(self): diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index 60e7eb3b..e2c3ff90 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -2,6 +2,7 @@ """Tests for keyboard support.""" import tempfile import StringIO +import signal import curses import time import math @@ -26,6 +27,55 @@ import mock +def test_kbhit_interrupted(): + """kbhit() should not be interrupted with a signal handler.""" + pid, master_fd = pty.fork() + if pid is 0: + try: + cov = __import__('cov_core_init').init() + except ImportError: + cov = None + + # child pauses, writes semaphore and begins awaiting input + global got_sigwinch + got_sigwinch = False + + def on_resize(sig, action): + global got_sigwinch + got_sigwinch = True + + term = TestTerminal() + signal.signal(signal.SIGWINCH, on_resize) + read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.raw(): + term.inkey(timeout=1.05) + os.write(sys.__stdout__.fileno(), b'complete') + assert got_sigwinch is True + if cov is not None: + cov.stop() + cov.save() + os._exit(0) + + with echo_off(master_fd): + os.write(master_fd, SEND_SEMAPHORE) + read_until_semaphore(master_fd) + stime = time.time() + os.kill(pid, signal.SIGWINCH) + time.sleep(0.5) + os.kill(pid, signal.SIGWINCH) + time.sleep(0.5) + os.kill(pid, signal.SIGWINCH) + time.sleep(0.5) + os.kill(pid, signal.SIGWINCH) + output = read_until_eof(master_fd) + + pid, status = os.waitpid(pid, 0) + assert output == u'complete' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 1.0 + + def test_cbreak_no_kb(): """cbreak() should not call tty.setcbreak() without keyboard""" @as_subprocess @@ -56,8 +106,9 @@ def test_kbhit_no_kb(): def child(): term = TestTerminal(stream=StringIO.StringIO()) stime = time.time() - assert term.kbhit(timeout=1.5) is False - assert (math.floor(time.time() - stime) == 0.0) + assert term.keyboard_fd is None + assert term.kbhit(timeout=1.1) is False + assert (math.floor(time.time() - stime) == 1.0) child() From f9d182f57d7d972e84db8e6837385035a5cf5d49 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 16:35:48 -0700 Subject: [PATCH 115/459] unused variable exc --- blessed/terminal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blessed/terminal.py b/blessed/terminal.py index 936ba023..f7404cfd 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -524,7 +524,7 @@ def kbhit(self, timeout=0): try: ready_r, ready_w, ready_x = select.select( check_r, check_w, check_x, timeout) - except InterruptedError as exc: + except InterruptedError: if timeout != 0: # subtract time already elapsed, timeout -= time.time() - stime From bb01f44065c610f806623b53ccebf21c192d9c6e Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 16:39:46 -0700 Subject: [PATCH 116/459] vanity, add self to LICENSE --- LICENSE | 1 + 1 file changed, 1 insertion(+) diff --git a/LICENSE b/LICENSE index 3d3a44e6..4b071328 100644 --- a/LICENSE +++ b/LICENSE @@ -1,3 +1,4 @@ +Copyright (c) 2014 Jeff Quast Copyright (c) 2011 Erik Rose Permission is hereby granted, free of charge, to any person obtaining a copy of From 0f4850c977abcba0e4aca5ccc1297019ae1a59cb Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 17:03:00 -0700 Subject: [PATCH 117/459] documentation updates --- README.rst | 40 +++++++++++++++++++++++----------------- docs/conf.py | 16 ++++++++++------ setup.cfg | 3 --- 3 files changed, 33 insertions(+), 26 deletions(-) delete mode 100644 setup.cfg diff --git a/README.rst b/README.rst index 68c4c980..794facbf 100644 --- a/README.rst +++ b/README.rst @@ -30,7 +30,7 @@ The Pitch * No more C-like calls to tigetstr_ and `tparm`_. * Act intelligently when somebody redirects your output to a file, omitting all of the terminal sequences such as styling, colors, or positioning. - +* Dead-simple keyboard handling, modeled after the Basic language's *INKEY$* Before And After ---------------- @@ -89,8 +89,8 @@ decoded, as well as your locale-specific encoded multibyte input. Simple Formatting ----------------- -Lots of handy formatting codes `terminfo(5)`_ are available as attributes -on a *Terminal* class instance. For example:: +Lots of handy formatting codes are available as attributes on a *Terminal* class +instance. For example:: from blessed import Terminal @@ -133,8 +133,8 @@ Note that, while the inverse of *underline* is *no_underline*, the only way to turn off *bold* or *reverse* is *normal*, which also cancels any custom colors. -Many of these are aliases, their true capability names (such as *smul* for -*begin underline mode*) may still be used. Any capability in the `terminfo(5)`_ +Many of these are aliases, their true capability names (such as 'smul' for +'begin underline mode') may still be used. Any capability in the `terminfo(5)`_ manual, under column **Cap-name**, may be used as an attribute to a *Terminal* instance. If it is not a supported capability, or a non-tty is used as an output stream, an empty string is returned. @@ -446,7 +446,7 @@ less arrow or function keys that emit multibyte sequences. Special `termios(4)` routines are required to enter Non-canonical, known in curses as `cbreak(3)_`. These functions also receive bytes, which must be incrementally decoded to unicode. -Blessed handles all of these special keyboarding purposes! +Blessed handles all of these special cases with the following simple calls. cbreak ~~~~~~ @@ -467,8 +467,8 @@ raw ~~~ The context manager ``raw`` is the same as ``cbreak``, except interrupt (^C), -quit (^\\), suspend (^Z), and flow control (^S, ^Q) characters are not trapped -by signal handlers, but instead sent directly. This is necessary if you +quit (^\\), suspend (^Z), and flow control (^S, ^Q) characters are not trapped, +but instead sent directly as their natural character. This is necessary if you actually want to handle the receipt of Ctrl+C inkey @@ -512,6 +512,10 @@ Its output might appear as:: got q. bye! +A *timeout* value of None (default) will block forever. Any other value specifies +the length of time to poll for input, if no input is received after such time +has elapsed, an empty string is returned. A timeout value of 0 is nonblocking. + keyboard codes ~~~~~~~~~~~~~~ @@ -603,16 +607,18 @@ Version History 1.7 * Forked github project `erikrose/blessings`_ to `jquast/blessed`_, this project was previously known as **blessings** version 1.6 and prior. - * introduced: context manager ``cbreak`` and ``raw``, which is equivalent - to ``tty.setcbreak`` and ``tty.setraw``, allowing input from stdin to be + * introduced: context manager ``cbreak()`` and ``raw()``, which is equivalent + to ``tty.setcbreak()`` and ``tty.setraw()``, allowing input from stdin to be read as each key is pressed. - * introduced: ``inkey()``, which will return 1 or more characters as - a unicode sequence, with attributes ``.code`` and ``.name`` non-None when - a multibyte sequence is received, allowing arrow keys and such to be - detected. Optional value ``timeout`` allows timed polling or blocking. - * introduced: ``center()``, ``rjust()``, and ``ljust()`` methods, allows text - containing sequences to be aligned to screen, or ``width`` specified. - * introduced: ``wrap()``, allows text containing sequences to be + * introduced: ``inkey()`` and ``kbhit()``, which will return 1 or more + characters as a unicode sequence, with attributes ``.code`` and ``.name`` + non-None when a multibyte sequence is received, allowing arrow keys and + such to be detected. Optional value ``timeout`` allows timed polling or + blocking. + * introduced: ``center()``, ``rjust()``, ``ljust()``, ``strip()``, and + ``strip_seqs()`` methods. Allows text containing sequences to be aligned + to screen, or ``width`` specified. + * introduced: ``wrap()`` method. allows text containing sequences to be word-wrapped without breaking mid-sequence and honoring their printable width. diff --git a/docs/conf.py b/docs/conf.py index 34a409f9..96b183dc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,13 +12,16 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import blessed - -#import sys, os +import sys +import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +here = os.path.dirname(__file__) +sys.path.insert(0, os.path.abspath(os.path.join(here, '..'))) + +import blessed + # -- General configuration ---------------------------------------------------- @@ -43,14 +46,15 @@ # General information about the project. project = u'Blessed' -copyright = u'2011 Erik Rose, 2014 Jeff Quast' +copyright = u'2014 Jeff Quast, 2011 Erik Rose' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '1.6' +version = '1.7' + # The full version, including alpha/beta/rc tags. release = version diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 2507f1b6..00000000 --- a/setup.cfg +++ /dev/null @@ -1,3 +0,0 @@ -[pytest] -flakes-ignore = - tests/*.py UnusedImport From b3918ae37d5459ae5e0683093ddd2de209204b97 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 17:06:01 -0700 Subject: [PATCH 118/459] api docs fix (blessings->blessed) --- docs/index.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 63ee5be9..6094c689 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,5 +16,4 @@ in the right place. Then Read This If You Want ========================== -.. autoclass:: blessings.Terminal - :members: __init__, __getattr__, location, height, width, color, on_color, number_of_colors, fullscreen, hidden_cursor +.. autoclass:: blessed.Terminal From 0137f3ed8a698107eb379b3187498cf6b03abd0b Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 17:10:34 -0700 Subject: [PATCH 119/459] more blessings -> blessed --- blessed/__init__.py | 2 +- blessed/formatters.py | 10 +++++----- blessed/keyboard.py | 4 ++-- blessed/sequences.py | 3 +-- blessed/terminal.py | 4 ++-- docs/Makefile | 8 ++++---- docs/make.bat | 4 ++-- 7 files changed, 17 insertions(+), 18 deletions(-) diff --git a/blessed/__init__.py b/blessed/__init__.py index adb4be82..e6737e37 100644 --- a/blessed/__init__.py +++ b/blessed/__init__.py @@ -1,4 +1,4 @@ -"""A thin, practical wrapper around curses terminal capabilities.""" +"A thin, practical wrapper around curses terminal capabilities." # import as _platform to avoid tab-completion with IPython (thanks @kanzure) import platform as _platform diff --git a/blessed/formatters.py b/blessed/formatters.py index ef69c851..e9f4e25c 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -1,3 +1,4 @@ +"This sub-module provides formatting functions." import curses _derivitives = ('on', 'bright', 'on_bright',) @@ -14,16 +15,15 @@ class ParameterizingString(unicode): - """A Unicode string which can be called to parametrize it as a terminal - capability""" + "A Unicode string which can be called as a parameterizing termcap." def __new__(cls, name, attr, normal): - """Instantiate. - + """ + :arg name: name of terminal capability. + :arg attr: terminal capability name to receive arguments. :arg normal: If non-None, indicates that, once parametrized, this can be used as a ``FormattingString``. The value is used as the "normal" capability. - """ new = unicode.__new__(cls, attr) new._name = name diff --git a/blessed/keyboard.py b/blessed/keyboard.py index d6cfb43c..8d62ae18 100644 --- a/blessed/keyboard.py +++ b/blessed/keyboard.py @@ -1,4 +1,4 @@ -"""This sub-module provides 'keyboard awareness' for blessings.""" +"This sub-module provides 'keyboard awareness'." __author__ = 'Jeff Quast ' __license__ = 'MIT' @@ -112,7 +112,7 @@ def get_keyboard_sequences(term): """init_keyboard_sequences(T) -> (OrderedDict) Initialize and return a keyboard map and sequence lookup table, - (sequence, constant) from blessings Terminal instance ``term``, + (sequence, constant) from blessed Terminal instance ``term``, where ``sequence`` is a multibyte input sequence, such as u'\x1b[D', and ``constant`` is a constant, such as term.KEY_LEFT. The return value is an OrderedDict instance, with their keys sorted longest-first. diff --git a/blessed/sequences.py b/blessed/sequences.py index f0b6ec48..161aa214 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -1,5 +1,4 @@ -""" This sub-module provides 'sequence awareness' for blessings. -""" +" This sub-module provides 'sequence awareness' for blessed." __author__ = 'Jeff Quast ' __license__ = 'MIT' diff --git a/blessed/terminal.py b/blessed/terminal.py index f7404cfd..dd824567 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -1,3 +1,4 @@ +"This primary module provides the Terminal class." # standard modules import collections import contextlib @@ -125,8 +126,7 @@ def __init__(self, kind=None, stream=None, force_styling=False): 'A terminal of kind "%s" has been requested; due to an' ' internal python curses bug, terminal capabilities' ' for a terminal of kind "%s" will continue to be' - ' returned for the remainder of this process. see:' - ' https://github.com/erikrose/blessings/issues/33' % ( + ' returned for the remainder of this process.' % ( self._kind, _CUR_TERM,)) if self.does_styling: diff --git a/docs/Makefile b/docs/Makefile index c1d668a3..47febae6 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -72,17 +72,17 @@ qthelp: @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/blessings.qhcp" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/blessed.qhcp" @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/blessings.qhc" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/blessed.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/blessings" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/blessings" + @echo "# mkdir -p $$HOME/.local/share/devhelp/blessed" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/blessed" @echo "# devhelp" epub: diff --git a/docs/make.bat b/docs/make.bat index c24653f0..21ff37b3 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -95,9 +95,9 @@ if "%1" == "qthelp" ( echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\blessings.qhcp + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\blessed.qhcp echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\blessings.ghc + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\blessed.ghc goto end ) From 4f22ab7a5b159f4c8e8931b5c38f578ce4143bc8 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 17:14:23 -0700 Subject: [PATCH 120/459] update api docs, more blessings->blessed --- docs/index.rst | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 6094c689..3900935f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,19 +1,35 @@ -======================= -Blessings API Reference -======================= +===================== +Blessed API Reference +===================== Read The Readme First ===================== -This is the API documentation for the Blessings terminal library. Because -Blessings uses quite a bit of dynamism, you should `read the readme first`_, as -not all the useful stuff shows up in these autogenerated docs. However, if -you're looking for seldom-used keyword args or a peek at the internals, you're -in the right place. +This is the API documentation for the Blessed terminal library. Because +Blessed uses quite a bit of dynamism, you should `read the readme first`_, as +not all the useful stuff shows up in these autogenerated docs. -.. _`read the readme first`: http://pypi.python.org/pypi/blessings/ +However, if you're looking for seldom-used keyword args or a peek at the +internals, you're in the right place. + +.. _`read the readme first`: http://pypi.python.org/pypi/blessed Then Read This If You Want ========================== .. autoclass:: blessed.Terminal + :members: + +Internal modules +================ +.. automodule:: blessed.formatters + :members: +.. automodule:: blessed.keyboard + :members: +.. automodule:: blessed.sequences + :members: +blessed/__init__.py +blessed/formatters.py +blessed/keyboard.py +blessed/sequences.py +blessed/terminal.py From 017b675840bd0cec57c6c2ff5b20afcb678bce11 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 17:17:07 -0700 Subject: [PATCH 121/459] ignore UnusedImport, required for pytest compat --- tox.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tox.ini b/tox.ini index 848294ca..2d1c40cd 100644 --- a/tox.ini +++ b/tox.ini @@ -1,3 +1,7 @@ +[pytest] +flakes-ignore = + UnusedImport + [tox] envlist = py26, py27, From 74d026a9e47fc40bf585d5add4d030ec39e25c6d Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 17:19:45 -0700 Subject: [PATCH 122/459] api docsfix --- docs/index.rst | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 3900935f..c00f395e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -17,19 +17,13 @@ internals, you're in the right place. Then Read This If You Want ========================== -.. autoclass:: blessed.Terminal - :members: +Internal modules are as follows. -Internal modules -================ +.. automodule:: blessed.terminal + :members: .. automodule:: blessed.formatters :members: .. automodule:: blessed.keyboard :members: .. automodule:: blessed.sequences :members: -blessed/__init__.py -blessed/formatters.py -blessed/keyboard.py -blessed/sequences.py -blessed/terminal.py From ad698a85b8614491e857c814948feb713864a93d Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 17:22:23 -0700 Subject: [PATCH 123/459] level 3 header for each module --- docs/index.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index c00f395e..ec23b8a8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -19,11 +19,26 @@ Then Read This If You Want Internal modules are as follows. +terminal +-------- + .. automodule:: blessed.terminal :members: + +formatters +---------- + .. automodule:: blessed.formatters :members: + +keyboard +-------- + .. automodule:: blessed.keyboard :members: + +sequences +--------- + .. automodule:: blessed.sequences :members: From ab47f6bc6438b04a0e5d17dfb7792b392af5282c Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 17:23:49 -0700 Subject: [PATCH 124/459] docfix, inlined code shouldn't prefix >> --- blessed/formatters.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blessed/formatters.py b/blessed/formatters.py index e9f4e25c..620450ca 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -54,8 +54,8 @@ def __call__(self, *args): class FormattingString(unicode): """A Unicode string which can be called using ``text``, returning a new string, ``attr`` + ``text`` + ``normal``:: - >> style = FormattingString(term.bright_blue, term.normal) - >> style('Big Blue') + style = FormattingString(term.bright_blue, term.normal) + style('Big Blue') '\x1b[94mBig Blue\x1b(B\x1b[m' """ def __new__(cls, attr, normal): From adc8afc767b0bbe3f39190aee225d5156847342d Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 17:56:51 -0700 Subject: [PATCH 125/459] documentation updates, just drafting and polishing --- blessed/terminal.py | 144 ++++++++++++++++++-------------------------- docs/conf.py | 2 + docs/index.rst | 20 +++--- 3 files changed, 72 insertions(+), 94 deletions(-) diff --git a/blessed/terminal.py b/blessed/terminal.py index dd824567..182c02b7 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -39,11 +39,6 @@ class IOUnsupportedOperation(Exception): class Terminal(object): """An abstraction around terminal capabilities - Unlike curses, this doesn't require clearing the screen before doing - anything, and it's friendlier to use. It keeps the endless calls to - ``tigetstr()`` and ``tparm()`` out of your code, and it acts intelligently - when somebody pipes your output to a non-terminal. - Instance attributes: ``stream`` @@ -211,18 +206,13 @@ def __getattr__(self, attr): @property def does_styling(self): - """Whether attempt to emit capabilities - - This is influenced by the ``is_a_tty`` property and by the - ``force_styling`` argument to the constructor. You can examine - this value to decide whether to draw progress bars or other frippery. - - """ + """Whether this instance will emit terminal sequences (bool).""" return self._does_styling @property def is_a_tty(self): - """Whether my ``stream`` appears to be associated with a terminal""" + """Whether the ``stream`` associated with this instance is a terminal + (bool).""" return self._is_a_tty @property @@ -230,16 +220,6 @@ def height(self): """T.height -> int The height of the terminal in characters. - - If an alternative ``stream`` is chosen, the size of that stream - is returned if it is a connected to a terminal such as a pty. - Otherwise, the size of the controlling terminal is returned. - - If neither of these streams are terminals, such as when stdout is piped - to less(1), the values of the environment variable LINES and COLS are - returned. - - None may be returned if no suitable size is discovered. """ return self._height_and_width().ws_row @@ -248,8 +228,6 @@ def width(self): """T.width -> int The width of the terminal in characters. - - None may be returned if no suitable size is discovered. """ return self._height_and_width().ws_col @@ -320,7 +298,14 @@ def location(self, x=None, y=None): @contextlib.contextmanager def fullscreen(self): """Return a context manager that enters fullscreen mode while inside it - and restores normal mode on leaving.""" + and restores normal mode on leaving. Fullscreen mode is characterized + by instructing the terminal emulator to store and save the current + screen state (all screen output), switch to "alternate screen". Upon + exiting, the previous screen state is returned. + + This call may not be tested; only one screen state may be saved at a + time. + """ self.stream.write(self.enter_fullscreen) try: yield @@ -329,8 +314,8 @@ def fullscreen(self): @contextlib.contextmanager def hidden_cursor(self): - """Return a context manager that hides the cursor while inside it and - makes it visible on leaving.""" + """Return a context manager that hides the cursor upon entering, + and makes it visible again upon exiting.""" self.stream.write(self.hide_cursor) try: yield @@ -339,7 +324,7 @@ def hidden_cursor(self): @property def color(self): - """Return a capability that sets the foreground color. + """Returns capability that sets the foreground color. The capability is unparameterized until called and passed a number (0-15), at which point it returns another string which represents a @@ -357,11 +342,7 @@ def color(self): @property def on_color(self): - """Return a capability that sets the background color. - - See ``color()``. - - """ + "Returns capability that sets the background color." if not self.does_styling: return formatters.NullCallableString() return formatters.ParameterizingString(name='on_color', @@ -370,8 +351,7 @@ def on_color(self): @property def normal(self): - """Return capability that resets video attribute. - """ + "Returns sequence that resets video attribute." if self._normal: return self._normal self._normal = formatters.resolve_capability(self, 'normal') @@ -381,23 +361,11 @@ def normal(self): def number_of_colors(self): """Return the number of colors the terminal supports. - Common values are 0, 8, 16, 88, and 256. - - Though the underlying capability returns -1 when there is no color - support, we return 0. This lets you test more Pythonically:: + Common values are 0, 8, 16, 88, and 256. Most commonly + this may be used to test color capabilities at all:: if term.number_of_colors: - ... - - We also return 0 if the terminal won't tell us how many colors it - supports, which I think is rare. - - """ - # This is actually the only remotely useful numeric capability. We - # don't name it after the underlying capability, because we deviate - # slightly from its behavior, and we might someday wish to give direct - # access to it. - # + ...""" # trim value to 0, as tigetnum('colors') returns -1 if no support, # -2 if no such capability. return max(0, self.does_styling and curses.tigetnum('colors') or -1) @@ -411,34 +379,34 @@ def _background_color(self): return self.setab or self.setb def ljust(self, text, width=None, fillchar=u' '): - """T.ljust(text, [width], [fillchar]) -> string + """T.ljust(text, [width], [fillchar]) -> unicode Return string ``text``, left-justified by printable length ``width``. Padding is done using the specified fill character (default is a - space). Default width is the attached terminal's width. ``text`` is - escape-sequence safe.""" + space). Default ``width`` is the attached terminal's width. ``text`` + may contain terminal sequences.""" if width is None: width = self.width return sequences.Sequence(text, self).ljust(width, fillchar) def rjust(self, text, width=None, fillchar=u' '): - """T.rjust(text, [width], [fillchar]) -> string + """T.rjust(text, [width], [fillchar]) -> unicode Return string ``text``, right-justified by printable length ``width``. - Padding is done using the specified fill character (default is a space) - Default width is the attached terminal's width. ``text`` is - escape-sequence safe.""" + Padding is done using the specified fill character (default is a + space). Default ``width`` is the attached terminal's width. ``text`` + may contain terminal sequences.""" if width is None: width = self.width return sequences.Sequence(text, self).rjust(width, fillchar) def center(self, text, width=None, fillchar=u' '): - """T.center(text, [width], [fillchar]) -> string + """T.center(text, [width], [fillchar]) -> unicode Return string ``text``, centered by printable length ``width``. Padding is done using the specified fill character (default is a - space). Default width is the attached terminal's width. ``text`` is - escape-sequence safe.""" + space). Default ``width`` is the attached terminal's width. ``text`` + may contain terminal sequences.""" if width is None: width = self.width return sequences.Sequence(text, self).center(width, fillchar) @@ -446,24 +414,26 @@ def center(self, text, width=None, fillchar=u' '): def length(self, text): """T.length(text) -> int - Return printable length of string ``text``, which may contain (some - kinds) of sequences. Strings containing sequences such as 'clear', - which repositions the cursor will not give accurate results. + Return the printable length of string ``text``, which may contain + terminal sequences. Strings containing sequences such as 'clear', + which repositions the cursor, does not give accurate results, and + their printable length is evaluated *0*.. """ return sequences.Sequence(text, self).length() def strip(self, text): - """T.strip(text) -> int + """T.strip(text) -> unicode Return string ``text`` stripped of its whitespace *and* sequences. + Text containing backspace or term.left will "overstrike", so that - the string u"_\\b" or u"__\\b\\b=" becomes u"x", not u"=" (as would - actually be printed). + the string ``u"_\\b"`` or ``u"__\\b\\b="`` becomes ``u"x"``, + not ``u"="`` (as would actually be printed on a terminal). """ return sequences.Sequence(text, self).strip() def strip_seqs(self, text): - """T.strip_seqs(text) -> int + """T.strip_seqs(text) -> unicode Return string ``text`` stripped only of its sequences. """ @@ -504,9 +474,9 @@ def kbhit(self, timeout=0): Returns True if a keypress has been detected on keyboard. - When ``timeout`` is 0, this call is non-blocking(default), or blocking - indefinitely until keypress when ``None``, and blocking until keypress - or time elapsed when ``timeout`` is non-zero. + When ``timeout`` is 0, this call is non-blocking(default). + Otherwise blocking until keypress is detected, returning + True, or False after ``timeout`` seconds have elapsed. If input is not a terminal, False is always returned. """ @@ -549,13 +519,13 @@ def cbreak(self): explicitly print any input received, if they so wish. More information can be found in the manual page for curses.h, - http://www.openbsd.org/cgi-bin/man.cgi?query=cbreak + http://www.openbsd.org/cgi-bin/man.cgi?query=cbreak The python manual for curses, - http://docs.python.org/2/library/curses.html + http://docs.python.org/2/library/curses.html Note also that setcbreak sets VMIN = 1 and VTIME = 0, - http://www.unixwiz.net/techtips/termios-vmin-vtime.html + http://www.unixwiz.net/techtips/termios-vmin-vtime.html """ if self.keyboard_fd is not None: # save current terminal mode, @@ -573,11 +543,11 @@ def cbreak(self): @contextlib.contextmanager def raw(self): - """Return a context manager that enters 'raw' mode. Raw mode is - similar to cbreak mode, in that characters typed are immediately passed - through to the user program. The differences are that in raw mode, the - interrupt, quit, suspend, and flow control characters are all passed - through uninterpreted, instead of generating a signal. + """Return a context manager that enters *raw* mode. Raw mode is + similar to *cbreak* mode, in that characters typed are immediately + available to ``inkey()`` with one exception: the interrupt, quit, + suspend, and flow control characters are all passed through as their + raw character values instead of generating a signal. """ if self.keyboard_fd is not None: # save current terminal mode, @@ -600,17 +570,19 @@ def inkey(self, timeout=None, esc_delay=0.35): keypress is received or ``timeout`` elapsed, if specified. When used without the context manager ``cbreak``, stdin remains - line-buffered, and this function will block until return is pressed. + line-buffered, and this function will block until return is pressed, + even though only one unicode character is returned at a time.. The value returned is an instance of ``Keystroke``, with properties - ``is_sequence``, and, when True, non-None values for ``code`` and - ``name``. The value of ``code`` may be compared against attributes - of this terminal beginning with KEY, such as KEY_ESCAPE. + ``is_sequence``, and, when True, non-None values for attributes + ``code`` and ``name``. The value of ``code`` may be compared against + attributes of this terminal beginning with *KEY*, such as + ``KEY_ESCAPE``. - To distinguish between KEY_ESCAPE, and sequences beginning with + To distinguish between ``KEY_ESCAPE``, and sequences beginning with escape, the ``esc_delay`` specifies the amount of time after receiving - the escape character ('\x1b') to seek for application keys. - + the escape character ('\\x1b', chr(27)) to seek for the completion + of other application keys before returning ``KEY_ESCAPE``. """ # TODO(jquast): "meta sends escape", where alt+1 would send '\x1b1', # what do we do with that? Surely, something useful. diff --git a/docs/conf.py b/docs/conf.py index 96b183dc..f79d5cbf 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -222,5 +222,7 @@ ('index', 'Blessed', u'Blessed Documentation', [u'Jeff Quast'], 1) ] +autodoc_member_order = 'bysource' + del blessed # imported but unused diff --git a/docs/index.rst b/docs/index.rst index ec23b8a8..b5bdcd77 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -19,26 +19,30 @@ Then Read This If You Want Internal modules are as follows. -terminal --------- +blessed.terminal +---------------- .. automodule:: blessed.terminal :members: + :undoc-members: -formatters ----------- +blessed.formatters +------------------ .. automodule:: blessed.formatters :members: + :undoc-members: -keyboard --------- +blessed.keyboard +---------------- .. automodule:: blessed.keyboard :members: + :undoc-members: -sequences ---------- +blessed.sequences +----------------- .. automodule:: blessed.sequences :members: + :undoc-members: From fa05d7f5b0f1150154e1a382a13bc1c02425d6f1 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 18:00:43 -0700 Subject: [PATCH 126/459] wait a bit longer on_resize, worms==py3 only --- bin/on_resize.py | 2 +- bin/worms.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/on_resize.py b/bin/on_resize.py index bce18beb..dd955605 100755 --- a/bin/on_resize.py +++ b/bin/on_resize.py @@ -10,4 +10,4 @@ def on_resize(sig, action): signal.signal(signal.SIGWINCH, on_resize) -term.inkey(1) +term.inkey(10) diff --git a/bin/worms.py b/bin/worms.py index 2afdeaad..5a8e4db0 100755 --- a/bin/worms.py +++ b/bin/worms.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 from __future__ import division, print_function from collections import namedtuple from random import randrange From f4fccf3cae8afde9eddf307edb734b18686d7190 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 18:05:29 -0700 Subject: [PATCH 127/459] badges --- README.rst | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 794facbf..82cef094 100644 --- a/README.rst +++ b/README.rst @@ -1,3 +1,19 @@ +.. image:: https://secure.travis-ci.org/jquast/blessed.png + :target: https://travis-ci.org/jquast/blessed + :alt: travis continous integration +.. image:: http://coveralls.io/repos/jquast/blessed/badge.png + :target: http://coveralls.io/r/jquast/blessed + :alt: coveralls code coveraage +.. image:: https://pypip.in/d/blessed/badge.png + :target: https://pypi.python.org/pypi/blessed/ + :alt: Downloads +.. image:: https://pypip.in/v/blessed/badge.png + :target: https://pypi.python.org/pypi/blessed/ + :alt: Latest Version +.. image:: https://pypip.in/license/blessed/badge.png + :target: https://pypi.python.org/pypi/blessed/ + :alt: License + ======= Blessed ======= @@ -583,13 +599,6 @@ Bugs or suggestions? Visit the `issue tracker`_. .. _`issue tracker`: https://github.com/jquast/blessed/issues/ -.. image:: https://secure.travis-ci.org/jquast/blessed.png - :target: https://travis-ci.org/jquast/blessed - :alt: travis continous integration -.. image:: http://coveralls.io/repos/jquast/blessed/badge.png - :target: http://coveralls.io/r/jquast/blessed - :alt: coveralls code coveraage - For patches, please construct a test case if possible. To test, install and execute python package command *tox*. From a4b83a6b149cb90ddd4e08195c187561c0b83748 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 18:06:52 -0700 Subject: [PATCH 128/459] try images centered --- README.rst | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 82cef094..8e69ecdf 100644 --- a/README.rst +++ b/README.rst @@ -1,18 +1,23 @@ .. image:: https://secure.travis-ci.org/jquast/blessed.png :target: https://travis-ci.org/jquast/blessed :alt: travis continous integration + :align: center .. image:: http://coveralls.io/repos/jquast/blessed/badge.png :target: http://coveralls.io/r/jquast/blessed :alt: coveralls code coveraage -.. image:: https://pypip.in/d/blessed/badge.png - :target: https://pypi.python.org/pypi/blessed/ - :alt: Downloads + :align: center .. image:: https://pypip.in/v/blessed/badge.png :target: https://pypi.python.org/pypi/blessed/ :alt: Latest Version + :align: center .. image:: https://pypip.in/license/blessed/badge.png :target: https://pypi.python.org/pypi/blessed/ :alt: License + :align: center +.. image:: https://pypip.in/d/blessed/badge.png + :target: https://pypi.python.org/pypi/blessed/ + :alt: Downloads + :align: center ======= Blessed From 90876b1d524c04969051d425860a705262954c33 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 18:10:40 -0700 Subject: [PATCH 129/459] docfixes --- README.rst | 5 ----- blessed/formatters.py | 1 + blessed/keyboard.py | 14 +++++++------- blessed/sequences.py | 34 ++++++++++++++++++++-------------- 4 files changed, 28 insertions(+), 26 deletions(-) diff --git a/README.rst b/README.rst index 8e69ecdf..e2b336aa 100644 --- a/README.rst +++ b/README.rst @@ -1,23 +1,18 @@ .. image:: https://secure.travis-ci.org/jquast/blessed.png :target: https://travis-ci.org/jquast/blessed :alt: travis continous integration - :align: center .. image:: http://coveralls.io/repos/jquast/blessed/badge.png :target: http://coveralls.io/r/jquast/blessed :alt: coveralls code coveraage - :align: center .. image:: https://pypip.in/v/blessed/badge.png :target: https://pypi.python.org/pypi/blessed/ :alt: Latest Version - :align: center .. image:: https://pypip.in/license/blessed/badge.png :target: https://pypi.python.org/pypi/blessed/ :alt: License - :align: center .. image:: https://pypip.in/d/blessed/badge.png :target: https://pypi.python.org/pypi/blessed/ :alt: Downloads - :align: center ======= Blessed diff --git a/blessed/formatters.py b/blessed/formatters.py index 620450ca..a7bd0214 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -54,6 +54,7 @@ def __call__(self, *args): class FormattingString(unicode): """A Unicode string which can be called using ``text``, returning a new string, ``attr`` + ``text`` + ``normal``:: + style = FormattingString(term.bright_blue, term.normal) style('Big Blue') '\x1b[94mBig Blue\x1b(B\x1b[m' diff --git a/blessed/keyboard.py b/blessed/keyboard.py index 8d62ae18..7e9a9af3 100644 --- a/blessed/keyboard.py +++ b/blessed/keyboard.py @@ -76,13 +76,13 @@ def get_keyboard_codes(): values and their mnemonic name. Such as key 260, with the value of its identity, 'KEY_LEFT'. These are derived from the attributes by the same of the curses module, with the following exceptions: - * KEY_DELETE in place of KEY_DC - * KEY_INSERT in place of KEY_IC - * KEY_PGUP in place of KEY_PPAGE - * KEY_PGDOWN in place of KEY_NPAGE - * KEY_ESCAPE in place of KEY_EXIT - * KEY_SUP in place of KEY_SR - * KEY_SDOWN in place of KEY_SF + * KEY_DELETE in place of KEY_DC + * KEY_INSERT in place of KEY_IC + * KEY_PGUP in place of KEY_PPAGE + * KEY_PGDOWN in place of KEY_NPAGE + * KEY_ESCAPE in place of KEY_EXIT + * KEY_SUP in place of KEY_SR + * KEY_SDOWN in place of KEY_SF """ keycodes = OrderedDict(get_curses_keycodes()) keycodes.update(CURSES_KEYCODE_OVERRIDE_MIXIN) diff --git a/blessed/sequences.py b/blessed/sequences.py index 161aa214..cf080f56 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -63,20 +63,26 @@ def init_sequence_patterns(term): and parses several known terminal capabilities, and builds a database of regular expressions and attaches them to ``term`` as attributes: - ``_re_will_move``: any sequence matching this pattern will - cause the terminal cursor to move (such as term.home). - ``_re_wont_move``: any sequence matching this pattern will - not cause the cursor to move (such as term.bold). - ``_re_cuf``: regular expression that matches term.cuf(N) - (move N characters forward). - ``_cuf1``: term.cuf1 sequence (cursor forward 1 character) - as a static value. - ``_re_cub``: regular expression that matches term.cub(N) - (move N characters backward). - ``_cub1``: term.cuf1 sequence (cursor backward 1 character) - as a static value. - - These attribtues make it possible to perform introspection on + ``_re_will_move`` + any sequence matching this pattern will cause the terminal + cursor to move (such as *term.home*). + ``_re_wont_move`` + any sequence matching this pattern will not cause the cursor + to move (such as *term.bold*). + ``_re_cuf`` + regular expression that matches term.cuf(N) (move N characters + forward). + ``_cuf1`` + *term.cuf1* sequence (cursor forward 1 character) as a static + value. + ``_re_cub`` + regular expression that matches term.cub(N) (move N characters + backward). + ``_cub1`` + *term.cuf1* sequence (cursor backward 1 character) as a static + value. + + These attributes make it possible to perform introspection on strings containing sequences generated by this terminal, to determine the printable length of a string. From 7e5804bc1e53986cdb66c1734d340f049bc7e707 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 18:26:44 -0700 Subject: [PATCH 130/459] more docfixes --- blessed/formatters.py | 7 ++-- blessed/keyboard.py | 75 ++++++++++++++++++++----------------------- blessed/sequences.py | 1 + blessed/terminal.py | 2 +- 4 files changed, 40 insertions(+), 45 deletions(-) diff --git a/blessed/formatters.py b/blessed/formatters.py index a7bd0214..0fc6a6da 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -149,7 +149,8 @@ def resolve_capability(term, attr): def resolve_color(term, color): """Resolve a color, to callable capability, valid ``color`` capabilities - are format ``red``, or ``on_right_green``. + are simple colors such as ``red``, or compounded, such as + ``on_bright_green``. """ # NOTE(erikrose): Does curses automatically exchange red and blue and cyan # and yellow when a terminal supports setf/setb rather than setaf/setab? @@ -172,8 +173,8 @@ def resolve_color(term, color): def resolve_attribute(term, attr): """Resolve a sugary or plain capability name, color, or compound - formatting function name into a *callable* unicode string - capability, ``ParameterizingString`` or ``FormattingString``. + formatting name into a *callable* unicode string capability, + ``ParameterizingString`` or ``FormattingString``. """ # A simple color, such as `red' or `blue'. if attr in COLORS: diff --git a/blessed/keyboard.py b/blessed/keyboard.py index 7e9a9af3..82ecd6c2 100644 --- a/blessed/keyboard.py +++ b/blessed/keyboard.py @@ -28,11 +28,11 @@ class Keystroke(unicode): """A unicode-derived class for describing keyboard input returned by - the ``keypress()`` method of ``Terminal``, which may, at times, be a - multibyte sequence, providing properties ``is_sequence`` as True when - the string is a known sequence, and ``code``, which returns an integer - value that may be compared against the terminal class attributes such - as ``KEY_LEFT``. + the ``inkey()`` method of ``Terminal``, which may, at times, be a + multibyte sequence, providing properties ``is_sequence`` as ``True`` + when the string is a known sequence, and ``code``, which returns an + integer value that may be compared against the terminal class attributes + such as ``KEY_LEFT``. """ def __new__(cls, ucs='', code=None, name=None): new = unicode.__new__(cls, ucs) @@ -42,10 +42,7 @@ def __new__(cls, ucs='', code=None, name=None): @property def is_sequence(self): - """K.is_sequence -> bool - - Returns True if value represents a multibyte sequence. - """ + "Whether the value represents a multibyte sequence (bool)." return self._code is not None def __repr__(self): @@ -54,18 +51,12 @@ def __repr__(self): @property def name(self): - """K.name -> str - - Returns string-name of key sequence, such as 'KEY_LEFT' - """ + "String-name of key sequence, such as ``'KEY_LEFT'`` (str)." return self._name @property def code(self): - """K.code -> int - - Returns integer keycode value of multibyte sequence. - """ + "Integer keycode value of multibyte sequence (int)." return self._code @@ -73,16 +64,17 @@ def get_keyboard_codes(): """get_keyboard_codes() -> dict Returns dictionary of (code, name) pairs for curses keyboard constant - values and their mnemonic name. Such as key 260, with the value of its - identity, 'KEY_LEFT'. These are derived from the attributes by the same - of the curses module, with the following exceptions: - * KEY_DELETE in place of KEY_DC - * KEY_INSERT in place of KEY_IC - * KEY_PGUP in place of KEY_PPAGE - * KEY_PGDOWN in place of KEY_NPAGE - * KEY_ESCAPE in place of KEY_EXIT - * KEY_SUP in place of KEY_SR - * KEY_SDOWN in place of KEY_SF + values and their mnemonic name. Such as key ``260``, with the value of + its identity, ``KEY_LEFT``. These are derived from the attributes by the + same of the curses module, with the following exceptions: + + * ``KEY_DELETE`` in place of ``KEY_DC`` + * ``KEY_INSERT`` in place of ``KEY_IC`` + * ``KEY_PGUP`` in place of ``KEY_PPAGE`` + * ``KEY_PGDOWN`` in place of ``KEY_NPAGE`` + * ``KEY_ESCAPE`` in place of ``KEY_EXIT`` + * ``KEY_SUP`` in place of ``KEY_SR`` + * ``KEY_SDOWN`` in place of ``KEY_SF`` """ keycodes = OrderedDict(get_curses_keycodes()) keycodes.update(CURSES_KEYCODE_OVERRIDE_MIXIN) @@ -93,12 +85,14 @@ def get_keyboard_codes(): def _alternative_left_right(term): - """Return dict of sequence term._cuf1, _cub1 as values curses.KEY_RIGHT, - LEFT when appropriate. + """_alternative_left_right(T) -> dict - some terminals report a different value for kcuf1 than cuf1, - but actually send the value of cuf1 for right arrow key - (which is non-destructive space). + Return dict of sequences ``term._cuf1``, and ``term._cub1``, + valued as ``KEY_RIGHT``, ``KEY_LEFT`` when appropriate if available. + + some terminals report a different value for *kcuf1* than *cuf1*, but + actually send the value of *cuf1* for right arrow key (which is + non-destructive space). """ keymap = dict() if term._cuf1 and term._cuf1 != u' ': @@ -109,7 +103,7 @@ def _alternative_left_right(term): def get_keyboard_sequences(term): - """init_keyboard_sequences(T) -> (OrderedDict) + """get_keyboard_sequences(T) -> (OrderedDict) Initialize and return a keyboard map and sequence lookup table, (sequence, constant) from blessed Terminal instance ``term``, @@ -176,14 +170,13 @@ def resolve_sequence(text, mapper, codes): ('KEY_SDOWN', curses.KEY_SF), ) -# In a perfect world, terminal emulators would always send exactly what the -# terminfo(5) capability database plans for them, accordingly by the value -# of the TERM name they declare. -# -# But this isn't a perfect world. Many vt220-derived terminals, such as -# those declaring 'xterm', will continue to send vt220 codes instead of -# their native-declared codes. This goes for many: rxvt, putty, iTerm. -# +"""In a perfect world, terminal emulators would always send exactly what +the terminfo(5) capability database plans for them, accordingly by the +value of the ``TERM`` name they declare. + +But this isn't a perfect world. Many vt220-derived terminals, such as +those declaring 'xterm', will continue to send vt220 codes instead of +their native-declared codes. This goes for many: rxvt, putty, iTerm.""" DEFAULT_SEQUENCE_MIXIN = ( # these common control characters (and 127, ctrl+'?') mapped to # an application key definition. diff --git a/blessed/sequences.py b/blessed/sequences.py index cf080f56..4262e394 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -63,6 +63,7 @@ def init_sequence_patterns(term): and parses several known terminal capabilities, and builds a database of regular expressions and attaches them to ``term`` as attributes: + ``_re_will_move`` any sequence matching this pattern will cause the terminal cursor to move (such as *term.home*). diff --git a/blessed/terminal.py b/blessed/terminal.py index 182c02b7..f92b4308 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -581,7 +581,7 @@ def inkey(self, timeout=None, esc_delay=0.35): To distinguish between ``KEY_ESCAPE``, and sequences beginning with escape, the ``esc_delay`` specifies the amount of time after receiving - the escape character ('\\x1b', chr(27)) to seek for the completion + the escape character (chr(27)) to seek for the completion of other application keys before returning ``KEY_ESCAPE``. """ # TODO(jquast): "meta sends escape", where alt+1 would send '\x1b1', From a3205035ee55e5fd468b7dc1fddf5c802435e428 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 18:35:09 -0700 Subject: [PATCH 131/459] incremental doc updates --- blessed/sequences.py | 66 +++++++++++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 25 deletions(-) diff --git a/blessed/sequences.py b/blessed/sequences.py index 4262e394..f8d89817 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -338,23 +338,27 @@ class Sequence(unicode): """ def __new__(cls, sequence_text, term): + "Sequence(sequence_text, term) -> unicode object" new = unicode.__new__(cls, sequence_text) new._term = term return new def ljust(self, width, fillchar=u' '): + "S.ljust(width, fillchar=u'') -> unicode" return self + fillchar * (max(0, width - self.length())) def rjust(self, width, fillchar=u' '): + "S.rjust(width, fillchar=u'') -> unicode" return fillchar * (max(0, width - self.length())) + self def center(self, width, fillchar=u' '): + "S.center(width, fillchar=u'') -> unicode" split = max(0.0, float(width) - self.length()) / 2 return (fillchar * (max(0, int(math.floor(split)))) + self + fillchar * (max(0, int(math.ceil(split))))) def length(self): - """ S.length() -> integer + """S.length() -> int Return the printable length of a string that contains (some types) of (escape) sequences. Although accounted for, strings containing @@ -372,14 +376,14 @@ def length(self): return len(self.strip_seqs()) def strip(self): - """ S.strip() -> str + """S.strip() -> unicode Strips sequences and whitespaces of ``S`` and returns. """ return self.strip_seqs().strip() def strip_seqs(self): - """ S.strip_seqs() -> str + """S.strip_seqs() -> unicode Return a string without sequences for a string that contains (most types) of (escape) sequences for the Terminal with which @@ -412,8 +416,7 @@ def strip_seqs(self): def measure_length(ucs, term): - """ - measure_length(S) -> integer + """measure_length(S, term) -> int Returns non-zero for string ``S`` that begins with a terminal sequence, that is: the width of the first unprintable sequence found in S. For use @@ -444,8 +447,37 @@ def measure_length(ucs, term): return 0 +def termcap_distance(ucs, cap, unit, term): + """termcap_distance(S, cap, unit, term) -> int + + Match horizontal distance by simple ``cap`` capability name, ``cub1`` or + ``cuf1``, with string matching the sequences identified by Terminal + instance ``term`` and a distance of ``unit`` *1* or *-1*, for right and + left, respectively. + + Otherwise, by regular expression (using dynamic regular expressions built + using ``cub(n)`` and ``cuf(n)``. Failing that, any of the standard SGR + sequences (``\033[C``, ``\033[D``, ``\033[nC``, ``\033[nD``). + + Returns 0 if unmatched. + """ + assert cap in ('cuf', 'cub') + # match cub1(left), cuf1(right) + one = getattr(term, '_%s1' % (cap,)) + if one and ucs.startswith(one): + return unit + + # match cub(n), cuf(n) using regular expressions + re_pattern = getattr(term, '_re_%s' % (cap,)) + _dist = re_pattern and re_pattern.match(ucs) + if _dist: + return unit * int(_dist.group(1)) + + return 0 + + def horizontal_distance(ucs, term): - """ horizontal_distance(S) -> integer + """horizontal_distance(S, term) -> int Returns Integer in SGR sequence of form [C (T.move_right(nn)). Returns Integer -(n) in SGR sequence of form [D (T.move_left(nn)). @@ -455,24 +487,6 @@ def horizontal_distance(ucs, term): position cannot be determined: 8 is always (and, incorrectly) returned. """ - def term_distance(cap, unit): - """ Match by simple cub1/cuf1 string matching (distance of 1) - Or, by regular expression (using dynamic regular expressions - built using cub(n) and cuf(n). Failing that, the standard - SGR sequences (\033[C, \033[D, \033[nC, \033[nD - """ - assert cap in ('cuf', 'cub') - # match cub1(left), cuf1(right) - one = getattr(term, '_%s1' % (cap,)) - if one and ucs.startswith(one): - return unit - - # match cub(n), cuf(n) using regular expressions - re_pattern = getattr(term, '_re_%s' % (cap,)) - _dist = re_pattern and re_pattern.match(ucs) - if _dist: - return unit * int(_dist.group(1)) - if ucs.startswith('\b'): return -1 @@ -486,4 +500,6 @@ def term_distance(cap, unit): # \t would consume on the output device! return 8 - return term_distance('cub', -1) or term_distance('cuf', 1) or 0 + return (termcap_distance(ucs, 'cub', -1, term) or + termcap_distance(ucs, 'cuf', 1, term) or + 0) From 1f35c0384623f1cc44d2bef27aa3f89b98f24910 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 19:15:23 -0700 Subject: [PATCH 132/459] documentation spit and polish --- blessed/formatters.py | 55 +++++++++++------ blessed/sequences.py | 122 +++++++++++++++++++++---------------- blessed/terminal.py | 136 +++++++++++++++++++++++------------------- docs/index.rst | 30 +++++----- 4 files changed, 197 insertions(+), 146 deletions(-) diff --git a/blessed/formatters.py b/blessed/formatters.py index 0fc6a6da..6f0ebf78 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -1,29 +1,35 @@ "This sub-module provides formatting functions." import curses -_derivitives = ('on', 'bright', 'on_bright',) +_derivatives = ('on', 'bright', 'on_bright',) _colors = set('black red green yellow blue magenta cyan white'.split()) _compoundables = set('bold underline reverse blink dim italic shadow ' 'standout subscript superscript'.split()) +#: Valid colors and their background (on), bright, and bright-bg derivatives. COLORS = set(['_'.join((derivitive, color)) - for derivitive in _derivitives + for derivitive in _derivatives for color in _colors]) | _colors +#: All valid compoundable names. COMPOUNDABLES = (COLORS | _compoundables) class ParameterizingString(unicode): - "A Unicode string which can be called as a parameterizing termcap." + """A Unicode string which can be called as a parameterizing termcap. - def __new__(cls, name, attr, normal): + For example:: + + >> move = ParameterizingString('move') + >> move(0, 0)('text') + """ + + def __new__(cls, name, normal): """ :arg name: name of terminal capability. - :arg attr: terminal capability name to receive arguments. - :arg normal: If non-None, indicates that, once parametrized, this can - be used as a ``FormattingString``. The value is used as the - "normal" capability. + :arg attr: terminal attribute sequence to receive arguments. + :arg normal: terminating sequence for this capability. """ new = unicode.__new__(cls, attr) new._name = name @@ -31,6 +37,12 @@ def __new__(cls, name, attr, normal): return new def __call__(self, *args): + """P(*args) -> unicode + + Return evaluated terminal capability (self), receiving arguments + ``*args``, followed by the terminating sequence (self.normal) into + a FormattingString capable of being called. + """ try: # Re-encode the cap, because tparm() takes a bytestring in Python # 3. However, appear to be a plain Unicode string otherwise so @@ -52,14 +64,19 @@ def __call__(self, *args): class FormattingString(unicode): - """A Unicode string which can be called using ``text``, returning a - new string, ``attr`` + ``text`` + ``normal``:: + """A Unicode string which can be called using ``text``, + returning a new string, ``attr`` + ``text`` + ``normal``:: - style = FormattingString(term.bright_blue, term.normal) - style('Big Blue') + >> style = FormattingString(term.bright_blue, term.normal) + >> style('Big Blue') '\x1b[94mBig Blue\x1b(B\x1b[m' """ + def __new__(cls, attr, normal): + """ + :arg attr: terminal attribute sequence. + :arg normal: terminating sequence for this attribute. + """ new = unicode.__new__(cls, attr) new._normal = normal return new @@ -148,9 +165,13 @@ def resolve_capability(term, attr): def resolve_color(term, color): - """Resolve a color, to callable capability, valid ``color`` capabilities - are simple colors such as ``red``, or compounded, such as - ``on_bright_green``. + """resolve_color(T, color) -> FormattingString() + + Resolve a ``color`` name to callable capability, ``FormattingString`` + unless ``term.number_of_colors`` is 0, then ``NullCallableString``. + + Valid ``color`` capabilities names are any of the simple color + names, such as ``red``, or compounded, such as ``on_bright_green``. """ # NOTE(erikrose): Does curses automatically exchange red and blue and cyan # and yellow when a terminal supports setf/setb rather than setaf/setab? @@ -193,6 +214,4 @@ def resolve_attribute(term, attr): resolution = (resolve_attribute(term, fmt) for fmt in formatters) return FormattingString(u''.join(resolution), term.normal) else: - return ParameterizingString(name=attr, - attr=resolve_capability(term, attr), - normal=term.normal) + return ParameterizingString(name=attr, normal=term.normal) diff --git a/blessed/sequences.py b/blessed/sequences.py index f8d89817..1d8544c0 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -59,37 +59,37 @@ def _build_any_numeric_capability(term, cap, num=99, nparams=1): def init_sequence_patterns(term): - """ Given a Terminal instance, ``term``, this function processes - and parses several known terminal capabilities, and builds a - database of regular expressions and attaches them to ``term`` - as attributes: - - ``_re_will_move`` - any sequence matching this pattern will cause the terminal - cursor to move (such as *term.home*). - ``_re_wont_move`` - any sequence matching this pattern will not cause the cursor - to move (such as *term.bold*). - ``_re_cuf`` - regular expression that matches term.cuf(N) (move N characters - forward). - ``_cuf1`` - *term.cuf1* sequence (cursor forward 1 character) as a static - value. - ``_re_cub`` - regular expression that matches term.cub(N) (move N characters - backward). - ``_cub1`` - *term.cuf1* sequence (cursor backward 1 character) as a static - value. - - These attributes make it possible to perform introspection on - strings containing sequences generated by this terminal, to determine - the printable length of a string. - - For debugging, complimentary lists of these sequence matching - pattern values prior to compilation are attached as attributes - ``_will_move``, ``_wont_move``, ``_cuf``, ``_cub``. + """Given a Terminal instance, ``term``, this function processes + and parses several known terminal capabilities, and builds a + database of regular expressions and attaches them to ``term`` + as attributes: + + ``_re_will_move`` + any sequence matching this pattern will cause the terminal + cursor to move (such as *term.home*). + ``_re_wont_move`` + any sequence matching this pattern will not cause the cursor + to move (such as *term.bold*). + ``_re_cuf`` + regular expression that matches term.cuf(N) (move N characters + forward). + ``_cuf1`` + *term.cuf1* sequence (cursor forward 1 character) as a static + value. + ``_re_cub`` + regular expression that matches term.cub(N) (move N characters + backward). + ``_cub1`` + *term.cuf1* sequence (cursor backward 1 character) as a static + value. + + These attributes make it possible to perform introspection on strings + containing sequences generated by this terminal, to determine the + printable length of a string. + + For debugging, complimentary lists of these sequence matching pattern + values prior to compilation are attached as attributes ``_will_move``, + ``_wont_move``, ``_cuf``, ``_cub``. """ if term._kind in _BINTERM_UNSUPPORTED: warnings.warn(_BINTERM_UNSUPPORTED_MSG) @@ -329,6 +329,8 @@ def _wrap_chunks(self, chunks): lines.append(indent + u''.join(cur_line)) return lines +SequenceTextWrapper.__doc__ = textwrap.TextWrapper.__doc__ + class Sequence(unicode): """ @@ -338,21 +340,28 @@ class Sequence(unicode): """ def __new__(cls, sequence_text, term): - "Sequence(sequence_text, term) -> unicode object" + """Sequence(sequence_text, term) -> unicode object + + :arg sequence_text: A string containing sequences. + :arg term: Terminal instance this string was created with. + """ new = unicode.__new__(cls, sequence_text) new._term = term return new def ljust(self, width, fillchar=u' '): - "S.ljust(width, fillchar=u'') -> unicode" + """S.ljust(width, fillchar=u'') -> unicode""" return self + fillchar * (max(0, width - self.length())) def rjust(self, width, fillchar=u' '): - "S.rjust(width, fillchar=u'') -> unicode" + """S.rjust(width, fillchar=u'') -> unicode""" return fillchar * (max(0, width - self.length())) + self def center(self, width, fillchar=u' '): - "S.center(width, fillchar=u'') -> unicode" + """S.center(width, fillchar=u'') -> unicode + + Returns string derived from unicode string ``S`` stripped of whitespace + and terminal sequences.""" split = max(0.0, float(width) - self.length()) / 2 return (fillchar * (max(0, int(math.floor(split)))) + self + fillchar * (max(0, int(math.ceil(split))))) @@ -360,14 +369,16 @@ def center(self, width, fillchar=u' '): def length(self): """S.length() -> int - Return the printable length of a string that contains (some types) of - (escape) sequences. Although accounted for, strings containing - sequences such as 'clear' will not give accurate returns, it is - considered un-lengthy (length of 0). + Returns printable length of unicode string ``S`` that may contain + terminal sequences. + + Although accounted for, strings containing sequences such as + ``term.clear`` will not give accurate returns, it is considered + un-lengthy (length of 0). - Strings contaning term.left or '\b' will cause "overstrike", but - a length less than 0 is not ever returned. So '_\b+' is a length of - 1 ('+'), but '\b' is simply a length of 0. + Strings containing ``term.left`` or ``\b`` will cause "overstrike", + but a length less than 0 is not ever returned. So ``_\b+`` is a + length of 1 (``+``), but ``\b`` is simply a length of 0. """ # TODO(jquast): Should we implement the terminal printable # width of 'East Asian Fullwidth' and 'East Asian Wide' characters, @@ -378,7 +389,8 @@ def length(self): def strip(self): """S.strip() -> unicode - Strips sequences and whitespaces of ``S`` and returns. + Returns string derived from unicode string ``S``, stripped of + whitespace and terminal sequences. """ return self.strip_seqs().strip() @@ -423,16 +435,20 @@ def measure_length(ucs, term): as a *next* pointer to skip past sequences. If string ``S`` is not a sequence, 0 is returned. - A sequence may be a typical terminal sequence beginning with Escape (\x1b), - especially a Control Sequence Initiator ("CSI", '\x1b[' ... ), or those of - '\a', '\b', '\r', '\n', '\xe0' (shift in), '\x0f' (shift out). They do not - necessarily have to begin with CSI, they need only match the capabilities - of attributes ``_re_will_move`` and ``_re_wont_move`` of terminal ``term``. + A sequence may be a typical terminal sequence beginning with Escape + (``\x1b``), especially a Control Sequence Initiator (``CSI``, ``\x1b[``, + ...), or those of ``\a``, ``\b``, ``\r``, ``\n``, ``\xe0`` (shift in), + ``\x0f`` (shift out). They do not necessarily have to begin with CSI, they + need only match the capabilities of attributes ``_re_will_move`` and + ``_re_wont_move`` of terminal ``term``. """ + # simple terminal control characters, ctrl_seqs = u'\a\b\r\n\x0e\x0f' + if any([ucs.startswith(_ch) for _ch in ctrl_seqs]): return 1 + # known multibyte sequences, matching_seq = term and ( term._re_will_move.match(ucs) or @@ -440,9 +456,11 @@ def measure_length(ucs, term): term._re_cub and term._re_cub.match(ucs) or term._re_cuf and term._re_cuf.match(ucs) ) + if matching_seq: start, end = matching_seq.span() return end + # none found, must be printable! return 0 @@ -479,11 +497,11 @@ def termcap_distance(ucs, cap, unit, term): def horizontal_distance(ucs, term): """horizontal_distance(S, term) -> int - Returns Integer in SGR sequence of form [C (T.move_right(nn)). - Returns Integer -(n) in SGR sequence of form [D (T.move_left(nn)). - Returns -1 for backspace (0x08), Otherwise 0. + Returns Integer ```` in SGR sequence of form ``[C`` + (T.move_right(n)), or ``-(n)`` in sequence of form ``[D`` + (T.move_left(n)). Returns -1 for backspace (0x08), Otherwise 0. - Tabstop (\t) cannot be correctly calculated, as the relative column + Tabstop (``\t``) cannot be correctly calculated, as the relative column position cannot be determined: 8 is always (and, incorrectly) returned. """ diff --git a/blessed/terminal.py b/blessed/terminal.py index f92b4308..3f7b8dac 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -31,13 +31,28 @@ class IOUnsupportedOperation(Exception): InterruptedError = select.error # local imports -import formatters -import sequences -import keyboard +from formatters import ( + ParameterizingString, + NullCallableString, + resolve_capability, + resolve_attribute, +) + +from sequences import ( + get_keyboard_sequences, + init_sequence_patterns, + SequenceTextWrapper, + Sequence, +) + +from keyboard import ( + get_keyboard_codes, + resolve_sequence, +) class Terminal(object): - """An abstraction around terminal capabilities + """A wrapper for curses and related terminfo(5) terminal capabilities. Instance attributes: @@ -47,6 +62,43 @@ class Terminal(object): is and saves sticking lots of extra args on client functions in practice. """ + + #: Sugary names for commonly-used capabilities + _sugar = dict( + save='sc', + restore='rc', + # 'clear' clears the whole screen. + clear_eol='el', + clear_bol='el1', + clear_eos='ed', + position='cup', # deprecated + enter_fullscreen='smcup', + exit_fullscreen='rmcup', + move='cup', + move_x='hpa', + move_y='vpa', + move_left='cub1', + move_right='cuf1', + move_up='cuu1', + move_down='cud1', + hide_cursor='civis', + normal_cursor='cnorm', + reset_colors='op', # oc doesn't work on my OS X terminal. + normal='sgr0', + reverse='rev', + italic='sitm', + no_italic='ritm', + shadow='sshm', + no_shadow='rshm', + standout='smso', + no_standout='rmso', + subscript='ssubm', + no_subscript='rsubm', + superscript='ssupm', + no_superscript='rsupm', + underline='smul', + no_underline='rmul') + def __init__(self, kind=None, stream=None, force_styling=False): """Initialize the terminal. @@ -125,17 +177,17 @@ def __init__(self, kind=None, stream=None, force_styling=False): self._kind, _CUR_TERM,)) if self.does_styling: - sequences.init_sequence_patterns(self) + init_sequence_patterns(self) # build database of int code <=> KEY_NAME - self._keycodes = keyboard.get_keyboard_codes() + self._keycodes = get_keyboard_codes() # store attributes as: self.KEY_NAME = code for key_code, key_name in self._keycodes.items(): setattr(self, key_name, key_code) # build database of sequence <=> KEY_NAME - self._keymap = keyboard.get_keyboard_sequences(self) + self._keymap = get_keyboard_sequences(self) self._keyboard_buf = collections.deque() locale.setlocale(locale.LC_ALL, '') @@ -144,42 +196,6 @@ def __init__(self, kind=None, stream=None, force_styling=False): self.stream = stream - #: Sugary names for commonly-used capabilities - _sugar = dict( - save='sc', - restore='rc', - # 'clear' clears the whole screen. - clear_eol='el', - clear_bol='el1', - clear_eos='ed', - position='cup', # deprecated - enter_fullscreen='smcup', - exit_fullscreen='rmcup', - move='cup', - move_x='hpa', - move_y='vpa', - move_left='cub1', - move_right='cuf1', - move_up='cuu1', - move_down='cud1', - hide_cursor='civis', - normal_cursor='cnorm', - reset_colors='op', # oc doesn't work on my OS X terminal. - normal='sgr0', - reverse='rev', - italic='sitm', - no_italic='ritm', - shadow='sshm', - no_shadow='rshm', - standout='smso', - no_standout='rmso', - subscript='ssubm', - no_subscript='rsubm', - superscript='ssupm', - no_superscript='rsupm', - underline='smul', - no_underline='rmul') - def __getattr__(self, attr): """Return a terminal capability as Unicode string. @@ -198,8 +214,8 @@ def __getattr__(self, attr): manual page terminfo(5) for a complete list of capabilities. """ if not self.does_styling: - return formatters.NullCallableString() - val = formatters.resolve_attribute(self, attr) + return NullCallableString() + val = resolve_attribute(self, attr) # Cache capability codes. setattr(self, attr, val) return val @@ -335,26 +351,22 @@ def color(self): """ if not self.does_styling: - return formatters.NullCallableString() - return formatters.ParameterizingString(name='color', - attr=self._foreground_color, - normal=self.normal) + return NullCallableString() + return ParameterizingString(name='color', normal=self.normal) @property def on_color(self): "Returns capability that sets the background color." if not self.does_styling: - return formatters.NullCallableString() - return formatters.ParameterizingString(name='on_color', - attr=self._background_color, - normal=self.normal) + return NullCallableString() + return ParameterizingString(name='on_color', normal=self.normal) @property def normal(self): "Returns sequence that resets video attribute." if self._normal: return self._normal - self._normal = formatters.resolve_capability(self, 'normal') + self._normal = resolve_capability(self, 'normal') return self._normal @property @@ -387,7 +399,7 @@ def ljust(self, text, width=None, fillchar=u' '): may contain terminal sequences.""" if width is None: width = self.width - return sequences.Sequence(text, self).ljust(width, fillchar) + return Sequence(text, self).ljust(width, fillchar) def rjust(self, text, width=None, fillchar=u' '): """T.rjust(text, [width], [fillchar]) -> unicode @@ -398,7 +410,7 @@ def rjust(self, text, width=None, fillchar=u' '): may contain terminal sequences.""" if width is None: width = self.width - return sequences.Sequence(text, self).rjust(width, fillchar) + return Sequence(text, self).rjust(width, fillchar) def center(self, text, width=None, fillchar=u' '): """T.center(text, [width], [fillchar]) -> unicode @@ -409,7 +421,7 @@ def center(self, text, width=None, fillchar=u' '): may contain terminal sequences.""" if width is None: width = self.width - return sequences.Sequence(text, self).center(width, fillchar) + return Sequence(text, self).center(width, fillchar) def length(self, text): """T.length(text) -> int @@ -419,7 +431,7 @@ def length(self, text): which repositions the cursor, does not give accurate results, and their printable length is evaluated *0*.. """ - return sequences.Sequence(text, self).length() + return Sequence(text, self).length() def strip(self, text): """T.strip(text) -> unicode @@ -430,14 +442,14 @@ def strip(self, text): the string ``u"_\\b"`` or ``u"__\\b\\b="`` becomes ``u"x"``, not ``u"="`` (as would actually be printed on a terminal). """ - return sequences.Sequence(text, self).strip() + return Sequence(text, self).strip() def strip_seqs(self, text): """T.strip_seqs(text) -> unicode Return string ``text`` stripped only of its sequences. """ - return sequences.Sequence(text, self).strip_seqs() + return Sequence(text, self).strip_seqs() def wrap(self, text, width=None, **kwargs): """T.wrap(text, [width=None, indent=u'', ...]) -> unicode @@ -463,7 +475,7 @@ def wrap(self, text, width=None, **kwargs): lines = [] for line in text.splitlines(): lines.extend( - (_linewrap for _linewrap in sequences.SequenceTextWrapper( + (_linewrap for _linewrap in SequenceTextWrapper( width=width, term=self, **kwargs).wrap(text)) if line.strip() else (u'',)) @@ -606,7 +618,7 @@ def _decode_next(): byte = os.read(self.keyboard_fd, 1) return self._keyboard_decoder.decode(byte, final=False) - resolve = functools.partial(keyboard.resolve_sequence, + resolve = functools.partial(resolve_sequence, mapper=self._keymap, codes=self._keycodes) diff --git a/docs/index.rst b/docs/index.rst index b5bdcd77..0383b3a1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,43 +5,45 @@ Blessed API Reference Read The Readme First ===================== -This is the API documentation for the Blessed terminal library. Because -Blessed uses quite a bit of dynamism, you should `read the readme first`_, as -not all the useful stuff shows up in these autogenerated docs. +This is the API documentation for the Blessed terminal library. -However, if you're looking for seldom-used keyword args or a peek at the +Because Blessed uses quite a bit of dynamism, you should +`read the readme first`_ for a general guide and overview. + +However, if you're looking for the documentation of the internal +classes, their methods, and related functions that make up the internals, you're in the right place. .. _`read the readme first`: http://pypi.python.org/pypi/blessed -Then Read This If You Want -========================== +API Documentation +================= Internal modules are as follows. -blessed.terminal ----------------- +terminal module (primary) +------------------------- .. automodule:: blessed.terminal :members: :undoc-members: -blessed.formatters ------------------- +formatters module +----------------- .. automodule:: blessed.formatters :members: :undoc-members: -blessed.keyboard ----------------- +keyboard module +--------------- .. automodule:: blessed.keyboard :members: :undoc-members: -blessed.sequences ------------------ +sequences module +---------------- .. automodule:: blessed.sequences :members: From 03ffaa13df9635eefd94bd67a0451715342fbe3a Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 19:16:26 -0700 Subject: [PATCH 133/459] attempt to fix indentation error --- blessed/sequences.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/blessed/sequences.py b/blessed/sequences.py index 1d8544c0..f38b4fd8 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -65,23 +65,24 @@ def init_sequence_patterns(term): as attributes: ``_re_will_move`` - any sequence matching this pattern will cause the terminal - cursor to move (such as *term.home*). + any sequence matching this pattern will cause the terminal + cursor to move (such as *term.home*). + ``_re_wont_move`` - any sequence matching this pattern will not cause the cursor - to move (such as *term.bold*). + any sequence matching this pattern will not cause the cursor + to move (such as *term.bold*). + ``_re_cuf`` - regular expression that matches term.cuf(N) (move N characters - forward). + regular expression that matches term.cuf(N) (move N characters forward). + ``_cuf1`` - *term.cuf1* sequence (cursor forward 1 character) as a static - value. + *term.cuf1* sequence (cursor forward 1 character) as a static value. + ``_re_cub`` - regular expression that matches term.cub(N) (move N characters - backward). + regular expression that matches term.cub(N) (move N characters backward). + ``_cub1`` - *term.cuf1* sequence (cursor backward 1 character) as a static - value. + *term.cuf1* sequence (cursor backward 1 character) as a static value. These attributes make it possible to perform introspection on strings containing sequences generated by this terminal, to determine the From 23014c41583461e3a4bed640e2ddbb07385af084 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 19:22:02 -0700 Subject: [PATCH 134/459] polish center/rjust/ljust docstrings --- blessed/sequences.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/blessed/sequences.py b/blessed/sequences.py index f38b4fd8..eaddf407 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -351,21 +351,34 @@ def __new__(cls, sequence_text, term): return new def ljust(self, width, fillchar=u' '): - """S.ljust(width, fillchar=u'') -> unicode""" - return self + fillchar * (max(0, width - self.length())) + """S.ljust(width, fillchar=u'') -> unicode + + Returns string derived from unicode string ``S``, left-adjusted + by trailing whitespace padding ``fillchar`.""" + rightside = fillchar * ((max(0, width - self.length())) + / len(fillchar)) + return u''.join((self, rightside)) def rjust(self, width, fillchar=u' '): - """S.rjust(width, fillchar=u'') -> unicode""" - return fillchar * (max(0, width - self.length())) + self + """S.rjust(width, fillchar=u'') -> unicode + + Returns string derived from unicode string ``S``, right-adjusted + by leading whitespace padding ``fillchar``.""" + leftside = fillchar * ((max(0, width - self.length())) + / len(fillchar)) + return u''.join((leftside, self)) def center(self, width, fillchar=u' '): """S.center(width, fillchar=u'') -> unicode - Returns string derived from unicode string ``S`` stripped of whitespace - and terminal sequences.""" + Returns string derived from unicode string ``S``, centered + and surrounded with whitespace padding ``fillchar``.""" split = max(0.0, float(width) - self.length()) / 2 - return (fillchar * (max(0, int(math.floor(split)))) + self - + fillchar * (max(0, int(math.ceil(split))))) + leftside = fillchar * ((max(0, int(math.floor(split)))) + / len(fillchar)) + rightside = fillchar * ((max(0, int(math.ceil(split)))) + / len(fillchar)) + return u''.join((leftside, self, rightside)) def length(self): """S.length() -> int From 47f1079f991f97b32568e90ace4c4211c0762e8a Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 19:22:52 -0700 Subject: [PATCH 135/459] resolve ImportError --- blessed/terminal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blessed/terminal.py b/blessed/terminal.py index 3f7b8dac..909fa4ad 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -39,13 +39,13 @@ class IOUnsupportedOperation(Exception): ) from sequences import ( - get_keyboard_sequences, init_sequence_patterns, SequenceTextWrapper, Sequence, ) from keyboard import ( + get_keyboard_sequences, get_keyboard_codes, resolve_sequence, ) From 35591a38de946ec1772badc8862b6aee819c7349 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 19:27:59 -0700 Subject: [PATCH 136/459] re-introduce correct ParameterizingString kwargs confused myself while documenting it --- blessed/formatters.py | 6 ++++-- blessed/terminal.py | 8 ++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/blessed/formatters.py b/blessed/formatters.py index 6f0ebf78..10151099 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -25,7 +25,7 @@ class ParameterizingString(unicode): >> move(0, 0)('text') """ - def __new__(cls, name, normal): + def __new__(cls, name, attr, normal): """ :arg name: name of terminal capability. :arg attr: terminal attribute sequence to receive arguments. @@ -214,4 +214,6 @@ def resolve_attribute(term, attr): resolution = (resolve_attribute(term, fmt) for fmt in formatters) return FormattingString(u''.join(resolution), term.normal) else: - return ParameterizingString(name=attr, normal=term.normal) + return ParameterizingString(name=attr, + attr=resolve_capability(term, attr), + normal=term.normal) diff --git a/blessed/terminal.py b/blessed/terminal.py index 909fa4ad..f7755b0f 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -352,14 +352,18 @@ def color(self): """ if not self.does_styling: return NullCallableString() - return ParameterizingString(name='color', normal=self.normal) + return ParameterizingString(name='color', + attr=self._foreground_color, + normal=self.normal) @property def on_color(self): "Returns capability that sets the background color." if not self.does_styling: return NullCallableString() - return ParameterizingString(name='on_color', normal=self.normal) + return ParameterizingString(name='on_color', + attr=self._background_color, + normal=self.normal) @property def normal(self): From 478b653f4ab9b5d843c2796773d8a58c7fd02020 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 20:12:22 -0700 Subject: [PATCH 137/459] python3 float fixes --- blessed/sequences.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/blessed/sequences.py b/blessed/sequences.py index eaddf407..ef5c87e0 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -355,8 +355,8 @@ def ljust(self, width, fillchar=u' '): Returns string derived from unicode string ``S``, left-adjusted by trailing whitespace padding ``fillchar`.""" - rightside = fillchar * ((max(0, width - self.length())) - / len(fillchar)) + rightside = fillchar * int((max(0.0, float(width - self.length()))) + / float(len(fillchar))) return u''.join((self, rightside)) def rjust(self, width, fillchar=u' '): @@ -364,8 +364,8 @@ def rjust(self, width, fillchar=u' '): Returns string derived from unicode string ``S``, right-adjusted by leading whitespace padding ``fillchar``.""" - leftside = fillchar * ((max(0, width - self.length())) - / len(fillchar)) + leftside = fillchar * int((max(0, float(width - self.length()))) + / float(len(fillchar))) return u''.join((leftside, self)) def center(self, width, fillchar=u' '): @@ -374,10 +374,10 @@ def center(self, width, fillchar=u' '): Returns string derived from unicode string ``S``, centered and surrounded with whitespace padding ``fillchar``.""" split = max(0.0, float(width) - self.length()) / 2 - leftside = fillchar * ((max(0, int(math.floor(split)))) - / len(fillchar)) - rightside = fillchar * ((max(0, int(math.ceil(split)))) - / len(fillchar)) + leftside = fillchar * int((max(0,0, math.floor(split))) + / float(len(fillchar))) + rightside = fillchar * int((max(0,0, math.ceil(split))) + / float(len(fillchar))) return u''.join((leftside, self, rightside)) def length(self): From 542031dfb42fc84d6296432012e6d9acbc609814 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 20:12:27 -0700 Subject: [PATCH 138/459] add images / screenshots --- README.rst | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.rst b/README.rst index e2b336aa..52a6a5a0 100644 --- a/README.rst +++ b/README.rst @@ -90,6 +90,30 @@ The same program with *Blessed* is simply:: print('This is', term.underline('pretty!')) +Screenshots +=========== + +.. image:: http://jeffquast.com/blessed-weather.png + :target: http://jeffquast.com/blessed-weather.png + :scale: 50 % + :alt: Weather forecast demo (by @jquast) + +.. image:: http://jeffquast.com/blessed-tetris.png + :target: http://jeffquast.com/blessed-tetris.png + :scale: 50 % + :alt: Tetris game demo (by @johannesl) + +.. image:: http://jeffquast.com/blessed-wall.png + :target: http://jeffquast.com/blessed-wall.png + :scale: 50 % + :alt: bbs-scene.org api oneliners demo (art by xzip!impure) + +.. image:: http://jeffquast.com/blessed-quick-logon.png + :target: http://jeffquast.com/blessed-quick-logon.png + :scale: 50 % + :alt: x/84 bbs quick logon screen (art by xzip!impure) + + What It Provides ================ From 60840a2f6c0012bf3b47cdf17ec46339bb9d7eb8 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 20:14:22 -0700 Subject: [PATCH 139/459] doc literal `` fix for ljust() method --- blessed/sequences.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blessed/sequences.py b/blessed/sequences.py index ef5c87e0..7777f2c9 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -351,10 +351,10 @@ def __new__(cls, sequence_text, term): return new def ljust(self, width, fillchar=u' '): - """S.ljust(width, fillchar=u'') -> unicode + """S.ljust(width, fillchar) -> unicode Returns string derived from unicode string ``S``, left-adjusted - by trailing whitespace padding ``fillchar`.""" + by trailing whitespace padding ``fillchar``.""" rightside = fillchar * int((max(0.0, float(width - self.length()))) / float(len(fillchar))) return u''.join((self, rightside)) From 00c47d5c4e45a94d164090fac7ec8edc006eae04 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 20:17:29 -0700 Subject: [PATCH 140/459] docfix ParameterizingString inline code --- blessed/formatters.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/blessed/formatters.py b/blessed/formatters.py index 10151099..21f41f91 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -21,8 +21,9 @@ class ParameterizingString(unicode): For example:: - >> move = ParameterizingString('move') - >> move(0, 0)('text') + >>> c = ParameterizingString('color', term.color, term.normal) + >>> c(9)('color #9') + u'\x1b[91mcolor #9\x1b(B\x1b[m' """ def __new__(cls, name, attr, normal): @@ -67,9 +68,9 @@ class FormattingString(unicode): """A Unicode string which can be called using ``text``, returning a new string, ``attr`` + ``text`` + ``normal``:: - >> style = FormattingString(term.bright_blue, term.normal) - >> style('Big Blue') - '\x1b[94mBig Blue\x1b(B\x1b[m' + >>> style = FormattingString(term.bright_blue, term.normal) + >>> style('Big Blue') + u'\x1b[94mBig Blue\x1b(B\x1b[m' """ def __new__(cls, attr, normal): From 71380dcb94532b9ce784258bf0d1ea3d213ef52e Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 20:18:26 -0700 Subject: [PATCH 141/459] float fixes again --- blessed/sequences.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/blessed/sequences.py b/blessed/sequences.py index 7777f2c9..f69c999b 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -364,7 +364,7 @@ def rjust(self, width, fillchar=u' '): Returns string derived from unicode string ``S``, right-adjusted by leading whitespace padding ``fillchar``.""" - leftside = fillchar * int((max(0, float(width - self.length()))) + leftside = fillchar * int((max(0.0, float(width - self.length()))) / float(len(fillchar))) return u''.join((leftside, self)) @@ -374,9 +374,9 @@ def center(self, width, fillchar=u' '): Returns string derived from unicode string ``S``, centered and surrounded with whitespace padding ``fillchar``.""" split = max(0.0, float(width) - self.length()) / 2 - leftside = fillchar * int((max(0,0, math.floor(split))) + leftside = fillchar * int((max(0.0, math.floor(split))) / float(len(fillchar))) - rightside = fillchar * int((max(0,0, math.ceil(split))) + rightside = fillchar * int((max(0.0, math.ceil(split))) / float(len(fillchar))) return u''.join((leftside, self, rightside)) From b3a97e9b8b7270099cf045e9a337c5d79feb9793 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 20:33:06 -0700 Subject: [PATCH 142/459] remove SIGWINCH anchor for pypi rendering? --- README.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 52a6a5a0..935dac6c 100644 --- a/README.rst +++ b/README.rst @@ -290,7 +290,7 @@ Moving The Cursor When you want to move the cursor, you have a few choices, the ``location(y=None, x=None)`` context manager, ``move(y, x)``, ``move_y(row)``, - and ``move_x(col)`` attributes. +and ``move_x(col)`` attributes. Moving Temporarily @@ -372,6 +372,7 @@ Use the *height* and *width* properties of the *Terminal* class instance:: print('1/3 ways in!') These are always current, so they may be used with a callback from SIGWINCH_ signals.:: + import signal from blessed import Terminal @@ -769,4 +770,4 @@ Version History .. _colorama: http://pypi.python.org/pypi/colorama/0.2.4 .. _tigetstr: http://www.openbsd.org/cgi-bin/man.cgi?query=tigetstr&sektion=3 .. _tparm: http://www.openbsd.org/cgi-bin/man.cgi?query=tparm&sektion=3 -.. _SIGWINCH: https://en.wikipedia.org/wiki/SIGWINCH#SIGWINCH +.. _SIGWINCH: https://en.wikipedia.org/wiki/SIGWINCH From 0d847185a650de662ebf14268bd8f8bda13dfd08 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 21:35:30 -0700 Subject: [PATCH 143/459] pypy is *not* supported -> *now* supported --- README.rst | 119 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 75 insertions(+), 44 deletions(-) diff --git a/README.rst b/README.rst index 935dac6c..c7a84d3f 100644 --- a/README.rst +++ b/README.rst @@ -1,15 +1,19 @@ .. image:: https://secure.travis-ci.org/jquast/blessed.png :target: https://travis-ci.org/jquast/blessed :alt: travis continous integration + .. image:: http://coveralls.io/repos/jquast/blessed/badge.png :target: http://coveralls.io/r/jquast/blessed :alt: coveralls code coveraage + .. image:: https://pypip.in/v/blessed/badge.png :target: https://pypi.python.org/pypi/blessed/ :alt: Latest Version + .. image:: https://pypip.in/license/blessed/badge.png :target: https://pypi.python.org/pypi/blessed/ :alt: License + .. image:: https://pypip.in/d/blessed/badge.png :target: https://pypi.python.org/pypi/blessed/ :alt: Downloads @@ -81,7 +85,7 @@ print some underlined text at the bottom of the screen:: normal=normal)) print(rc) # Restore cursor position. -The same program with *Blessed* is simply:: +The same program with *Blessed* is simply:: from blessed import Terminal @@ -151,23 +155,51 @@ method, so that capabilities can be displayed in-line for more complex strings:: print('{t.red_on_yellow}Candy corn{t.normal} for everyone!'.format(t=term)) + +Capabilities +------------ + The basic capabilities supported by most terminals are: -* ``bold``: Turn on 'extra bright' mode. -* ``reverse``: Switch fore and background attributes. -* ``blink``: Turn on blinking. -* ``normal``: Reset attributes to default. +``bold`` + Turn on 'extra bright' mode. +``reverse`` + Switch fore and background attributes. +``blink`` + Turn on blinking. +``normal`` + Reset attributes to default. The less commonly supported capabilities: -* ``dim``: Turn on *half-bright* mode. -* ``underline`` and ``no_underline``. -* ``italic`` and ``no_italic``. -* ``shadow`` and ``no_shadow``. -* ``standout`` and ``no_standout``. -* ``subscript`` and ``no_subscript``. -* ``superscript`` and ``no_superscript``. -* ``flash``: Visual bell, which flashes the screen. +``dim`` + Enable half-bright mode. +``underline`` + Enable underline mode. +``no_underline`` + Exit underline mode. +``italic`` + Enable italicized text. +``no_italic`` + Exit italics. +``shadow`` + Enable shadow text mode (rare). +``no_shadow`` + Exit shadow text mode. +``standout`` + Enable standout mode (often, an alias for ``reverse``.). +``no_standout`` + Exit standout mode. +``subscript`` + Enable subscript mode. +``no_subscript`` + Exit subscript mode. +``superscript`` + Enable superscript mode. +``no_superscript`` + Exit superscript mode. +``flash`` + Visual bell, flashes the screen. Note that, while the inverse of *underline* is *no_underline*, the only way to turn off *bold* or *reverse* is *normal*, which also cancels any custom @@ -180,8 +212,8 @@ instance. If it is not a supported capability, or a non-tty is used as an output stream, an empty string is returned. -Color ------ +Colors +------ Color terminals are capable of at least 8 basic colors. @@ -207,7 +239,7 @@ terminals also provide an additional 8 high-intensity versions using print(term.on_bright_blue('Blue skies!')) print(term.bright_red_on_bright_yellow('Pepperoni Pizza!')) - + There is also a numerical interface to colors, which takes an integer from 0-15.:: @@ -371,19 +403,19 @@ Use the *height* and *width* properties of the *Terminal* class instance:: with term.location(x=term.width / 3, y=term.height / 3): print('1/3 ways in!') -These are always current, so they may be used with a callback from SIGWINCH_ signals.:: +These are always current, so they may be used with a callback from SIGWINCH_ signals.:: - import signal - from blessed import Terminal + import signal + from blessed import Terminal - term = Terminal() + term = Terminal() - def on_resize(sig, action): - print('height={t.height}, width={t.width}'.format(t=term)) + def on_resize(sig, action): + print('height={t.height}, width={t.width}'.format(t=term)) - signal.signal(signal.SIGWINCH, on_resize) + signal.signal(signal.SIGWINCH, on_resize) - term.inkey() + term.inkey() Clearing The Screen @@ -484,7 +516,7 @@ Keyboard Input The built-in python *raw_input* function does not return a value until the return key is pressed, and is not suitable for detecting each individual keypress, much less arrow or function keys that emit multibyte sequences. Special `termios(4)`_ -routines are required to enter Non-canonical, known in curses as `cbreak(3)_`. +routines are required to enter Non-canonical, known in curses as `cbreak(3)`_. These functions also receive bytes, which must be incrementally decoded to unicode. Blessed handles all of these special cases with the following simple calls. @@ -566,25 +598,25 @@ representing an application key of your terminal. The *code* property (int) may then be compared with any of the following attributes of the *Terminal* instance, which are equivalent to the same -available in `curs_getch(3)_`, with the following exceptions: +available in `curs_getch(3)`_, with the following exceptions: - * use ``KEY_DELETE`` instead of ``KEY_DC`` (chr(127)) - * use ``KEY_INSERT`` instead of ``KEY_IC`` - * use ``KEY_PGUP`` instead of ``KEY_PPAGE`` - * use ``KEY_PGDOWN`` instead of ``KEY_NPAGE`` - * use ``KEY_ESCAPE`` instead of ``KEY_EXIT`` - * use ``KEY_SUP`` instead of ``KEY_SR`` (shift + up) - * use ``KEY_SDOWN`` instead of ``KEY_SF`` (shift + down) +* use ``KEY_DELETE`` instead of ``KEY_DC`` (chr(127)) +* use ``KEY_INSERT`` instead of ``KEY_IC`` +* use ``KEY_PGUP`` instead of ``KEY_PPAGE`` +* use ``KEY_PGDOWN`` instead of ``KEY_NPAGE`` +* use ``KEY_ESCAPE`` instead of ``KEY_EXIT`` +* use ``KEY_SUP`` instead of ``KEY_SR`` (shift + up) +* use ``KEY_SDOWN`` instead of ``KEY_SF`` (shift + down) Additionally, use any of the following common attributes: - * ``KEY_BACKSPACE`` (chr(8)). - * ``KEY_TAB`` (chr(9)). - * ``KEY_DOWN``, ``KEY_UP``, ``KEY_LEFT``, ``KEY_RIGHT``. - * ``KEY_SLEFT`` (shift + left). - * ``KEY_SRIGHT`` (shift + right). - * ``KEY_HOME``, ``KEY_END``. - * ``KEY_F1`` through ``KEY_F22``. +* ``KEY_BACKSPACE`` (chr(8)). +* ``KEY_TAB`` (chr(9)). +* ``KEY_DOWN``, ``KEY_UP``, ``KEY_LEFT``, ``KEY_RIGHT``. +* ``KEY_SLEFT`` (shift + left). +* ``KEY_SRIGHT`` (shift + right). +* ``KEY_HOME``, ``KEY_END``. +* ``KEY_F1`` through ``KEY_F22``. Shopping List @@ -622,8 +654,6 @@ Bugs Bugs or suggestions? Visit the `issue tracker`_. -.. _`issue tracker`: https://github.com/jquast/blessed/issues/ - For patches, please construct a test case if possible. To test, install and execute python package command *tox*. @@ -671,7 +701,7 @@ Version History * enhancement: some attributes are now properties, raise exceptions when assigned. - * enhancement: pypy is not a supported python platform implementation. + * enhancement: pypy is now a supported python platform implementation. * enhancement: removed pokemon ``curses.error`` exceptions. * enhancement: converted nose tests to pytest, merged travis and tox. * enhancement: pytest fixtures, paired with a new ``@as_subprocess`` @@ -763,7 +793,7 @@ Version History .. _`jquast/blessed`: https://github.com/jquast/blessed .. _curses: http://docs.python.org/library/curses.html .. _couleur: http://pypi.python.org/pypi/couleur -.. _`cbreak(3)`: www.openbsd.org/cgi-bin/man.cgi?query=cbreak&apropos=0&sektion=3 +.. _`cbreak(3)`: http://www.openbsd.org/cgi-bin/man.cgi?query=cbreak&apropos=0&sektion=3 .. _`curs_getch(3)`: http://www.openbsd.org/cgi-bin/man.cgi?query=curs_getch&apropos=0&sektion=3 .. _`termios(4)`: http://www.openbsd.org/cgi-bin/man.cgi?query=termios&apropos=0&sektion=4 .. _`terminfo(5)`: http://www.openbsd.org/cgi-bin/man.cgi?query=terminfo&apropos=0&sektion=5 @@ -771,3 +801,4 @@ Version History .. _tigetstr: http://www.openbsd.org/cgi-bin/man.cgi?query=tigetstr&sektion=3 .. _tparm: http://www.openbsd.org/cgi-bin/man.cgi?query=tparm&sektion=3 .. _SIGWINCH: https://en.wikipedia.org/wiki/SIGWINCH +.. _`issue tracker`: https://github.com/jquast/blessed/issues/ From a0af3f3c588b86763f97f36bf23b8b7b415985f8 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 23 Mar 2014 21:39:25 -0700 Subject: [PATCH 144/459] link to API documentation --- README.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index c7a84d3f..876055b7 100644 --- a/README.rst +++ b/README.rst @@ -649,14 +649,16 @@ Blessed does not provide... when used in concert with colorama_. Patches welcome! -Bugs -==== +Devlopers, Bugs +=============== Bugs or suggestions? Visit the `issue tracker`_. For patches, please construct a test case if possible. To test, install and execute python package command *tox*. +For the keenly interested, `API` Documentation is available. + License ======= @@ -802,3 +804,4 @@ Version History .. _tparm: http://www.openbsd.org/cgi-bin/man.cgi?query=tparm&sektion=3 .. _SIGWINCH: https://en.wikipedia.org/wiki/SIGWINCH .. _`issue tracker`: https://github.com/jquast/blessed/issues/ +.. _API: http://blessed.rtfd.org From dd58baa65a81ee24156198f52fd4c4818a4e8649 Mon Sep 17 00:00:00 2001 From: jquast Date: Mon, 24 Mar 2014 23:18:51 -0700 Subject: [PATCH 145/459] closes issue #9, refactor _decode_next as getch() export keyboard-read function as public method getch(), so that it may be overridden by custom terminal implementors ( x/84 telnet bbs, https://github.com/jquast/x84/ ) --- README.rst | 15 +++++++-------- blessed/terminal.py | 28 ++++++++++++++++++---------- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/README.rst b/README.rst index 876055b7..bcdcc038 100644 --- a/README.rst +++ b/README.rst @@ -669,7 +669,9 @@ shares the same. See the LICENSE file. Version History =============== - +1.8 + * enhancement: export keyboard-read function as public method getch(), so + that it may be overridden by custom terminal implementors (x/84 telnet bbs) 1.7 * Forked github project `erikrose/blessings`_ to `jquast/blessed`_, this project was previously known as **blessings** version 1.6 and prior. @@ -684,23 +686,21 @@ Version History * introduced: ``center()``, ``rjust()``, ``ljust()``, ``strip()``, and ``strip_seqs()`` methods. Allows text containing sequences to be aligned to screen, or ``width`` specified. - * introduced: ``wrap()`` method. allows text containing sequences to be + * introduced: ``wrap()`` method. Allows text containing sequences to be word-wrapped without breaking mid-sequence and honoring their printable width. - * bugfix: cannot call ``setupterm()`` more than once per process -- issue a warning about what terminal kind subsequent calls will use. * bugfix: resolved issue where ``number_of_colors`` fails when - ``does_styling`` is ``False``. resolves issue where piping tests + ``does_styling`` is ``False``. Resolves issue where piping tests output would fail. * bugfix: warn and set ``does_styling`` to ``False`` when TERM is unknown. * bugfix: allow unsupported terminal capabilities to be callable just as supported capabilities, so that the return value of ``term.color(n)`` may be called on terminals without color capabilities. * bugfix: for terminals without underline, such as vt220, - ``term.underline('text')``. would be ``u'text' + term.normal``, now is + ``term.underline('text')``. Would be ``u'text' + term.normal``, now is only ``u'text'``. - * enhancement: some attributes are now properties, raise exceptions when assigned. * enhancement: pypy is now a supported python platform implementation. @@ -711,7 +711,6 @@ Version History are used to test a multitude of terminal types. * enhancement: test accessories ``@as_subprocess`` resolves various issues with different terminal types that previously went untested. - * deprecation: python2.5 is no longer supported (as tox does not supported). 1.6 @@ -743,7 +742,7 @@ Version History capabilities. * Endorse the ``location()`` idiom for restoring cursor position after a series of manual movements. - * Fix a bug in which ``location()`` wouldn't do anything when passed zeroes. + * Fix a bug in which ``location()`` wouldn't do anything when passed zeros. * Allow tests to be run with ``python setup.py test``. 1.3 diff --git a/blessed/terminal.py b/blessed/terminal.py index f7755b0f..ea7ad084 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -485,6 +485,19 @@ def wrap(self, text, width=None, **kwargs): return lines + def getch(self): + """T.getch() -> unicode + + Read and decode next byte from keyboard stream. May return u'' + if decoding is not yet complete, or completed unicode character. + Should always return bytes when self.kbhit() returns True. + + Implementors of input streams other than os.read() on the stdin fd + should derive and override this method. + """ + byte = os.read(self.keyboard_fd, 1) + return self._keyboard_decoder.decode(byte, final=False) + def kbhit(self, timeout=0): """T.kbhit([timeout=0]) -> bool @@ -609,19 +622,14 @@ def inkey(self, timeout=None, esc_delay=0.35): def _timeleft(stime, timeout): """_timeleft(stime, timeout) -> float - Returns time-relative time remaining before ``timeout`` after time - elapsed since ``stime``. + Returns time-relative time remaining before ``timeout`` + after time elapsed since ``stime``. """ if timeout is not None: if timeout is 0: return 0 return max(0, timeout - (time.time() - stime)) - def _decode_next(): - """Read and decode next byte from stdin.""" - byte = os.read(self.keyboard_fd, 1) - return self._keyboard_decoder.decode(byte, final=False) - resolve = functools.partial(resolve_sequence, mapper=self._keymap, codes=self._keycodes) @@ -635,7 +643,7 @@ def _decode_next(): # receive all immediately available bytes while self.kbhit(): - ucs += _decode_next() + ucs += self.getch() # decode keystroke, if any ks = resolve(text=ucs) @@ -644,7 +652,7 @@ def _decode_next(): # incomplete, (which may be a multibyte encoding), block until until # one is received. while not ks and self.kbhit(_timeleft(stime, timeout)): - ucs += _decode_next() + ucs += self.getch() ks = resolve(text=ucs) # handle escape key (KEY_ESCAPE) vs. escape sequence (which begins @@ -656,7 +664,7 @@ def _decode_next(): esctime = time.time() while (ks.code is self.KEY_ESCAPE and self.kbhit(_timeleft(esctime, esc_delay))): - ucs += _decode_next() + ucs += self.getch() ks = resolve(text=ucs) # buffer any remaining text received From 00f935c36048b38c7ffe9fefa216fb1d60e8bd56 Mon Sep 17 00:00:00 2001 From: jquast Date: Mon, 24 Mar 2014 23:57:50 -0700 Subject: [PATCH 146/459] closes issue #8, missing locale defaults to ascii if the return value of locale.getpreferredencoding() is empty or results in a string that is not a valid codec (raises LookupError), then set the keyboard _encoding to 'ascii'. --- blessed/terminal.py | 16 ++++++++++++---- blessed/tests/test_core.py | 33 ++++++++++++++++++++++++++++++--- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/blessed/terminal.py b/blessed/terminal.py index ea7ad084..0ae7daf0 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -189,10 +189,18 @@ def __init__(self, kind=None, stream=None, force_styling=False): # build database of sequence <=> KEY_NAME self._keymap = get_keyboard_sequences(self) - self._keyboard_buf = collections.deque() - locale.setlocale(locale.LC_ALL, '') - self._encoding = locale.getpreferredencoding() - self._keyboard_decoder = codecs.getincrementaldecoder(self._encoding)() + if self.keyboard_fd is not None: + self._keyboard_buf = collections.deque() + locale.setlocale(locale.LC_ALL, '') + self._encoding = locale.getpreferredencoding() or 'ascii' + try: + self._keyboard_decoder = codecs.getincrementaldecoder( + self._encoding)() + except LookupError, err: + warnings.warn('%s, fallback to ASCII for keyboard.' % (err,)) + self._encoding = 'ascii' + self._keyboard_decoder = codecs.getincrementaldecoder( + self._encoding)() self.stream = stream diff --git a/blessed/tests/test_core.py b/blessed/tests/test_core.py index 92511414..72988c26 100644 --- a/blessed/tests/test_core.py +++ b/blessed/tests/test_core.py @@ -6,7 +6,9 @@ from io import StringIO import collections +import warnings import platform +import locale import sys import imp import os @@ -152,7 +154,6 @@ def test_setupterm_singleton_issue33(): "A warning is emitted if a new terminal ``kind`` is used per process." @as_subprocess def child(): - import warnings warnings.filterwarnings("error", category=UserWarning) # instantiate first terminal, of type xterm-256color @@ -184,7 +185,6 @@ def test_setupterm_invalid_issue39(): # fail to lookup and emit a warning, only. @as_subprocess def child(): - import warnings warnings.filterwarnings("error", category=UserWarning) try: @@ -208,7 +208,6 @@ def test_setupterm_invalid_has_no_styling(): # fail to lookup and emit a warning, only. @as_subprocess def child(): - import warnings warnings.filterwarnings("ignore", category=UserWarning) term = TestTerminal(kind='unknown', force_styling=True) @@ -351,3 +350,31 @@ def child(kind): assert (t.stream.getvalue() == expected_output) child(all_terms) + + +def test_no_preferredencoding_fallback_ascii(): + "Ensure empty preferredencoding value defaults to ascii." + @as_subprocess + def child(): + with mock.patch('locale.getpreferredencoding') as get_enc: + get_enc.return_value = u'' + t = TestTerminal() + assert t._encoding == 'ascii' + + child() + + +def test_unknown_preferredencoding_warned_and_fallback_ascii(): + "Ensure a locale without a codecs incrementaldecoder emits a warning." + @as_subprocess + def child(): + with mock.patch('locale.getpreferredencoding') as get_enc: + with warnings.catch_warnings(record=True) as warned: + get_enc.return_value = '---unknown--encoding---' + t = TestTerminal() + assert t._encoding == 'ascii' + assert len(warned) == 1 + assert issubclass(warned[-1].category, UserWarning) + assert "fallback to ASCII" in str(warned[-1].message) + + child() From 81f6c9a3c89a3dc3762f8adbf8c10065ca8b9846 Mon Sep 17 00:00:00 2001 From: jquast Date: Tue, 25 Mar 2014 00:05:28 -0700 Subject: [PATCH 147/459] release 1.8 --- README.rst | 7 +++++-- docs/conf.py | 2 +- setup.py | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index bcdcc038..77db8e0e 100644 --- a/README.rst +++ b/README.rst @@ -670,8 +670,11 @@ shares the same. See the LICENSE file. Version History =============== 1.8 - * enhancement: export keyboard-read function as public method getch(), so - that it may be overridden by custom terminal implementors (x/84 telnet bbs) + * enhancement: export keyboard-read function as public method ``getch()``, so + that it may be overridden by custom terminal implementers (x/84 telnet bbs) + * bugfix: if ``locale.getpreferredencoding()`` returns empty string or an + encoding that is not a valid codec for ``codecs.getincrementaldecoder``, + fallback to ascii and emit a warning. 1.7 * Forked github project `erikrose/blessings`_ to `jquast/blessed`_, this project was previously known as **blessings** version 1.6 and prior. diff --git a/docs/conf.py b/docs/conf.py index f79d5cbf..2a772d2e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -53,7 +53,7 @@ # built documents. # # The short X.Y version. -version = '1.7' +version = '1.8' # The full version, including alpha/beta/rc tags. release = version diff --git a/setup.py b/setup.py index 487baefb..4c86ea42 100755 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ here = os.path.dirname(__file__) setup( name='blessed', - version='1.7', + version='1.8', description="A feature-filled fork of Erik Rose's blessings project", long_description=open(os.path.join(here, 'README.rst')).read(), author='Jeff Quast', From 2ecbb8abe56b9af23471f52048c820ae2dbbcc49 Mon Sep 17 00:00:00 2001 From: jquast Date: Tue, 25 Mar 2014 00:10:24 -0700 Subject: [PATCH 148/459] _keyboard_buf should always be available even when custom derived classes override getch(). --- blessed/terminal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blessed/terminal.py b/blessed/terminal.py index 0ae7daf0..ba53a9fd 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -189,8 +189,8 @@ def __init__(self, kind=None, stream=None, force_styling=False): # build database of sequence <=> KEY_NAME self._keymap = get_keyboard_sequences(self) + self._keyboard_buf = collections.deque() if self.keyboard_fd is not None: - self._keyboard_buf = collections.deque() locale.setlocale(locale.LC_ALL, '') self._encoding = locale.getpreferredencoding() or 'ascii' try: From 51932ddc1d9d5c22a1295d872cd337f7b1ffe474 Mon Sep 17 00:00:00 2001 From: jquast Date: Tue, 25 Mar 2014 00:12:59 -0700 Subject: [PATCH 149/459] release 1.8.1 with _keyboard_buf fix --- docs/conf.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 2a772d2e..b8b44e33 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -53,7 +53,7 @@ # built documents. # # The short X.Y version. -version = '1.8' +version = '1.8.1' # The full version, including alpha/beta/rc tags. release = version diff --git a/setup.py b/setup.py index 4c86ea42..6238f103 100755 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ here = os.path.dirname(__file__) setup( name='blessed', - version='1.8', + version='1.8.1', description="A feature-filled fork of Erik Rose's blessings project", long_description=open(os.path.join(here, 'README.rst')).read(), author='Jeff Quast', From b6b0e62d69ef5aa2ac45c5ab33192b4010f62690 Mon Sep 17 00:00:00 2001 From: jquast Date: Wed, 26 Mar 2014 08:44:34 -0700 Subject: [PATCH 150/459] Closes issue #10: inkey() could not be interrupted by a signal handler when timeout is None. A timeout value of None should block indefinitely, while a value of 0 should be non-blocking --- bin/on_resize.py | 4 +- blessed/terminal.py | 10 ++--- blessed/tests/test_keyboard.py | 45 +++++++++++++++++++++ docs/conf.py | 74 +++++++++++++++++----------------- tox.ini | 4 -- 5 files changed, 90 insertions(+), 47 deletions(-) diff --git a/bin/on_resize.py b/bin/on_resize.py index dd955605..9ef4b85b 100755 --- a/bin/on_resize.py +++ b/bin/on_resize.py @@ -10,4 +10,6 @@ def on_resize(sig, action): signal.signal(signal.SIGWINCH, on_resize) -term.inkey(10) +with term.cbreak(): + while True: + print(repr(term.inkey())) diff --git a/blessed/terminal.py b/blessed/terminal.py index ba53a9fd..eb322137 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -506,12 +506,12 @@ def getch(self): byte = os.read(self.keyboard_fd, 1) return self._keyboard_decoder.decode(byte, final=False) - def kbhit(self, timeout=0): + def kbhit(self, timeout=None): """T.kbhit([timeout=0]) -> bool Returns True if a keypress has been detected on keyboard. - When ``timeout`` is 0, this call is non-blocking(default). + When ``timeout`` is None, this call is non-blocking(default). Otherwise blocking until keypress is detected, returning True, or False after ``timeout`` seconds have elapsed. @@ -532,12 +532,12 @@ def kbhit(self, timeout=0): ready_r, ready_w, ready_x = select.select( check_r, check_w, check_x, timeout) except InterruptedError: - if timeout != 0: + if timeout is not None: # subtract time already elapsed, timeout -= time.time() - stime if timeout > 0: continue - ready_r = False + ready_r = [] break else: break @@ -650,7 +650,7 @@ def _timeleft(stime, timeout): ucs += self._keyboard_buf.pop() # receive all immediately available bytes - while self.kbhit(): + while self.kbhit(0): ucs += self.getch() # decode keystroke, if any diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index e2c3ff90..b60339de 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -76,6 +76,51 @@ def on_resize(sig, action): assert math.floor(time.time() - stime) == 1.0 +def test_kbhit_interrupted_nonetype(): + """kbhit() should also allow interruption with timeout of None.""" + pid, master_fd = pty.fork() + if pid is 0: + try: + cov = __import__('cov_core_init').init() + except ImportError: + cov = None + + # child pauses, writes semaphore and begins awaiting input + global got_sigwinch + got_sigwinch = False + + def on_resize(sig, action): + global got_sigwinch + got_sigwinch = True + + term = TestTerminal() + signal.signal(signal.SIGWINCH, on_resize) + read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.raw(): + term.inkey(timeout=None) + os.write(sys.__stdout__.fileno(), b'complete') + assert got_sigwinch is True + if cov is not None: + cov.stop() + cov.save() + os._exit(0) + + with echo_off(master_fd): + os.write(master_fd, SEND_SEMAPHORE) + read_until_semaphore(master_fd) + stime = time.time() + time.sleep(1.0) + os.kill(pid, signal.SIGWINCH) + os.write(master_fd, 'X') + output = read_until_eof(master_fd) + + pid, status = os.waitpid(pid, 0) + assert output == u'complete' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 1.0 + + def test_cbreak_no_kb(): """cbreak() should not call tty.setcbreak() without keyboard""" @as_subprocess diff --git a/docs/conf.py b/docs/conf.py index b8b44e33..f3ea46cc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -26,7 +26,7 @@ # -- General configuration ---------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. @@ -39,7 +39,7 @@ source_suffix = '.rst' # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' @@ -60,13 +60,13 @@ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -74,24 +74,24 @@ # The reST default role (used for this markup: `text`) to use for all # documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # -- Options for HTML output -------------------------------------------------- @@ -103,26 +103,26 @@ # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -131,44 +131,44 @@ # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'blesseddoc' @@ -177,10 +177,10 @@ # -- Options for LaTeX output ------------------------------------------------- # The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' +# latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' +# latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass @@ -192,26 +192,26 @@ # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Additional stuff for the LaTeX preamble. -#latex_preamble = '' +# latex_preamble = '' # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output ------------------------------------------- diff --git a/tox.ini b/tox.ini index 2d1c40cd..848294ca 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,3 @@ -[pytest] -flakes-ignore = - UnusedImport - [tox] envlist = py26, py27, From cc4a4f612685578d499709cca988eb6da95e751d Mon Sep 17 00:00:00 2001 From: jquast Date: Wed, 26 Mar 2014 08:46:20 -0700 Subject: [PATCH 151/459] update documentation for kbhit() --- blessed/terminal.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/blessed/terminal.py b/blessed/terminal.py index eb322137..58ed162e 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -507,13 +507,13 @@ def getch(self): return self._keyboard_decoder.decode(byte, final=False) def kbhit(self, timeout=None): - """T.kbhit([timeout=0]) -> bool + """T.kbhit([timeout=None]) -> bool Returns True if a keypress has been detected on keyboard. - When ``timeout`` is None, this call is non-blocking(default). - Otherwise blocking until keypress is detected, returning - True, or False after ``timeout`` seconds have elapsed. + When ``timeout`` is 0, this call is non-blocking, Otherwise blocking + until keypress is detected (default). When ``timeout`` is a positive + number, returns after ``timeout`` seconds have elapsed. If input is not a terminal, False is always returned. """ From 88347ad579507db45ab56c028d1ed657d6c25822 Mon Sep 17 00:00:00 2001 From: jquast Date: Wed, 26 Mar 2014 08:47:12 -0700 Subject: [PATCH 152/459] bump to 1.8.2 for bugfix issue #10 --- docs/conf.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index f3ea46cc..c61950da 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -53,7 +53,7 @@ # built documents. # # The short X.Y version. -version = '1.8.1' +version = '1.8.2' # The full version, including alpha/beta/rc tags. release = version diff --git a/setup.py b/setup.py index 6238f103..6f0ed0e7 100755 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ here = os.path.dirname(__file__) setup( name='blessed', - version='1.8.1', + version='1.8.2', description="A feature-filled fork of Erik Rose's blessings project", long_description=open(os.path.join(here, 'README.rst')).read(), author='Jeff Quast', From 7164282b5af72422bba5d1d480e97ae4a2d62a1f Mon Sep 17 00:00:00 2001 From: jquast Date: Wed, 26 Mar 2014 08:55:49 -0700 Subject: [PATCH 153/459] tox.ini is honored on travis, but not locally? --- tox.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tox.ini b/tox.ini index 848294ca..89bc0ce3 100644 --- a/tox.ini +++ b/tox.ini @@ -40,3 +40,7 @@ changedir = {toxworkdir} commands = {envbindir}/py.test -v \ -x --strict --pep8 --flakes \ {envsitepackagesdir}/blessed/tests {posargs} + +[pytest] +flakes-ignore = + UnusedImport From 50f6cb5d6ff2b93650e597d9def92b32012f6e4d Mon Sep 17 00:00:00 2001 From: jquast Date: Wed, 26 Mar 2014 10:04:29 -0700 Subject: [PATCH 154/459] comment fix --- blessed/formatters.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/blessed/formatters.py b/blessed/formatters.py index 21f41f91..b242ca85 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -130,11 +130,8 @@ def __call__(self, *args): # # is actually simplified result of NullCallable()(), so # turtles all the way down: we return another instance. - return NullCallableString() - return args[0] # Should we force even strs in Python 2.x to be - # unicodes? No. How would I know what encoding to use - # to convert it? + return args[0] def split_compound(compound): From e4c21cfe1ba4eb69b12f20a43e5fdf38ef0bbe47 Mon Sep 17 00:00:00 2001 From: jquast Date: Wed, 26 Mar 2014 10:20:42 -0700 Subject: [PATCH 155/459] byte-type for test --- blessed/tests/test_keyboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index b60339de..e0d3a8b7 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -112,7 +112,7 @@ def on_resize(sig, action): stime = time.time() time.sleep(1.0) os.kill(pid, signal.SIGWINCH) - os.write(master_fd, 'X') + os.write(master_fd, b'X') output = read_until_eof(master_fd) pid, status = os.waitpid(pid, 0) From 227bc3f579a43eb6a26ec52aadc9b20dee0bf995 Mon Sep 17 00:00:00 2001 From: jquast Date: Tue, 1 Apr 2014 17:57:00 -0700 Subject: [PATCH 156/459] closes issue #12, picklability issues FormattingString and ParameterizingString now use *args for their __new__ methods instead of (keyword=args), so that they are picklable, esp. in regards to Multiprocessing. all pickle protocol versions are tested. --- blessed/formatters.py | 53 +++++++++++++++++--------------- blessed/terminal.py | 20 ++++++------ blessed/tests/test_formatters.py | 52 ++++++++++++++++++++++++++----- 3 files changed, 84 insertions(+), 41 deletions(-) diff --git a/blessed/formatters.py b/blessed/formatters.py index b242ca85..81a2c1d8 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -21,24 +21,27 @@ class ParameterizingString(unicode): For example:: - >>> c = ParameterizingString('color', term.color, term.normal) - >>> c(9)('color #9') + >> term = Terminal() + >> color = ParameterizingString(term.color, term.normal, 'color') + >> color(9)('color #9') u'\x1b[91mcolor #9\x1b(B\x1b[m' """ - def __new__(cls, name, attr, normal): - """ - :arg name: name of terminal capability. - :arg attr: terminal attribute sequence to receive arguments. + def __new__(cls, *args): + """P.__new__(cls, capname, [normal, [name]]) + + :arg capname: parameterized string suitable for curses.tparm() :arg normal: terminating sequence for this capability. + :arg name: name of this terminal capability. """ - new = unicode.__new__(cls, attr) - new._name = name - new._normal = normal + assert len(args) and len(args) < 4, args + new = unicode.__new__(cls, args[0]) + new._normal = len(args) >= 2 and args[1] or u'' + new._name = len(args) == 3 and args[2] or u'' return new def __call__(self, *args): - """P(*args) -> unicode + """P(*args) -> FormattingString() Return evaluated terminal capability (self), receiving arguments ``*args``, followed by the terminating sequence (self.normal) into @@ -49,7 +52,7 @@ def __call__(self, *args): # 3. However, appear to be a plain Unicode string otherwise so # concats work. attr = curses.tparm(self.encode('latin1'), *args).decode('latin1') - return FormattingString(attr=attr, normal=self._normal) + return FormattingString(attr, self._normal) except TypeError, err: # If the first non-int (i.e. incorrect) arg was a string, suggest # something intelligent: @@ -68,22 +71,25 @@ class FormattingString(unicode): """A Unicode string which can be called using ``text``, returning a new string, ``attr`` + ``text`` + ``normal``:: - >>> style = FormattingString(term.bright_blue, term.normal) - >>> style('Big Blue') + >> style = FormattingString(term.bright_blue, term.normal) + >> style('Big Blue') u'\x1b[94mBig Blue\x1b(B\x1b[m' """ - def __new__(cls, attr, normal): - """ - :arg attr: terminal attribute sequence. + def __new__(cls, *args): + """P.__new__(cls, sequence, [normal]) + :arg sequence: terminal attribute sequence. :arg normal: terminating sequence for this attribute. """ - new = unicode.__new__(cls, attr) - new._normal = normal + assert 1 <= len(args) <= 2, args + new = unicode.__new__(cls, args[0]) + new._normal = len(args) > 1 and args[1] or u'' return new def __call__(self, text): - """Return string ``text``, joined by specified video attribute, + """P(text) -> unicode + + Return string ``text``, joined by specified video attribute, (self), and followed by reset attribute sequence (term.normal). """ if len(self): @@ -201,8 +207,8 @@ def resolve_attribute(term, attr): # A direct compoundable, such as `bold' or `on_red'. if attr in COMPOUNDABLES: - return FormattingString(resolve_capability(term, attr), - term.normal) + sequence = resolve_capability(term, attr) + return FormattingString(sequence, term.normal) # Given `bold_on_red', resolve to ('bold', 'on_red'), RECURSIVE # call for each compounding section, joined and returned as @@ -212,6 +218,5 @@ def resolve_attribute(term, attr): resolution = (resolve_attribute(term, fmt) for fmt in formatters) return FormattingString(u''.join(resolution), term.normal) else: - return ParameterizingString(name=attr, - attr=resolve_capability(term, attr), - normal=term.normal) + tparm_capseq = resolve_capability(term, attr) + return ParameterizingString(tparm_capseq, term.normal, attr) diff --git a/blessed/terminal.py b/blessed/terminal.py index 58ed162e..463914d8 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -322,10 +322,12 @@ def location(self, x=None, y=None): @contextlib.contextmanager def fullscreen(self): """Return a context manager that enters fullscreen mode while inside it - and restores normal mode on leaving. Fullscreen mode is characterized - by instructing the terminal emulator to store and save the current - screen state (all screen output), switch to "alternate screen". Upon - exiting, the previous screen state is returned. + and restores normal mode on leaving. + + Fullscreen mode is characterized by instructing the terminal emulator + to store and save the current screen state (all screen output), switch + to "alternate screen". Upon exiting, the previous screen state is + returned. This call may not be tested; only one screen state may be saved at a time. @@ -360,18 +362,16 @@ def color(self): """ if not self.does_styling: return NullCallableString() - return ParameterizingString(name='color', - attr=self._foreground_color, - normal=self.normal) + return ParameterizingString(self._foreground_color, + self.normal, 'color') @property def on_color(self): "Returns capability that sets the background color." if not self.does_styling: return NullCallableString() - return ParameterizingString(name='on_color', - attr=self._background_color, - normal=self.normal) + return ParameterizingString(self._background_color, + self.normal, 'on_color') @property def normal(self): diff --git a/blessed/tests/test_formatters.py b/blessed/tests/test_formatters.py index a158941f..a72adedc 100644 --- a/blessed/tests/test_formatters.py +++ b/blessed/tests/test_formatters.py @@ -17,10 +17,10 @@ def test_parameterizing_string_args(monkeypatch): monkeypatch.setattr(curses, 'tparm', tparm) # given, - pstr = ParameterizingString(name=u'cap', attr=u'seqname', normal=u'norm') + pstr = ParameterizingString(u'seqname', u'norm', u'cap-name') # excersize __new__ - assert pstr._name == u'cap' + assert pstr._name == u'cap-name' assert pstr._normal == u'norm' assert str(pstr) == u'seqname' @@ -47,7 +47,7 @@ def tparm_raises_TypeError(*args): monkeypatch.setattr(curses, 'tparm', tparm_raises_TypeError) # given, - pstr = ParameterizingString(name=u'cap', attr=u'seqname', normal=u'norm') + pstr = ParameterizingString(u'seqname', u'norm', u'cap-name') # ensure TypeError when given a string raises custom exception try: @@ -56,12 +56,12 @@ def tparm_raises_TypeError(*args): except TypeError, err: assert (err.args[0] == ( # py3x "A native or nonexistent capability template, " - "'cap' received invalid argument ('XYZ',): " + "'cap-name' received invalid argument ('XYZ',): " "custom_err. You probably misspelled a " "formatting call like `bright_red'") or err.args[0] == ( "A native or nonexistent capability template, " - "u'cap' received invalid argument ('XYZ',): " + "u'cap-name' received invalid argument ('XYZ',): " "custom_err. You probably misspelled a " "formatting call like `bright_red'")) @@ -78,7 +78,7 @@ def test_formattingstring(monkeypatch): from blessed.formatters import (FormattingString) # given, with arg - pstr = FormattingString(attr=u'attr', normal=u'norm') + pstr = FormattingString(u'attr', u'norm') # excersize __call__, assert pstr._normal == u'norm' @@ -86,7 +86,7 @@ def test_formattingstring(monkeypatch): assert pstr('text') == u'attrtextnorm' # given, without arg - pstr = FormattingString(attr=u'', normal=u'norm') + pstr = FormattingString(u'', u'norm') assert pstr('text') == u'text' @@ -271,3 +271,41 @@ def test_resolve_attribute_recursive_compoundables(monkeypatch): assert type(pstr) == FormattingString assert str(pstr) == 'seq-6808seq-6502' assert pstr('text') == 'seq-6808seq-6502textseq-normal' + + +def test_pickled_parameterizing_string(monkeypatch): + """Test pickle-ability of a formatters.ParameterizingString.""" + from blessed.formatters import ParameterizingString, FormattingString + + # simply send()/recv() over multiprocessing Pipe, a simple + # pickle.loads(dumps(...)) did not reproduce this issue, + from multiprocessing import Pipe + import pickle + + # first argument to tparm() is the sequence name, returned as-is; + # subsequent arguments are usually Integers. + tparm = lambda *args: u'~'.join( + arg.decode('latin1') if not num else '%s' % (arg,) + for num, arg in enumerate(args)).encode('latin1') + + monkeypatch.setattr(curses, 'tparm', tparm) + + # given, + pstr = ParameterizingString(u'seqname', u'norm', u'cap-name') + + # multiprocessing Pipe implicitly pickles. + r, w = Pipe() + + # excersize picklability of ParameterizingString + for proto_num in range(pickle.HIGHEST_PROTOCOL): + assert pstr == pickle.loads(pickle.dumps(pstr, protocol=proto_num)) + w.send(pstr) + r.recv() == pstr + + # excersize picklability of FormattingString + # -- the return value of calling ParameterizingString + zero = pstr(0) + for proto_num in range(pickle.HIGHEST_PROTOCOL): + assert zero == pickle.loads(pickle.dumps(zero, protocol=proto_num)) + w.send(zero) + r.recv() == zero From 2f30bedec298d6e6f08ce4722b5be55d5fe53b8d Mon Sep 17 00:00:00 2001 From: jquast Date: Tue, 1 Apr 2014 17:58:25 -0700 Subject: [PATCH 157/459] ignore RedefinedWhileUnused pyflakes this is an issue in combination of: py33 + pyflakes + py.test --- tox.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.ini b/tox.ini index 89bc0ce3..6ceab11a 100644 --- a/tox.ini +++ b/tox.ini @@ -42,5 +42,7 @@ commands = {envbindir}/py.test -v \ {envsitepackagesdir}/blessed/tests {posargs} [pytest] +# py.test conflicts with pyflakes fixtures flakes-ignore = UnusedImport + RedefinedWhileUnused From 2d6660b8c8ea6e0a506946938fa13ec889a97a43 Mon Sep 17 00:00:00 2001 From: jquast Date: Tue, 1 Apr 2014 17:59:57 -0700 Subject: [PATCH 158/459] bump minor version for release pickle-fix --- README.rst | 1 + docs/conf.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 77db8e0e..2857bdb2 100644 --- a/README.rst +++ b/README.rst @@ -675,6 +675,7 @@ Version History * bugfix: if ``locale.getpreferredencoding()`` returns empty string or an encoding that is not a valid codec for ``codecs.getincrementaldecoder``, fallback to ascii and emit a warning. + * bugfix: ensure FormattingString and ParameterizingString may be pickled. 1.7 * Forked github project `erikrose/blessings`_ to `jquast/blessed`_, this project was previously known as **blessings** version 1.6 and prior. diff --git a/docs/conf.py b/docs/conf.py index c61950da..33125fde 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -53,7 +53,7 @@ # built documents. # # The short X.Y version. -version = '1.8.2' +version = '1.8.3' # The full version, including alpha/beta/rc tags. release = version diff --git a/setup.py b/setup.py index 6f0ed0e7..62e79714 100755 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ here = os.path.dirname(__file__) setup( name='blessed', - version='1.8.2', + version='1.8.3', description="A feature-filled fork of Erik Rose's blessings project", long_description=open(os.path.join(here, 'README.rst')).read(), author='Jeff Quast', From 7ab0f366a8ad1d546bdb5f19ad5837145deee08a Mon Sep 17 00:00:00 2001 From: jquast Date: Tue, 1 Apr 2014 18:10:02 -0700 Subject: [PATCH 159/459] more travis workarounds for ^C in raw() mode --- blessed/tests/test_keyboard.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index e0d3a8b7..a2e1272e 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -319,10 +319,11 @@ def test_inkey_0s_raw_ctrl_c(): pid, status = os.waitpid(pid, 0) if os.environ.get('TRAVIS', None) is not None: # For some reason, setraw has no effect travis-ci, - # is still accepts ^C, when causes system exit on - # py27, but exit 0 on py27 and p33 -- strangely, huh? - assert output == u'', repr(output) - assert os.WEXITSTATUS(status) in (0, 2) + # is still accepts ^C, causing system exit on py26, + # but exit 0 on py27, and either way on py33 + # .. strange, huh? + assert output == u'' and os.WEXITSTATUS(status) == 2 + assert output == u'\x03' and os.WEXITSTATUS(status) == 0 else: assert output == u'\x03', repr(output) assert os.WEXITSTATUS(status) == 0 From 2d4ddef01c870095bc85f25a97bbdbbbd3868846 Mon Sep 17 00:00:00 2001 From: jquast Date: Tue, 1 Apr 2014 18:28:48 -0700 Subject: [PATCH 160/459] still yet, be more forgiving only for travis, somehow, ^C is exiting with an exit status of 0 in travis D: > assert output == u'' and os.WEXITSTATUS(status) == 2 E assert ('' == '' and 0 == 2) E + where 0 = (0) E + where = os.WEXITSTATUS --- blessed/tests/test_keyboard.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index a2e1272e..e9d324d5 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -322,8 +322,8 @@ def test_inkey_0s_raw_ctrl_c(): # is still accepts ^C, causing system exit on py26, # but exit 0 on py27, and either way on py33 # .. strange, huh? - assert output == u'' and os.WEXITSTATUS(status) == 2 - assert output == u'\x03' and os.WEXITSTATUS(status) == 0 + assert output in (u'', u'\x03') + assert os.WEXITSTATUS(status) in (0, 2) else: assert output == u'\x03', repr(output) assert os.WEXITSTATUS(status) == 0 From a756dbe0f1e29bb563dba856e63f001b4f164525 Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 5 Apr 2014 09:46:08 -0700 Subject: [PATCH 161/459] change colors and revert to ascii, issue #11 --- bin/worms.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bin/worms.py b/bin/worms.py index 5a8e4db0..ec31f150 100755 --- a/bin/worms.py +++ b/bin/worms.py @@ -89,8 +89,8 @@ def main(): bearing = Direction(0, -1) nibble = Nibble(location=worm[0], value=0) color_nibble = term.black_on_green - color_worm = term.bright_yellow_on_blue - color_head = term.bright_red_on_blue + color_worm = term.on_yellow + color_head = term.on_red color_bg = term.on_blue echo(term.move(1, 1)) echo(color_bg(term.clear)) @@ -141,12 +141,12 @@ def main(): # display new worm head each turn, regardless. echo(term.move(*head)) - echo(color_head(u'\u263a')) + echo(color_head(u' ')) if worm: # and its old head (now, a body piece) echo(term.move(*worm[-1])) - echo(color_worm(u'\u2689')) + echo(color_worm(u' ')) # wait for keyboard input, which may indicate # a new direction (up/down/left/right) From 1c361cd3df4379d2349c95c68a679b51a3a1da8b Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 5 Apr 2014 09:47:10 -0700 Subject: [PATCH 162/459] do not allow instant death by keystroke one could travel into oneself -- that is, move left when already moving right, instant death. It actually worked ok to turn around with a worm length of only 1 or 2, but otherwise caused instant death. just ignore such changes of bearing/direction if "flipped' --- bin/worms.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/bin/worms.py b/bin/worms.py index ec31f150..67724da4 100755 --- a/bin/worms.py +++ b/bin/worms.py @@ -55,6 +55,9 @@ f_mov(segment).y - segment.y, f_mov(segment).x - segment.x) +# direction-flipped check, reject traveling in opposite direction. +bearing_flipped = lambda dir1, dir2: (0, 0) == (dir1.y + dir2.y, dir1.x + dir2.x) + echo = partial(print, end='', flush=True) # generate a new 'nibble' (number for worm bite) @@ -152,11 +155,17 @@ def main(): # a new direction (up/down/left/right) inp = term.inkey(speed) - # discover new direction, given keyboard input and/or bearing - direction = next_bearing(inp.code, bearing) + # discover new direction, given keyboard input and/or bearing. + nxt_direction = next_bearing(inp.code, bearing) # discover new bearing, given new direction compared to prev - bearing = change_bearing(direction, head) + nxt_bearing = change_bearing(nxt_direction, head) + + # disallow new bearing/direction when flipped (running into + # oneself, fe. traveling left while traveling right) + if not bearing_flipped(bearing, nxt_bearing): + direction = nxt_direction + bearing = nxt_bearing # append the prior `head' onto the worm, then # a new `head' for the given direction. From ae19467f97dbb09ce834da7653ed1f837e2886bf Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 5 Apr 2014 10:42:28 -0700 Subject: [PATCH 163/459] bring clarity to PatameterizingString's __new__ --- blessed/formatters.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/blessed/formatters.py b/blessed/formatters.py index 81a2c1d8..baa133cd 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -28,16 +28,16 @@ class ParameterizingString(unicode): """ def __new__(cls, *args): - """P.__new__(cls, capname, [normal, [name]]) + """P.__new__(cls, cap, [normal, [name]]) - :arg capname: parameterized string suitable for curses.tparm() + :arg cap: parameterized string suitable for curses.tparm() :arg normal: terminating sequence for this capability. :arg name: name of this terminal capability. """ assert len(args) and len(args) < 4, args new = unicode.__new__(cls, args[0]) - new._normal = len(args) >= 2 and args[1] or u'' - new._name = len(args) == 3 and args[2] or u'' + new._normal = len(args) > 1 and args[1] or u'' + new._name = len(args) > 2 and args[2] or u'' return new def __call__(self, *args): From c22d1554a063848a427297ed465e7accfcbda6b8 Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 5 Apr 2014 10:42:48 -0700 Subject: [PATCH 164/459] ParameterizingString default args test & fixes --- blessed/tests/test_formatters.py | 48 ++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/blessed/tests/test_formatters.py b/blessed/tests/test_formatters.py index a72adedc..6ca580fb 100644 --- a/blessed/tests/test_formatters.py +++ b/blessed/tests/test_formatters.py @@ -4,6 +4,38 @@ import mock +def test_parameterizing_string_args_unspecified(monkeypatch): + """Test default args of formatters.ParameterizingString.""" + from blessed.formatters import ParameterizingString, FormattingString + # first argument to tparm() is the sequence name, returned as-is; + # subsequent arguments are usually Integers. + tparm = lambda *args: u'~'.join( + arg.decode('latin1') if not num else '%s' % (arg,) + for num, arg in enumerate(args)).encode('latin1') + + monkeypatch.setattr(curses, 'tparm', tparm) + + # given, + pstr = ParameterizingString(u'') + + # excersize __new__ + assert str(pstr) == u'' + assert pstr._normal == u'' + assert pstr._name == u'' + + # excersize __call__ + zero = pstr(0) + assert type(zero) is FormattingString + assert zero == u'~0' + assert zero('text') == u'~0text' + + # excersize __call__ with multiple args + onetwo = pstr(1, 2) + assert type(onetwo) is FormattingString + assert onetwo == u'~1~2' + assert onetwo('text') == u'~1~2text' + + def test_parameterizing_string_args(monkeypatch): """Test basic formatters.ParameterizingString.""" from blessed.formatters import ParameterizingString, FormattingString @@ -17,24 +49,24 @@ def test_parameterizing_string_args(monkeypatch): monkeypatch.setattr(curses, 'tparm', tparm) # given, - pstr = ParameterizingString(u'seqname', u'norm', u'cap-name') + pstr = ParameterizingString(u'cap', u'norm', u'seq-name') # excersize __new__ - assert pstr._name == u'cap-name' + assert str(pstr) == u'cap' assert pstr._normal == u'norm' - assert str(pstr) == u'seqname' + assert pstr._name == u'seq-name' # excersize __call__ zero = pstr(0) assert type(zero) is FormattingString - assert zero == u'seqname~0' - assert zero('text') == u'seqname~0textnorm' + assert zero == u'cap~0' + assert zero('text') == u'cap~0textnorm' # excersize __call__ with multiple args onetwo = pstr(1, 2) assert type(onetwo) is FormattingString - assert onetwo == u'seqname~1~2' - assert onetwo('text') == u'seqname~1~2textnorm' + assert onetwo == u'cap~1~2' + assert onetwo('text') == u'cap~1~2textnorm' def test_parameterizing_string_type_error(monkeypatch): @@ -47,7 +79,7 @@ def tparm_raises_TypeError(*args): monkeypatch.setattr(curses, 'tparm', tparm_raises_TypeError) # given, - pstr = ParameterizingString(u'seqname', u'norm', u'cap-name') + pstr = ParameterizingString(u'cap', u'norm', u'cap-name') # ensure TypeError when given a string raises custom exception try: From f2baeff06c47a3e8c61663caab690dac87a1e069 Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 5 Apr 2014 10:43:27 -0700 Subject: [PATCH 165/459] tox: test w/o tty, keyboard: fix using isatty() --- blessed/tests/test_keyboard.py | 3 ++- tox.ini | 32 ++++++++++++++++++++++++++------ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index e9d324d5..bb89b75e 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -325,7 +325,8 @@ def test_inkey_0s_raw_ctrl_c(): assert output in (u'', u'\x03') assert os.WEXITSTATUS(status) in (0, 2) else: - assert output == u'\x03', repr(output) + assert (output == u'\x03' or + output == u'' and not os.isatty(0)) assert os.WEXITSTATUS(status) == 0 assert math.floor(time.time() - stime) == 0.0 diff --git a/tox.ini b/tox.ini index 6ceab11a..adbd689e 100644 --- a/tox.ini +++ b/tox.ini @@ -13,10 +13,17 @@ deps = pytest pytest-flakes mock -commands = {envbindir}/py.test -v \ - -x --strict --pep8 --flakes \ - blessed/tests {posargs} +whitelist_externals = /bin/bash +# run each test twice -- 1. w/o tty +commands = /bin/bash -c {envbindir}/py.test -v \ + -x --strict --pep8 --flakes \ + blessed/tests {posargs} \ + < /dev/null 2>&1 | tee /dev/null +# 2. w/tty + {envbindir}/py.test -v \ + -x --strict --pep8 --flakes \ + blessed/tests {posargs} [testenv:py27] # for python27, measure coverage @@ -27,7 +34,14 @@ deps = pytest pytest-flakes mock -commands = {envbindir}/py.test -v \ + +# run each test twice -- 1. w/o tty, +commands = /bin/bash -c {envbindir}/py.test -v \ + -x --strict --pep8 --flakes \ + blessed/tests {posargs} \ + < /dev/null 2>&1 | tee /dev/null +# 2. w/tty, w/coverage + {envbindir}/py.test -v \ -x --strict --pep8 --flakes \ --cov blessed {posargs} coverage report -m @@ -37,12 +51,18 @@ commands = {envbindir}/py.test -v \ # for python3, test the version of blessed that is *installed*, # and not from source. This is because we use the 2to3 tool. changedir = {toxworkdir} -commands = {envbindir}/py.test -v \ +# run each test twice -- 1. w/o tty, +commands = /bin/bash -c {envbindir}/py.test -v \ + -x --strict --pep8 --flakes \ + {envsitepackagesdir}blessed/tests {posargs} \ + < /dev/null 2>&1 | tee /dev/null +# 2. w/tty, w/coverage + {envbindir}/py.test -v \ -x --strict --pep8 --flakes \ {envsitepackagesdir}/blessed/tests {posargs} [pytest] -# py.test conflicts with pyflakes fixtures +# py.test fixtures conflict with pyflakes flakes-ignore = UnusedImport RedefinedWhileUnused From 15d70ce84897fa410cd110867a04e45aef906256 Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 5 Apr 2014 10:51:33 -0700 Subject: [PATCH 166/459] pep8 fix --- bin/worms.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bin/worms.py b/bin/worms.py index 67724da4..f1a7efbc 100755 --- a/bin/worms.py +++ b/bin/worms.py @@ -56,7 +56,9 @@ f_mov(segment).x - segment.x) # direction-flipped check, reject traveling in opposite direction. -bearing_flipped = lambda dir1, dir2: (0, 0) == (dir1.y + dir2.y, dir1.x + dir2.x) +bearing_flipped = lambda dir1, dir2: ( + (0, 0) == (dir1.y + dir2.y, dir1.x + dir2.x) +) echo = partial(print, end='', flush=True) From cddd358d254c609047ee8fecfe191e498d7f9b98 Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 5 Apr 2014 10:54:39 -0700 Subject: [PATCH 167/459] no-tty tests incompatible w/py.test + 2to3 + py33 --- tox.ini | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tox.ini b/tox.ini index adbd689e..e230f69b 100644 --- a/tox.ini +++ b/tox.ini @@ -51,13 +51,13 @@ commands = /bin/bash -c {envbindir}/py.test -v \ # for python3, test the version of blessed that is *installed*, # and not from source. This is because we use the 2to3 tool. changedir = {toxworkdir} -# run each test twice -- 1. w/o tty, -commands = /bin/bash -c {envbindir}/py.test -v \ - -x --strict --pep8 --flakes \ - {envsitepackagesdir}blessed/tests {posargs} \ - < /dev/null 2>&1 | tee /dev/null +# using a tty has problems with python3.3/pytest env +#commands = /bin/bash -c {envbindir}/py.test -v \ +# -x --strict --pep8 --flakes \ +# {envsitepackagesdir}blessed/tests {posargs} \ +# < /dev/null 2>&1 | tee /dev/null # 2. w/tty, w/coverage - {envbindir}/py.test -v \ +commands = {envbindir}/py.test -v \ -x --strict --pep8 --flakes \ {envsitepackagesdir}/blessed/tests {posargs} From 63126fffc3d2469e8864bef2f6f1fcfe31e5ba16 Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 5 Apr 2014 12:06:54 -0700 Subject: [PATCH 168/459] allow various calls without keyboard & styling previously an exception was raised, one of many examples -- File "blessed/terminal.py", line 492, in wrap if line.strip() else ('',)) File "..lib/python3.3/textwrap.py", line 296, in wrap return self._wrap_chunks(chunks) File "blessed/sequences.py", line 315, in _wrap_chunks Sequence(chunks[-1], term).strip() == '' and lines): File "blessed/sequences.py", line 409, in strip return self.strip_seqs().strip() File "blessed/sequences.py", line 436, in strip_seqs nxt = idx + measure_length(self[idx:], self._term) File "blessed/sequences.py", line 468, in measure_length term._re_will_move.match(ucs) or AttributeError: 'NullCallableString' object has no attribute 'match' --- blessed/formatters.py | 5 +- blessed/keyboard.py | 2 +- blessed/sequences.py | 160 ++++++++++++++++++------------- blessed/terminal.py | 18 ++-- blessed/tests/test_formatters.py | 11 ++- blessed/tests/test_keyboard.py | 28 +++++- 6 files changed, 145 insertions(+), 79 deletions(-) diff --git a/blessed/formatters.py b/blessed/formatters.py index baa133cd..97bc7450 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -160,10 +160,13 @@ def split_compound(compound): def resolve_capability(term, attr): """Return a Unicode string for the terminal capability ``attr``, - or an empty string if not found. + or an empty string if not found, or if terminal is without styling + capabilities. """ # Decode sequences as latin1, as they are always 8-bit bytes, so when # b'\xff' is returned, this must be decoded to u'\xff'. + if not term.does_styling: + return u'' val = curses.tigetstr(term._sugar.get(attr, attr)) return u'' if val is None else val.decode('latin1') diff --git a/blessed/keyboard.py b/blessed/keyboard.py index 82ecd6c2..880e4399 100644 --- a/blessed/keyboard.py +++ b/blessed/keyboard.py @@ -129,7 +129,7 @@ def get_keyboard_sequences(term): (curses.tigetstr(cap), val) for (val, cap) in capability_names.iteritems() ) if seq - )) + ) if term.does_styling else ()) sequence_map.update(_alternative_left_right(term)) sequence_map.update(DEFAULT_SEQUENCE_MIXIN) diff --git a/blessed/sequences.py b/blessed/sequences.py index f69c999b..dbc3a37b 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -58,59 +58,13 @@ def _build_any_numeric_capability(term, cap, num=99, nparams=1): return None # no such capability -def init_sequence_patterns(term): - """Given a Terminal instance, ``term``, this function processes - and parses several known terminal capabilities, and builds a - database of regular expressions and attaches them to ``term`` - as attributes: - - ``_re_will_move`` - any sequence matching this pattern will cause the terminal - cursor to move (such as *term.home*). - - ``_re_wont_move`` - any sequence matching this pattern will not cause the cursor - to move (such as *term.bold*). - - ``_re_cuf`` - regular expression that matches term.cuf(N) (move N characters forward). - - ``_cuf1`` - *term.cuf1* sequence (cursor forward 1 character) as a static value. - - ``_re_cub`` - regular expression that matches term.cub(N) (move N characters backward). - - ``_cub1`` - *term.cuf1* sequence (cursor backward 1 character) as a static value. - - These attributes make it possible to perform introspection on strings - containing sequences generated by this terminal, to determine the - printable length of a string. - - For debugging, complimentary lists of these sequence matching pattern - values prior to compilation are attached as attributes ``_will_move``, - ``_wont_move``, ``_cuf``, ``_cub``. +def get_movement_sequence_patterns(term): + """ Build and return set of regexp for capabilities of ``term`` known + to cause movement. """ - if term._kind in _BINTERM_UNSUPPORTED: - warnings.warn(_BINTERM_UNSUPPORTED_MSG) - -# # for some reason, 'screen' does not offer hpa and vpa, -# # although they function perfectly fine ! -# if term._kind == 'screen' and term.hpa == u'': -# def screen_hpa(*args): -# return u'\x1b[{}G'.format(len(args) and args[0] + 1 or 1) -# term.hpa = screen_hpa -# if term._kind == 'screen' and term.vpa == u'': -# def screen_vpa(*args): -# return u'\x1b[{}d'.format(len(args) and args[0] + 1 or 1) -# term.vpa = screen_vpa - bnc = functools.partial(_build_numeric_capability, term) - bna = functools.partial(_build_any_numeric_capability, term) - # Build will_move, a list of terminal capabilities that have - # indeterminate effects on the terminal cursor position. - will_move_seqs = set([ + + return set([ # carriage_return re.escape(term.cr), # column_address: Horizontal position, absolute @@ -144,9 +98,15 @@ def init_sequence_patterns(term): term._cub, ]) - # Build wont_move, a list of terminal capabilities that mainly affect - # video attributes, for use with measure_length(). - wont_move_seqs = list([ + +def get_wontmove_sequence_patterns(term): + """ Build and return set of regexp for capabilities of ``term`` known + not to cause any movement. + """ + bnc = functools.partial(_build_numeric_capability, term) + bna = functools.partial(_build_any_numeric_capability, term) + + return list([ # print_screen: Print contents of screen re.escape(term.mc0), # prtr_off: Turn off printer @@ -258,26 +218,94 @@ def init_sequence_patterns(term): # reset_{1,2,3}string: Reset string ] + map(re.escape, (term.r1, term.r2, term.r3,))) - # store pre-compiled list as '_will_move' and '_wont_move', for debugging - term._will_move = _merge_sequences(will_move_seqs) - term._wont_move = _merge_sequences(wont_move_seqs) + +def init_sequence_patterns(term): + """Given a Terminal instance, ``term``, this function processes + and parses several known terminal capabilities, and builds and + returns a dictionary database of regular expressions, which may + be re-attached to the terminal by attributes of the same key-name: + + ``_re_will_move`` + any sequence matching this pattern will cause the terminal + cursor to move (such as *term.home*). + + ``_re_wont_move`` + any sequence matching this pattern will not cause the cursor + to move (such as *term.bold*). + + ``_re_cuf`` + regular expression that matches term.cuf(N) (move N characters forward), + or None if temrinal is without cuf sequence. + + ``_cuf1`` + *term.cuf1* sequence (cursor forward 1 character) as a static value. + + ``_re_cub`` + regular expression that matches term.cub(N) (move N characters backward), + or None if terminal is without cub sequence. + + ``_cub1`` + *term.cuf1* sequence (cursor backward 1 character) as a static value. + + These attributes make it possible to perform introspection on strings + containing sequences generated by this terminal, to determine the + printable length of a string. + """ + if term._kind in _BINTERM_UNSUPPORTED: + warnings.warn(_BINTERM_UNSUPPORTED_MSG) + + # Build will_move, a list of terminal capabilities that have + # indeterminate effects on the terminal cursor position. + _will_move = _merge_sequences(get_movement_sequence_patterns(term) + ) if term.does_styling else set() + + # Build wont_move, a list of terminal capabilities that mainly affect + # video attributes, for use with measure_length(). + _wont_move = _merge_sequences(get_wontmove_sequence_patterns(term) + ) if term.does_styling else set() # compile as regular expressions, OR'd. - term._re_will_move = re.compile('(%s)' % ('|'.join(term._will_move))) - term._re_wont_move = re.compile('(%s)' % ('|'.join(term._wont_move))) + _re_will_move = re.compile('(%s)' % ('|'.join(_will_move))) + _re_wont_move = re.compile('(%s)' % ('|'.join(_wont_move))) - # static pattern matching for _horiontal_distance # + # static pattern matching for horizontal_distance(ucs, term) + # + bnc = functools.partial(_build_numeric_capability, term) + # parm_right_cursor: Move #1 characters to the right - term._cuf = bnc(cap='cuf', optional=True) - term._re_cuf = re.compile(term._cuf) if term._cuf else None + _cuf = bnc(cap='cuf', optional=True) + _re_cuf = re.compile(_cuf) if _cuf else None + # cursor_right: Non-destructive space (move right one space) - term._cuf1 = term.cuf1 + _cuf1 = term.cuf1 + # parm_left_cursor: Move #1 characters to the left - term._cub = bnc(cap='cub', optional=True) - term._re_cub = re.compile(term._cub) if term._cub else None + _cub = bnc(cap='cub', optional=True) + _re_cub = re.compile(_cub) if _cub else None + # cursor_left: Move left one space - term._cub1 = term.cub1 + _cub1 = term.cub1 + + return {'_re_will_move': _re_will_move, + '_re_wont_move': _re_wont_move, + '_re_cuf': _re_cuf, + '_re_cub': _re_cub, + '_cuf1': _cuf1, + '_cub1': _cub1, } + +# TODO(jquast): for some reason, 'screen' does not offer hpa and vpa, +# although they function perfectly fine ! We need some kind +# of patching function we may apply for such terminals .. +# +# if term._kind == 'screen' and term.hpa == u'': +# def screen_hpa(*args): +# return u'\x1b[{}G'.format(len(args) and args[0] + 1 or 1) +# term.hpa = screen_hpa +# if term._kind == 'screen' and term.vpa == u'': +# def screen_vpa(*args): +# return u'\x1b[{}d'.format(len(args) and args[0] + 1 or 1) +# term.vpa = screen_vpa class SequenceTextWrapper(textwrap.TextWrapper): diff --git a/blessed/terminal.py b/blessed/terminal.py index 463914d8..6f99dd53 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -176,18 +176,18 @@ def __init__(self, kind=None, stream=None, force_styling=False): ' returned for the remainder of this process.' % ( self._kind, _CUR_TERM,)) - if self.does_styling: - init_sequence_patterns(self) + for re_name, re_val in init_sequence_patterns(self).items(): + setattr(self, re_name, re_val) - # build database of int code <=> KEY_NAME - self._keycodes = get_keyboard_codes() + # build database of int code <=> KEY_NAME + self._keycodes = get_keyboard_codes() - # store attributes as: self.KEY_NAME = code - for key_code, key_name in self._keycodes.items(): - setattr(self, key_name, key_code) + # store attributes as: self.KEY_NAME = code + for key_code, key_name in self._keycodes.items(): + setattr(self, key_name, key_code) - # build database of sequence <=> KEY_NAME - self._keymap = get_keyboard_sequences(self) + # build database of sequence <=> KEY_NAME + self._keymap = get_keyboard_sequences(self) self._keyboard_buf = collections.deque() if self.keyboard_fd is not None: diff --git a/blessed/tests/test_formatters.py b/blessed/tests/test_formatters.py index 6ca580fb..15a18b95 100644 --- a/blessed/tests/test_formatters.py +++ b/blessed/tests/test_formatters.py @@ -164,13 +164,22 @@ def test_resolve_capability(monkeypatch): assert resolve_capability(term, 'mnemonic') == u'seq-xyz' assert resolve_capability(term, 'natural') == u'seq-natural' - # given, always returns None + # given, where tigetstr returns None tigetstr_none = lambda attr: None monkeypatch.setattr(curses, 'tigetstr', tigetstr_none) # excersize, assert resolve_capability(term, 'natural') == u'' + # given, where does_styling is False + def raises_exception(*args): + assert False, "Should not be called" + term.does_styling = False + monkeypatch.setattr(curses, 'tigetstr', raises_exception) + + # excersize, + assert resolve_capability(term, 'natural') == u'' + def test_resolve_color(monkeypatch): """Test formatters.resolve_color.""" diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index bb89b75e..1a8205e3 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -170,6 +170,19 @@ def child(): child() +def test_inkey_0s_cbreak_noinput_nokb(): + """0-second inkey without input or keyboard.""" + @as_subprocess + def child(): + term = TestTerminal(stream=StringIO.StringIO()) + with term.cbreak(): + stime = time.time() + inp = term.inkey(timeout=0) + assert (inp == u'') + assert (math.floor(time.time() - stime) == 0.0) + child() + + def test_inkey_1s_cbreak_noinput(): """1-second inkey without input; '' should be returned after ~1 second.""" @as_subprocess @@ -183,6 +196,19 @@ def child(): child() +def test_inkey_1s_cbreak_noinput_nokb(): + """1-second inkey without input or keyboard.""" + @as_subprocess + def child(): + term = TestTerminal(stream=StringIO.StringIO()) + with term.cbreak(): + stime = time.time() + inp = term.inkey(timeout=1) + assert (inp == u'') + assert (math.floor(time.time() - stime) == 1.0) + child() + + def test_inkey_0s_cbreak_input(): """0-second inkey with input; Keypress should be immediately returned.""" pid, master_fd = pty.fork() @@ -498,7 +524,7 @@ def test_esc_delay_cbreak_timout_0(): assert 35 <= int(duration_ms) <= 45, int(duration_ms) -def test_no_keystroke(): +def test_keystroke_default_args(): """Test keyboard.Keystroke constructor with default arguments.""" from blessed.keyboard import Keystroke ks = Keystroke() From 38b323d107c6782786b93bd344ef0092cf0671a4 Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 5 Apr 2014 12:46:26 -0700 Subject: [PATCH 169/459] closes issue #11, inkey() may be interrupted --- README.rst | 6 +- bin/on_resize.py | 2 +- blessed/terminal.py | 16 +++- blessed/tests/test_keyboard.py | 152 +++++++++++++++++++++++++-------- docs/conf.py | 2 +- setup.py | 2 +- 6 files changed, 138 insertions(+), 42 deletions(-) diff --git a/README.rst b/README.rst index 2857bdb2..2c878f09 100644 --- a/README.rst +++ b/README.rst @@ -671,11 +671,15 @@ Version History =============== 1.8 * enhancement: export keyboard-read function as public method ``getch()``, so - that it may be overridden by custom terminal implementers (x/84 telnet bbs) + that it may be overridden by custom terminal implementers. + * enhancement: allow ``inkey()`` and ``kbhit()`` to return early when + interrupted by signal by passing argument ``_intr_continue=False``. * bugfix: if ``locale.getpreferredencoding()`` returns empty string or an encoding that is not a valid codec for ``codecs.getincrementaldecoder``, fallback to ascii and emit a warning. * bugfix: ensure FormattingString and ParameterizingString may be pickled. + * bugfix: allow term.inkey() and related to be called without a keyboard. + 1.7 * Forked github project `erikrose/blessings`_ to `jquast/blessed`_, this project was previously known as **blessings** version 1.6 and prior. diff --git a/bin/on_resize.py b/bin/on_resize.py index 9ef4b85b..5d37f521 100755 --- a/bin/on_resize.py +++ b/bin/on_resize.py @@ -12,4 +12,4 @@ def on_resize(sig, action): with term.cbreak(): while True: - print(repr(term.inkey())) + print(repr(term.inkey(_intr_continue=False))) diff --git a/blessed/terminal.py b/blessed/terminal.py index 6f99dd53..c02e1f9e 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -506,7 +506,7 @@ def getch(self): byte = os.read(self.keyboard_fd, 1) return self._keyboard_decoder.decode(byte, final=False) - def kbhit(self, timeout=None): + def kbhit(self, timeout=None, _intr_continue=True): """T.kbhit([timeout=None]) -> bool Returns True if a keypress has been detected on keyboard. @@ -532,6 +532,8 @@ def kbhit(self, timeout=None): ready_r, ready_w, ready_x = select.select( check_r, check_w, check_x, timeout) except InterruptedError: + if not _intr_continue: + return u'' if timeout is not None: # subtract time already elapsed, timeout -= time.time() - stime @@ -600,8 +602,8 @@ def raw(self): else: yield - def inkey(self, timeout=None, esc_delay=0.35): - """T.inkey(timeout=None, esc_delay=0.35) -> Keypress() + def inkey(self, timeout=None, esc_delay=0.35, _intr_continue=True): + """T.inkey(timeout=None, [esc_delay, [_intr_continue]]) -> Keypress() Receive next keystroke from keyboard (stdin), blocking until a keypress is received or ``timeout`` elapsed, if specified. @@ -620,6 +622,12 @@ def inkey(self, timeout=None, esc_delay=0.35): escape, the ``esc_delay`` specifies the amount of time after receiving the escape character (chr(27)) to seek for the completion of other application keys before returning ``KEY_ESCAPE``. + + Normally, when this function is interrupted by a signal, such as the + installment of SIGWINCH, this function will ignore this interruption + and continue to poll for input up to the ``timemout`` specified. If + you'd rather this function return ``u''`` early, specify a value + of ``False`` for ``_intr_continue``. """ # TODO(jquast): "meta sends escape", where alt+1 would send '\x1b1', # what do we do with that? Surely, something useful. @@ -659,7 +667,7 @@ def _timeleft(stime, timeout): # so long as the most immediately received or buffered keystroke is # incomplete, (which may be a multibyte encoding), block until until # one is received. - while not ks and self.kbhit(_timeleft(stime, timeout)): + while not ks and self.kbhit(_timeleft(stime, timeout), _intr_continue): ucs += self.getch() ks = resolve(text=ucs) diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index 1a8205e3..d0db94a2 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Tests for keyboard support.""" +"Tests for keyboard support." import tempfile import StringIO import signal @@ -28,7 +28,7 @@ def test_kbhit_interrupted(): - """kbhit() should not be interrupted with a signal handler.""" + "kbhit() should not be interrupted with a signal handler." pid, master_fd = pty.fork() if pid is 0: try: @@ -49,7 +49,7 @@ def on_resize(sig, action): read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) os.write(sys.__stdout__.fileno(), SEMAPHORE) with term.raw(): - term.inkey(timeout=1.05) + assert term.inkey(timeout=1.05, _intr_continue=False) == u'' os.write(sys.__stdout__.fileno(), b'complete') assert got_sigwinch is True if cov is not None: @@ -62,12 +62,6 @@ def on_resize(sig, action): read_until_semaphore(master_fd) stime = time.time() os.kill(pid, signal.SIGWINCH) - time.sleep(0.5) - os.kill(pid, signal.SIGWINCH) - time.sleep(0.5) - os.kill(pid, signal.SIGWINCH) - time.sleep(0.5) - os.kill(pid, signal.SIGWINCH) output = read_until_eof(master_fd) pid, status = os.waitpid(pid, 0) @@ -77,7 +71,7 @@ def on_resize(sig, action): def test_kbhit_interrupted_nonetype(): - """kbhit() should also allow interruption with timeout of None.""" + "kbhit() should also allow interruption with timeout of None." pid, master_fd = pty.fork() if pid is 0: try: @@ -110,8 +104,9 @@ def on_resize(sig, action): os.write(master_fd, SEND_SEMAPHORE) read_until_semaphore(master_fd) stime = time.time() - time.sleep(1.0) + time.sleep(0.05) os.kill(pid, signal.SIGWINCH) + time.sleep(1.0) os.write(master_fd, b'X') output = read_until_eof(master_fd) @@ -121,8 +116,97 @@ def on_resize(sig, action): assert math.floor(time.time() - stime) == 1.0 +def test_kbhit_interrupted_no_continue(): + "kbhit() may be interrupted when _intr_continue=False." + pid, master_fd = pty.fork() + if pid is 0: + try: + cov = __import__('cov_core_init').init() + except ImportError: + cov = None + + # child pauses, writes semaphore and begins awaiting input + global got_sigwinch + got_sigwinch = False + + def on_resize(sig, action): + global got_sigwinch + got_sigwinch = True + + term = TestTerminal() + signal.signal(signal.SIGWINCH, on_resize) + read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.raw(): + term.inkey(timeout=1.05, _intr_continue=False) + os.write(sys.__stdout__.fileno(), b'complete') + assert got_sigwinch is True + if cov is not None: + cov.stop() + cov.save() + os._exit(0) + + with echo_off(master_fd): + os.write(master_fd, SEND_SEMAPHORE) + read_until_semaphore(master_fd) + stime = time.time() + time.sleep(0.05) + os.kill(pid, signal.SIGWINCH) + output = read_until_eof(master_fd) + + pid, status = os.waitpid(pid, 0) + assert output == u'complete' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 0.0 + + +def test_kbhit_interrupted_nonetype_no_continue(): + "kbhit() may be interrupted when _intr_continue=False with timeout None." + pid, master_fd = pty.fork() + if pid is 0: + try: + cov = __import__('cov_core_init').init() + except ImportError: + cov = None + + # child pauses, writes semaphore and begins awaiting input + global got_sigwinch + got_sigwinch = False + + def on_resize(sig, action): + global got_sigwinch + got_sigwinch = True + + term = TestTerminal() + signal.signal(signal.SIGWINCH, on_resize) + read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.raw(): + term.inkey(timeout=None, _intr_continue=False) + os.write(sys.__stdout__.fileno(), b'complete') + assert got_sigwinch is True + if cov is not None: + cov.stop() + cov.save() + os._exit(0) + + with echo_off(master_fd): + os.write(master_fd, SEND_SEMAPHORE) + read_until_semaphore(master_fd) + stime = time.time() + time.sleep(0.05) + os.kill(pid, signal.SIGWINCH) + os.write(master_fd, b'X') + output = read_until_eof(master_fd) + + pid, status = os.waitpid(pid, 0) + assert output == u'complete' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 0.0 + + def test_cbreak_no_kb(): - """cbreak() should not call tty.setcbreak() without keyboard""" + "cbreak() should not call tty.setcbreak() without keyboard." @as_subprocess def child(): with tempfile.NamedTemporaryFile() as stream: @@ -134,7 +218,7 @@ def child(): def test_raw_no_kb(): - """raw() should not call tty.setraw() without keyboard""" + "raw() should not call tty.setraw() without keyboard." @as_subprocess def child(): with tempfile.NamedTemporaryFile() as stream: @@ -146,7 +230,7 @@ def child(): def test_kbhit_no_kb(): - """kbhit() always immediately returns False without a keyboard.""" + "kbhit() always immediately returns False without a keyboard." @as_subprocess def child(): term = TestTerminal(stream=StringIO.StringIO()) @@ -158,7 +242,7 @@ def child(): def test_inkey_0s_cbreak_noinput(): - """0-second inkey without input; '' should be returned.""" + "0-second inkey without input; '' should be returned." @as_subprocess def child(): term = TestTerminal() @@ -171,7 +255,7 @@ def child(): def test_inkey_0s_cbreak_noinput_nokb(): - """0-second inkey without input or keyboard.""" + "0-second inkey without input or keyboard." @as_subprocess def child(): term = TestTerminal(stream=StringIO.StringIO()) @@ -184,7 +268,7 @@ def child(): def test_inkey_1s_cbreak_noinput(): - """1-second inkey without input; '' should be returned after ~1 second.""" + "1-second inkey without input; '' should be returned after ~1 second." @as_subprocess def child(): term = TestTerminal() @@ -197,7 +281,7 @@ def child(): def test_inkey_1s_cbreak_noinput_nokb(): - """1-second inkey without input or keyboard.""" + "1-second inkey without input or keyboard." @as_subprocess def child(): term = TestTerminal(stream=StringIO.StringIO()) @@ -210,7 +294,7 @@ def child(): def test_inkey_0s_cbreak_input(): - """0-second inkey with input; Keypress should be immediately returned.""" + "0-second inkey with input; Keypress should be immediately returned." pid, master_fd = pty.fork() if pid is 0: try: @@ -243,7 +327,7 @@ def test_inkey_0s_cbreak_input(): def test_inkey_cbreak_input_slowly(): - """0-second inkey with input; Keypress should be immediately returned.""" + "0-second inkey with input; Keypress should be immediately returned." pid, master_fd = pty.fork() if pid is 0: try: @@ -285,7 +369,7 @@ def test_inkey_cbreak_input_slowly(): def test_inkey_0s_cbreak_multibyte_utf8(): - """0-second inkey with multibyte utf-8 input; should decode immediately.""" + "0-second inkey with multibyte utf-8 input; should decode immediately." # utf-8 bytes represent "latin capital letter upsilon". pid, master_fd = pty.fork() if pid is 0: # child @@ -317,7 +401,7 @@ def test_inkey_0s_cbreak_multibyte_utf8(): def test_inkey_0s_raw_ctrl_c(): - """0-second inkey with raw allows receiving ^C.""" + "0-second inkey with raw allows receiving ^C." pid, master_fd = pty.fork() if pid is 0: # child try: @@ -358,7 +442,7 @@ def test_inkey_0s_raw_ctrl_c(): def test_inkey_0s_cbreak_sequence(): - """0-second inkey with multibyte sequence; should decode immediately.""" + "0-second inkey with multibyte sequence; should decode immediately." pid, master_fd = pty.fork() if pid is 0: # child try: @@ -388,7 +472,7 @@ def test_inkey_0s_cbreak_sequence(): def test_inkey_1s_cbreak_input(): - """1-second inkey w/multibyte sequence; should return after ~1 second.""" + "1-second inkey w/multibyte sequence; should return after ~1 second." pid, master_fd = pty.fork() if pid is 0: # child try: @@ -420,7 +504,7 @@ def test_inkey_1s_cbreak_input(): def test_esc_delay_cbreak_035(): - """esc_delay will cause a single ESC (\\x1b) to delay for 0.35.""" + "esc_delay will cause a single ESC (\\x1b) to delay for 0.35." pid, master_fd = pty.fork() if pid is 0: # child try: @@ -455,7 +539,7 @@ def test_esc_delay_cbreak_035(): def test_esc_delay_cbreak_135(): - """esc_delay=1.35 will cause a single ESC (\\x1b) to delay for 1.35.""" + "esc_delay=1.35 will cause a single ESC (\\x1b) to delay for 1.35." pid, master_fd = pty.fork() if pid is 0: # child try: @@ -525,7 +609,7 @@ def test_esc_delay_cbreak_timout_0(): def test_keystroke_default_args(): - """Test keyboard.Keystroke constructor with default arguments.""" + "Test keyboard.Keystroke constructor with default arguments." from blessed.keyboard import Keystroke ks = Keystroke() assert ks._name is None @@ -539,7 +623,7 @@ def test_keystroke_default_args(): def test_a_keystroke(): - """Test keyboard.Keystroke constructor with set arguments.""" + "Test keyboard.Keystroke constructor with set arguments." from blessed.keyboard import Keystroke ks = Keystroke(ucs=u'x', code=1, name=u'the X') assert ks._name is u'the X' @@ -552,7 +636,7 @@ def test_a_keystroke(): def test_get_keyboard_codes(): - """Test all values returned by get_keyboard_codes are from curses.""" + "Test all values returned by get_keyboard_codes are from curses." from blessed.keyboard import ( get_keyboard_codes, CURSES_KEYCODE_OVERRIDE_MIXIN, @@ -567,7 +651,7 @@ def test_get_keyboard_codes(): def test_alternative_left_right(): - """Test _alternative_left_right behavior for space/backspace.""" + "Test _alternative_left_right behavior for space/backspace." from blessed.keyboard import _alternative_left_right term = mock.Mock() term._cuf1 = u'' @@ -584,7 +668,7 @@ def test_alternative_left_right(): def test_cuf1_and_cub1_as_RIGHT_LEFT(all_terms): - """Test that cuf1 and cub1 are assigned KEY_RIGHT and KEY_LEFT.""" + "Test that cuf1 and cub1 are assigned KEY_RIGHT and KEY_LEFT." from blessed.keyboard import get_keyboard_sequences @as_subprocess @@ -606,7 +690,7 @@ def child(kind): def test_get_keyboard_sequences_sort_order(xterms): - """ordereddict ensures sequences are ordered longest-first.""" + "ordereddict ensures sequences are ordered longest-first." @as_subprocess def child(): term = TestTerminal(force_styling=True) @@ -620,7 +704,7 @@ def child(): def test_get_keyboard_sequence(monkeypatch): - """Test keyboard.get_keyboard_sequence. """ + "Test keyboard.get_keyboard_sequence. " import curses.has_key import blessed.keyboard @@ -662,7 +746,7 @@ def test_get_keyboard_sequence(monkeypatch): def test_resolve_sequence(): - """Test resolve_sequence for order-dependent mapping.""" + "Test resolve_sequence for order-dependent mapping." from blessed.keyboard import resolve_sequence, OrderedDict mapper = OrderedDict(((u'SEQ1', 1), (u'SEQ2', 2), diff --git a/docs/conf.py b/docs/conf.py index 33125fde..8b7385c4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -53,7 +53,7 @@ # built documents. # # The short X.Y version. -version = '1.8.3' +version = '1.8.4' # The full version, including alpha/beta/rc tags. release = version diff --git a/setup.py b/setup.py index 62e79714..1ed0deb7 100755 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ here = os.path.dirname(__file__) setup( name='blessed', - version='1.8.3', + version='1.8.4', description="A feature-filled fork of Erik Rose's blessings project", long_description=open(os.path.join(here, 'README.rst')).read(), author='Jeff Quast', From 11605be97f628f11d0d505dc650e08da3fe15966 Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 5 Apr 2014 13:10:49 -0700 Subject: [PATCH 170/459] removes use of _intr_continue in wrong kb test --- blessed/tests/test_keyboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index d0db94a2..428f58b1 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -49,7 +49,7 @@ def on_resize(sig, action): read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) os.write(sys.__stdout__.fileno(), SEMAPHORE) with term.raw(): - assert term.inkey(timeout=1.05, _intr_continue=False) == u'' + assert term.inkey(timeout=1.05) == u'' os.write(sys.__stdout__.fileno(), b'complete') assert got_sigwinch is True if cov is not None: From a4bed15a6c41a3e2facb47c0323b6174c7b2e0d5 Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 5 Apr 2014 13:38:27 -0700 Subject: [PATCH 171/459] typo timemout -> timeout thanks @polyphemus --- blessed/terminal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blessed/terminal.py b/blessed/terminal.py index c02e1f9e..b9207447 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -625,7 +625,7 @@ def inkey(self, timeout=None, esc_delay=0.35, _intr_continue=True): Normally, when this function is interrupted by a signal, such as the installment of SIGWINCH, this function will ignore this interruption - and continue to poll for input up to the ``timemout`` specified. If + and continue to poll for input up to the ``timeout`` specified. If you'd rather this function return ``u''`` early, specify a value of ``False`` for ``_intr_continue``. """ From 2ab924c3b11b197b9fab0897299be442542e8557 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 27 Apr 2014 22:16:16 -0700 Subject: [PATCH 172/459] 1.8.6: Support hpa/vpa for 'screen' by proxy also, some fixes to worm example program, and a new progress_bar that makes heavy use of the move_x sequence. --- README.rst | 16 +-- bin/on_resize.py | 28 ++++- bin/progress_bar.py | 40 +++++++ bin/worms.py | 199 +++++++++++++++++++------------- blessed/formatters.py | 81 ++++++++++++- blessed/terminal.py | 1 + blessed/tests/test_keyboard.py | 4 +- blessed/tests/test_sequences.py | 34 +++++- docs/conf.py | 2 +- setup.py | 2 +- 10 files changed, 303 insertions(+), 104 deletions(-) create mode 100755 bin/progress_bar.py diff --git a/README.rst b/README.rst index 2c878f09..f09b8acf 100644 --- a/README.rst +++ b/README.rst @@ -1,20 +1,20 @@ -.. image:: https://secure.travis-ci.org/jquast/blessed.png +.. image:: http://img.shields.io/travis/jquast/blessed.svg :target: https://travis-ci.org/jquast/blessed - :alt: travis continous integration + :alt: Travis Continous Integration -.. image:: http://coveralls.io/repos/jquast/blessed/badge.png +.. image:: http://img.shields.io/coveralls/jquast/blessed/badge.svg :target: http://coveralls.io/r/jquast/blessed - :alt: coveralls code coveraage + :alt: Coveralls Code Coveraage -.. image:: https://pypip.in/v/blessed/badge.png +.. image:: http://img.shields.io/pypi/v/blessed/badge.svg :target: https://pypi.python.org/pypi/blessed/ :alt: Latest Version -.. image:: https://pypip.in/license/blessed/badge.png +.. image:: https://pypip.in/license/blessed/badge.svg :target: https://pypi.python.org/pypi/blessed/ :alt: License -.. image:: https://pypip.in/d/blessed/badge.png +.. image:: http://img.shields.io/pypi/dm/blessed/badge.svg :target: https://pypi.python.org/pypi/blessed/ :alt: Downloads @@ -674,6 +674,8 @@ Version History that it may be overridden by custom terminal implementers. * enhancement: allow ``inkey()`` and ``kbhit()`` to return early when interrupted by signal by passing argument ``_intr_continue=False``. + * enhancement: allow ``hpa`` and ``vpa`` (move_x, move_y) to work on tmux(1) + or screen(1) by forcibly emulating their support by a proxy. * bugfix: if ``locale.getpreferredencoding()`` returns empty string or an encoding that is not a valid codec for ``codecs.getincrementaldecoder``, fallback to ascii and emit a warning. diff --git a/bin/on_resize.py b/bin/on_resize.py index 5d37f521..7398260c 100755 --- a/bin/on_resize.py +++ b/bin/on_resize.py @@ -1,4 +1,12 @@ #!/usr/bin/env python +""" +This is an example application for the 'blessed' Terminal library for python. + +Window size changes are caught by the 'on_resize' function using a traditional +signal handler. Meanwhile, blocking keyboard input is displayed to stdout. +If a resize event is discovered, an empty string is returned by term.inkey() +when _intr_continue is False, as it is here. +""" import signal from blessed import Terminal @@ -6,10 +14,22 @@ def on_resize(sig, action): - print('height={t.height}, width={t.width}'.format(t=term)) + # Its generally not a good idea to put blocking functions (such as print) + # within a signal handler -- if another SIGWINCH is recieved while this + # function blocks, an error will occur. In most programs, you'll want to + # set some kind of 'dirty' flag, perhaps by a Semaphore or global variable. + print('height={t.height}, width={t.width}\r'.format(t=term)) signal.signal(signal.SIGWINCH, on_resize) -with term.cbreak(): - while True: - print(repr(term.inkey(_intr_continue=False))) +# note that, a terminal driver actually writes '\r\n' when '\n' is found, but +# in raw mode, we are allowed to write directly to the terminal without the +# interference of such driver -- so we must write \r\n ourselves; as python +# will append '\n' to our print statements, we simply end our statements with +# \r. +with term.raw(): + print("press 'X' to stop.\r") + inp = None + while inp != 'X': + inp = term.inkey(_intr_continue=False) + print(repr(inp) + u'\r') diff --git a/bin/progress_bar.py b/bin/progress_bar.py new file mode 100755 index 00000000..4fd4ca09 --- /dev/null +++ b/bin/progress_bar.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python +""" +This is an example application for the 'blessed' Terminal library for python. + +This isn't a real progress bar, just a sample "animated prompt" of sorts +that demonstrates the separate move_x() and move_y() functions, made +mainly to test the `hpa' compatibility for 'screen' terminal type which +fails to provide one, but blessed recognizes that it actually does, and +provides a proxy. +""" +from __future__ import print_function +from blessed import Terminal +import sys + + +def main(): + term = Terminal() + assert term.hpa(1) != u'', ( + 'Terminal does not support hpa (Horizontal position absolute)') + + col, offset = 1, 1 + with term.cbreak(): + inp = None + print("press 'X' to stop.") + sys.stderr.write(term.move(term.height, 0) + u'[') + sys.stderr.write(term.move_x(term.width) + u']' + term.move_x(1)) + while inp != 'X': + if col >= (term.width - 2): + offset = -1 + elif col <= 1: + offset = 1 + sys.stderr.write(term.move_x(col) + u'.' if offset == -1 else '=') + col += offset + sys.stderr.write(term.move_x(col) + u'|\b') + sys.stderr.flush() + inp = term.inkey(0.04) + print() + +if __name__ == '__main__': + main() diff --git a/bin/worms.py b/bin/worms.py index f1a7efbc..6e669313 100755 --- a/bin/worms.py +++ b/bin/worms.py @@ -1,11 +1,29 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python +""" +This is an example application for the 'blessed' Terminal library for python. + +It is also an experiment in functional programming. +""" + from __future__ import division, print_function from collections import namedtuple from random import randrange from functools import partial from blessed import Terminal -term = Terminal() + +# python 2/3 compatibility, provide 'echo' function as an +# alias for "print without newline and flush" +try: + echo = partial(print, end='', flush=True) + echo('begin.') +except TypeError: + # TypeError: 'flush' is an invalid keyword argument for this function + import sys + + def echo(object): + sys.stdout.write(u'{}'.format(object)) + sys.stdout.flush() # a worm is a list of (y, x) segments Locations Location = namedtuple('Point', ('y', 'x',)) @@ -20,60 +38,59 @@ # these functions return a new Location instance, given # the direction indicated by their name. -left_of = lambda s: Location( - y=s.y, x=max(0, s.x - 1)) - -right_of = lambda s: Location( - y=s.y, x=min(term.width - 1, s.x + 1)) - -below = lambda s: Location( - y=min(term.height - 1, s.y + 1), x=s.x) - -above = lambda s: Location( - y=max(0, s.y - 1), x=s.x) - -# returns a function providing the new location for the -# given `bearing' - a (y,x) difference of (src, dst). -move_given = lambda bearing: { - (0, -1): left_of, - (0, 1): right_of, - (-1, 0): above, - (1, 0): below}[(bearing.y, bearing.x)] - -# return function that defines the new bearing for any matching -# keyboard code, otherwise the function for the current bearing. -next_bearing = lambda inp_code, bearing: { +LEFT = (0, -1) +left_of = lambda segment, term: Location( + y=segment.y, + x=max(0, segment.x - 1)) + +RIGHT = (0, 1) +right_of = lambda segment, term: Location( + y=segment.y, + x=min(term.width - 1, segment.x + 1)) + +UP = (-1, 0) +above = lambda segment, term: Location( + y=max(0, segment.y - 1), + x=segment.x) + +DOWN = (1, 0) +below = lambda segment, term: Location( + y=min(term.height - 1, segment.y + 1), + x=segment.x) + +# return a direction function that defines the new bearing for any matching +# keyboard code of inp_code; otherwise, the function for the current bearing. +next_bearing = lambda term, inp_code, bearing: { term.KEY_LEFT: left_of, term.KEY_RIGHT: right_of, - term.KEY_DOWN: below, term.KEY_UP: above, -}.get(inp_code, move_given(bearing)) + term.KEY_DOWN: below, +}.get(inp_code, + # direction function given the current bearing + {LEFT: left_of, + RIGHT: right_of, + UP: above, + DOWN: below}[(bearing.y, bearing.x)]) # return new bearing given the movement f(x). -change_bearing = lambda f_mov, segment: Direction( - f_mov(segment).y - segment.y, - f_mov(segment).x - segment.x) +change_bearing = lambda f_mov, segment, term: Direction( + f_mov(segment, term).y - segment.y, + f_mov(segment, term).x - segment.x) # direction-flipped check, reject traveling in opposite direction. bearing_flipped = lambda dir1, dir2: ( (0, 0) == (dir1.y + dir2.y, dir1.x + dir2.x) ) -echo = partial(print, end='', flush=True) - -# generate a new 'nibble' (number for worm bite) -new_nibble = lambda t, v: Nibble( - # create new random (x, y) location - location=Location(x=randrange(1, t.width - 1), - y=randrange(1, t.height - 1)), - # increase given value by 1 - value=v + 1) - # returns True if `loc' matches any (y, x) coordinates, # within list `segments' -- such as a list composing a worm. hit_any = lambda loc, segments: loc in segments +# same as above, but `locations' is also an array of (y, x) coordinates. +hit_vany = lambda locations, segments: any( + hit_any(loc, segments) for loc in locations) + # returns True if segments are same position (hit detection) hit = lambda src, dst: src.x == dst.x and src.y == dst.y @@ -87,95 +104,119 @@ speed * modifier if hit(head, nibble.location) else speed) +# when displaying worm head, show a different glyph for horizontal/vertical +head_glyph = lambda direction: (u':' if direction in (left_of, right_of) + else u'"') + + +# provide the next nibble -- continuously generate a random new nibble so +# long as the current nibble hits any location of the worm, otherwise +# return a nibble of the same location and value as provided. +def next_nibble(term, nibble, head, worm): + l, v = nibble.location, nibble.value + while hit_vany([head] + worm, nibble_locations(l, v)): + l = Location(x=randrange(1, term.width - 1), + y=randrange(1, term.height - 1)) + v = nibble.value + 1 + return Nibble(l, v) + + +# generate an array of locations for the current nibble's location -- a digit +# such as '123' may be hit at 3 different (y, x) coordinates. +def nibble_locations(nibble_location, nibble_value): + return [Location(x=nibble_location.x + offset, + y=nibble_location.y) + for offset in range(0, 1 + len('{}'.format(nibble_value)) - 1)] + def main(): + term = Terminal() worm = [Location(x=term.width // 2, y=term.height // 2)] worm_length = 2 - bearing = Direction(0, -1) + bearing = Direction(*LEFT) + direction = left_of nibble = Nibble(location=worm[0], value=0) color_nibble = term.black_on_green - color_worm = term.on_yellow - color_head = term.on_red + color_worm = term.yellow_reverse + color_head = term.red_reverse color_bg = term.on_blue echo(term.move(1, 1)) echo(color_bg(term.clear)) + + # speed is actually a measure of time; the shorter, the faster. speed = 0.1 - modifier = 0.95 - direction = next_bearing(None, bearing) + modifier = 0.93 + inp = None with term.hidden_cursor(), term.raw(): - inp = None while inp not in (u'q', u'Q'): # delete the tail of the worm at worm_length if len(worm) > worm_length: echo(term.move(*worm.pop(0))) echo(color_bg(u' ')) +# print(worm_length) + # compute head location head = worm.pop() + + # check for hit against self; hitting a wall results in the (y, x) + # location being clipped, -- and death by hitting self (not wall). if hit_any(head, worm): break - # check for nibble hit (new Nibble returned). - n_nibble = (new_nibble(term, nibble.value) - if hit(head, nibble.location) else nibble) - - # ensure new nibble is regenerated outside of worm - while hit_any(n_nibble, worm): - n_nibble = new_nibble(term, nibble, head, worm) + # get the next nibble, which may be equal to ours unless this + # nibble has been struck by any portion of our worm body. + n_nibble = next_nibble(term, nibble, head, worm) - # new worm_length & speed, if hit. + # get the next worm_length and speed, unless unchanged. worm_length = next_wormlength(nibble, head, worm_length) speed = next_speed(nibble, head, speed, modifier) - # display next nibble if a new one was generated, - # and erase the old one if n_nibble != nibble: - echo(term.move(*n_nibble.location)) - echo(color_nibble('{0}'.format(n_nibble.value))) - # erase '7' from nibble '17', using ' ' for empty space, - # or the worm body parts for a worm chunk - for offset in range(1, 1 + len(str(nibble.value)) - 1): - x = nibble.location.x + offset - y = nibble.location.y - echo(term.move(y, x)) - if hit_any((y, x), worm): - echo(color_worm(u'\u2689')) - else: - echo(color_bg(u' ')) - - # display new worm head each turn, regardless. - echo(term.move(*head)) - echo(color_head(u' ')) - + # erase the old one, careful to redraw the nibble contents + # with a worm color for those portions that overlay. + for (y, x) in nibble_locations(*nibble): + echo(term.move(y, x) + (color_worm if (y, x) == head + else color_bg)(u' ')) + echo(term.normal) + # and draw the new, + echo(term.move(*n_nibble.location) + ( + color_nibble('{}'.format(n_nibble.value)))) + + # display new worm head + echo(term.move(*head) + color_head(head_glyph(direction))) + + # and its old head (now, a body piece) if worm: - # and its old head (now, a body piece) - echo(term.move(*worm[-1])) + echo(term.move(*(worm[-1]))) echo(color_worm(u' ')) + echo(term.move(*head)) # wait for keyboard input, which may indicate # a new direction (up/down/left/right) inp = term.inkey(speed) # discover new direction, given keyboard input and/or bearing. - nxt_direction = next_bearing(inp.code, bearing) + nxt_direction = next_bearing(term, inp.code, bearing) # discover new bearing, given new direction compared to prev - nxt_bearing = change_bearing(nxt_direction, head) + nxt_bearing = change_bearing(nxt_direction, head, term) # disallow new bearing/direction when flipped (running into - # oneself, fe. traveling left while traveling right) + # oneself, fe. travelling left while traveling right) if not bearing_flipped(bearing, nxt_bearing): direction = nxt_direction bearing = nxt_bearing # append the prior `head' onto the worm, then # a new `head' for the given direction. - worm.extend([head, direction(head)]) + worm.extend([head, direction(head, term)]) # re-assign new nibble, nibble = n_nibble + echo(term.normal) score = (worm_length - 1) * 100 echo(u''.join((term.move(term.height - 1, 1), term.normal))) echo(u''.join((u'\r\n', u'score: {}'.format(score), u'\r\n'))) diff --git a/blessed/formatters.py b/blessed/formatters.py index 97bc7450..b33ca215 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -67,6 +67,67 @@ def __call__(self, *args): raise +class ParameterizingProxyString(unicode): + """A Unicode string which can be called to proxy missing termcap entries. + + For example:: + + >>> from blessed import Terminal + >>> term = Terminal('screen') + >>> hpa = ParameterizingString(term.hpa, term.normal, 'hpa') + >>> hpa(9) + u'' + >>> fmt = u'\x1b[{0}G' + >>> fmt_arg = lambda *arg: (arg[0] + 1,) + >>> hpa = ParameterizingProxyString((fmt, fmt_arg), term.normal, 'hpa') + >>> hpa(9) + u'\x1b[10G' + """ + + def __new__(cls, *args): + """P.__new__(cls, (fmt, callable), [normal, [name]]) + + :arg fmt: format string suitable for displaying terminal sequences. + :arg callable: receives __call__ arguments for formatting fmt. + :arg normal: terminating sequence for this capability. + :arg name: name of this terminal capability. + """ + assert len(args) and len(args) < 4, args + assert type(args[0]) is tuple, args[0] + assert callable(args[0][1]), args[0][1] + new = unicode.__new__(cls, args[0][0]) + new._fmt_args = args[0][1] + new._normal = len(args) > 1 and args[1] or u'' + new._name = len(args) > 2 and args[2] or u'' + return new + + def __call__(self, *args): + """P(*args) -> FormattingString() + + Return evaluated terminal capability format, (self), using callable + ``self._fmt_args`` receiving arguments ``*args``, followed by the + terminating sequence (self.normal) into a FormattingString capable + of being called. + """ + return FormattingString(self.format(*self._fmt_args(*args)), + self._normal) + + +def get_proxy_string(term, attr): + """ Returns an instance of ParameterizingProxyString + for (some kinds) of terminals and attributes. + """ + if term._kind == 'screen' and attr in ('hpa', 'vpa'): + if attr == 'hpa': + fmt = u'\x1b[{0}G' + elif attr == 'vpa': + fmt = u'\x1b[{0}d' + fmt_arg = lambda *arg: (arg[0] + 1,) + return ParameterizingProxyString((fmt, fmt_arg), + term.normal, 'hpa') + return None + + class FormattingString(unicode): """A Unicode string which can be called using ``text``, returning a new string, ``attr`` + ``text`` + ``normal``:: @@ -124,18 +185,20 @@ def __call__(self, *args): # tparm can take not only ints but also (at least) strings as its # 2nd...nth argument. But we don't support callable parameterizing # capabilities that take non-ints yet, so we can cheap out here. - # + # TODO(erikrose): Go through enough of the motions in the # capability resolvers to determine which of 2 special-purpose # classes, NullParameterizableString or NullFormattingString, # to return, and retire this one. - # + # As a NullCallableString, even when provided with a parameter, # such as t.color(5), we must also still be callable, fe: + # # >>> t.color(5)('shmoo') # - # is actually simplified result of NullCallable()(), so - # turtles all the way down: we return another instance. + # is actually simplified result of NullCallable()() on terminals + # without color support, so turtles all the way down: we return + # another instance. return NullCallableString() return args[0] @@ -221,5 +284,15 @@ def resolve_attribute(term, attr): resolution = (resolve_attribute(term, fmt) for fmt in formatters) return FormattingString(u''.join(resolution), term.normal) else: + # and, for special terminals, such as 'screen', provide a Proxy + # ParameterizingString for attributes they do not claim to support, but + # actually do! (such as 'hpa' and 'vpa'). + proxy = get_proxy_string(term, term._sugar.get(attr, attr)) + if proxy is not None: + return proxy + # otherwise, this is our end-game: given a sequence such as 'csr' + # (change scrolling region), return a ParameterizingString instance, + # that when called, performs and returns the final string after curses + # capability lookup is performed. tparm_capseq = resolve_capability(term, attr) return ParameterizingString(tparm_capseq, term.normal, attr) diff --git a/blessed/terminal.py b/blessed/terminal.py index b9207447..6ce23cff 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -539,6 +539,7 @@ def kbhit(self, timeout=None, _intr_continue=True): timeout -= time.time() - stime if timeout > 0: continue + # no time remains after handling exception (rare) ready_r = [] break else: diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index 428f58b1..839dac74 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -92,7 +92,7 @@ def on_resize(sig, action): read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) os.write(sys.__stdout__.fileno(), SEMAPHORE) with term.raw(): - term.inkey(timeout=None) + term.inkey(timeout=1) os.write(sys.__stdout__.fileno(), b'complete') assert got_sigwinch is True if cov is not None: @@ -106,8 +106,6 @@ def on_resize(sig, action): stime = time.time() time.sleep(0.05) os.kill(pid, signal.SIGWINCH) - time.sleep(1.0) - os.write(master_fd, b'X') output = read_until_eof(master_fd) pid, status = os.waitpid(pid, 0) diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index ad9f63f1..bd8a7a54 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -183,7 +183,27 @@ def child(kind): unicode_cap('rc'))) assert (t.stream.getvalue() == expected_output) - child(all_standard_terms) + # skip 'screen', hpa is proxied (see later tests) + if all_standard_terms != 'screen': + child(all_standard_terms) + + +def test_vertical_location(all_standard_terms): + """Make sure we can move the cursor horizontally without changing rows.""" + @as_subprocess + def child(kind): + t = TestTerminal(kind=kind, stream=StringIO(), force_styling=True) + with t.location(y=5): + pass + expected_output = u''.join( + (unicode_cap('sc'), + unicode_parm('vpa', 5), + unicode_cap('rc'))) + assert (t.stream.getvalue() == expected_output) + + # skip 'screen', vpa is proxied (see later tests) + if all_standard_terms != 'screen': + child(all_standard_terms) def test_inject_move_x_for_screen(): @@ -191,10 +211,12 @@ def test_inject_move_x_for_screen(): @as_subprocess def child(kind): t = TestTerminal(kind=kind, stream=StringIO(), force_styling=True) - with t.location(x=5): + COL = 5 + with t.location(x=COL): pass expected_output = u''.join( - (unicode_cap('sc'), t.hpa(5), + (unicode_cap('sc'), + u'\x1b[{0}G'.format(COL + 1), unicode_cap('rc'))) assert (t.stream.getvalue() == expected_output) @@ -206,10 +228,12 @@ def test_inject_move_y_for_screen(): @as_subprocess def child(kind): t = TestTerminal(kind=kind, stream=StringIO(), force_styling=True) - with t.location(y=5): + ROW = 5 + with t.location(y=ROW): pass expected_output = u''.join( - (unicode_cap('sc'), t.vpa(5), + (unicode_cap('sc'), + u'\x1b[{0}d'.format(ROW + 1), unicode_cap('rc'))) assert (t.stream.getvalue() == expected_output) diff --git a/docs/conf.py b/docs/conf.py index 8b7385c4..245cee5a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -53,7 +53,7 @@ # built documents. # # The short X.Y version. -version = '1.8.4' +version = '1.8.5' # The full version, including alpha/beta/rc tags. release = version diff --git a/setup.py b/setup.py index 1ed0deb7..2f95b320 100755 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ here = os.path.dirname(__file__) setup( name='blessed', - version='1.8.4', + version='1.8.5', description="A feature-filled fork of Erik Rose's blessings project", long_description=open(os.path.join(here, 'README.rst')).read(), author='Jeff Quast', From 3c510ac454f5cba700a4d33b8fae30c839b8785a Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 27 Apr 2014 22:34:02 -0700 Subject: [PATCH 173/459] version 1.8.5 --- docs/conf.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 8b7385c4..245cee5a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -53,7 +53,7 @@ # built documents. # # The short X.Y version. -version = '1.8.4' +version = '1.8.5' # The full version, including alpha/beta/rc tags. release = version diff --git a/setup.py b/setup.py index 1ed0deb7..2f95b320 100755 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ here = os.path.dirname(__file__) setup( name='blessed', - version='1.8.4', + version='1.8.5', description="A feature-filled fork of Erik Rose's blessings project", long_description=open(os.path.join(here, 'README.rst')).read(), author='Jeff Quast', From 33a2bcc12dd51b7039b0628513c6d0efa9fe77b1 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 4 May 2014 14:31:15 -0700 Subject: [PATCH 174/459] add python34 to travis, add 'test' and 'develop' targets for setup.py --- .travis.yml | 9 +++-- docs/conf.py | 2 +- setup.py | 108 ++++++++++++++++++++++++++++++--------------------- tox.ini | 21 +++++----- 4 files changed, 81 insertions(+), 59 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4a231716..096938bd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,20 +4,21 @@ env: - TOXENV=py26 - TOXENV=py27 - TOXENV=py33 + - TOXENV=py34 - TOXENV=pypy install: - pip install -q tox - # for python versions <26, we must install ordereddict + # for python versions <27, we must install ordereddict # mimicking the same dynamically generated 'requires=' # in setup.py - if [[ $TOXENV == "py25" ]] || [[ $TOXENV == "py26" ]]; then pip install -q ordereddict; fi - # for python version =27, cinstall and overage, coveralls. - # coverage is only measured and published for one version. + # for python version =27, install coverage, coveralls. + # (coverage only measured and published for one version) - if [[ $TOXENV == "py27" ]]; then pip install -q coverage coveralls; fi @@ -27,7 +28,7 @@ script: - tox -e $TOXENV after_success: - - if [[ $TOXENV == "py27" ]]; then + - if [[ "${TOXENV}" == "py27" ]]; then coveralls; fi diff --git a/docs/conf.py b/docs/conf.py index 245cee5a..547e231d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -53,7 +53,7 @@ # built documents. # # The short X.Y version. -version = '1.8.5' +version = '1.8.6' # The full version, including alpha/beta/rc tags. release = version diff --git a/setup.py b/setup.py index 2f95b320..8add74b2 100755 --- a/setup.py +++ b/setup.py @@ -1,48 +1,68 @@ #!/usr/bin/env python -from setuptools import setup import sys import os +import setuptools +import setuptools.command.develop +import setuptools.command.test -extra = {} - -if sys.version_info < (2, 7,): - extra.update({'install_requires': 'ordereddict'}) - -elif sys.version_info >= (3,): - extra.update({'use_2to3': True}) - -here = os.path.dirname(__file__) -setup( - name='blessed', - version='1.8.5', - description="A feature-filled fork of Erik Rose's blessings project", - long_description=open(os.path.join(here, 'README.rst')).read(), - author='Jeff Quast', - author_email='contact@jeffquast.com', - license='MIT', - packages=['blessed', 'blessed.tests'], - url='https://github.com/jquast/blessed', - include_package_data=True, - test_suite='blessed.tests', - classifiers=[ - 'Intended Audience :: Developers', - 'Natural Language :: English', - 'Development Status :: 5 - Production/Stable', - 'Environment :: Console', - 'Environment :: Console :: Curses', - 'License :: OSI Approved :: MIT License', - 'Operating System :: POSIX', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.2', - 'Topic :: Software Development :: Libraries', - 'Topic :: Software Development :: User Interfaces', - 'Topic :: Terminals' - ], - keywords=['terminal', 'sequences', 'tty', 'curses', 'ncurses', - 'formatting', 'style', 'color', 'console', 'keyboard', - 'ansi', 'xterm'], - **extra -) + +class SetupDevelop(setuptools.command.develop.develop): + def run(self): + assert os.getenv('VIRTUAL_ENV'), 'You should be in a virtualenv' + self.spawn(('pip', 'install', '-U', 'tox',)) + setuptools.command.develop.develop.run(self) + + +class SetupTest(setuptools.command.test.test): + def run(self): + self.spawn(('tox',)) + + +def main(): + extra = {} + if sys.version_info < (2, 7,): + extra.update({'install_requires': 'ordereddict'}) + + elif sys.version_info >= (3,): + extra.update({'use_2to3': True}) + + here = os.path.dirname(__file__) + setuptools.setup( + name='blessed', + version='1.8.6', + description="A feature-filled fork of Erik Rose's blessings project", + long_description=open(os.path.join(here, 'README.rst')).read(), + author='Jeff Quast', + author_email='contact@jeffquast.com', + license='MIT', + packages=['blessed', 'blessed.tests'], + url='https://github.com/jquast/blessed', + include_package_data=True, + test_suite='blessed.tests', + classifiers=[ + 'Intended Audience :: Developers', + 'Natural Language :: English', + 'Development Status :: 5 - Production/Stable', + 'Environment :: Console', + 'Environment :: Console :: Curses', + 'License :: OSI Approved :: MIT License', + 'Operating System :: POSIX', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.2', + 'Topic :: Software Development :: Libraries', + 'Topic :: Software Development :: User Interfaces', + 'Topic :: Terminals' + ], + keywords=['terminal', 'sequences', 'tty', 'curses', 'ncurses', + 'formatting', 'style', 'color', 'console', 'keyboard', + 'ansi', 'xterm'], + cmdclass={'develop': SetupDevelop, + 'test': SetupTest}, + **extra + ) + +if __name__ == '__main__': + main() diff --git a/tox.ini b/tox.ini index e230f69b..21fabd6f 100644 --- a/tox.ini +++ b/tox.ini @@ -2,6 +2,7 @@ envlist = py26, py27, py33, + py34, pypy @@ -46,19 +47,19 @@ commands = /bin/bash -c {envbindir}/py.test -v \ --cov blessed {posargs} coverage report -m - -[testenv:py33] # for python3, test the version of blessed that is *installed*, # and not from source. This is because we use the 2to3 tool. +# +# some issue with py.test & python 3 does not allow non-tty testing. + +[testenv:py33] changedir = {toxworkdir} -# using a tty has problems with python3.3/pytest env -#commands = /bin/bash -c {envbindir}/py.test -v \ -# -x --strict --pep8 --flakes \ -# {envsitepackagesdir}blessed/tests {posargs} \ -# < /dev/null 2>&1 | tee /dev/null -# 2. w/tty, w/coverage -commands = {envbindir}/py.test -v \ - -x --strict --pep8 --flakes \ +commands = {envbindir}/py.test -x --strict --pep8 --flakes \ + {envsitepackagesdir}/blessed/tests {posargs} + +[testenv:py34] +changedir = {toxworkdir} +commands = {envbindir}/py.test -x --strict --pep8 --flakes \ {envsitepackagesdir}/blessed/tests {posargs} [pytest] From a790054a3916b1ed1c75390236893b92e59d7cca Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 4 May 2014 14:33:51 -0700 Subject: [PATCH 175/459] term.keyboard_fd = None if stream is not a tty --- README.rst | 10 ++++++++-- blessed/terminal.py | 10 +++++++++- blessed/tests/accessories.py | 2 +- blessed/tests/test_keyboard.py | 16 ++++++++++++++++ 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index f09b8acf..d38486ca 100644 --- a/README.rst +++ b/README.rst @@ -676,11 +676,17 @@ Version History interrupted by signal by passing argument ``_intr_continue=False``. * enhancement: allow ``hpa`` and ``vpa`` (move_x, move_y) to work on tmux(1) or screen(1) by forcibly emulating their support by a proxy. + * enhancement: ``setup.py develop`` ensures virtualenv and installs tox, + and ``setup.py test`` calls tox. Requires pythons defined by tox.ini. * bugfix: if ``locale.getpreferredencoding()`` returns empty string or an encoding that is not a valid codec for ``codecs.getincrementaldecoder``, fallback to ascii and emit a warning. - * bugfix: ensure FormattingString and ParameterizingString may be pickled. - * bugfix: allow term.inkey() and related to be called without a keyboard. + * bugfix: ensure ``FormattingString`` and ``ParameterizingString`` may be + pickled. + * bugfix: allow ``term.inkey()`` and related to be called without a keyboard. + * **change**: ``term.keyboard_fd`` is set ``None`` if ``stream`` or + ``sys.stdout`` is not a tty, making ``term.inkey()``, ``term.cbreak()``, + ``term.raw()``, no-op. 1.7 * Forked github project `erikrose/blessings`_ to `jquast/blessed`_, this diff --git a/blessed/terminal.py b/blessed/terminal.py index 6ce23cff..808fce75 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -130,7 +130,8 @@ def __init__(self, kind=None, stream=None, force_styling=False): global _CUR_TERM self.keyboard_fd = None - # default stream is stdout, keyboard only valid as stdin with stdout. + # default stream is stdout, keyboard only valid as stdin when + # output stream is stdout and output stream is a tty if stream is None or stream == sys.__stdout__: stream = sys.__stdout__ self.keyboard_fd = sys.__stdin__.fileno() @@ -144,6 +145,12 @@ def __init__(self, kind=None, stream=None, force_styling=False): self._is_a_tty = stream_fd is not None and os.isatty(stream_fd) self._does_styling = ((self.is_a_tty or force_styling) and force_styling is not None) + + # keyboard_fd only non-None if both stdin and stdout is a tty. + self.keyboard_fd = (self.keyboard_fd + if self.keyboard_fd is not None and + self.is_a_tty and os.isatty(self.keyboard_fd) + else None) self._normal = None # cache normal attr, preventing recursive lookups # The descriptor to direct terminal initialization sequences to. @@ -503,6 +510,7 @@ def getch(self): Implementors of input streams other than os.read() on the stdin fd should derive and override this method. """ + assert self.keyboard_fd is not None byte = os.read(self.keyboard_fd, 1) return self._keyboard_decoder.decode(byte, final=False) diff --git a/blessed/tests/accessories.py b/blessed/tests/accessories.py index 9cfbe9d3..667d3900 100644 --- a/blessed/tests/accessories.py +++ b/blessed/tests/accessories.py @@ -22,7 +22,7 @@ all_terms_params = ['screen', 'vt220', 'rxvt', 'cons25', 'linux', 'ansi'] binpacked_terminal_params = ['avatar', 'kermit'] many_lines_params = [30, 100] -many_columns_params = [10, 30, 100] +many_columns_params = [10, 100] if os.environ.get('TRAVIS', None) is None: # TRAVIS-CI has a limited type of terminals, the others ... all_terms_params.extend(['avatar', 'kermit', 'dtterm', 'wyse520', diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index 839dac74..b4b5eb7b 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -212,6 +212,21 @@ def child(): with mock.patch("tty.setcbreak") as mock_setcbreak: with term.cbreak(): assert not mock_setcbreak.called + assert term.keyboard_fd is None + child() + + +def test_notty_kb_is_None(): + "keyboard_fd should be None when os.isatty returns False." + # in this scenerio, stream is sys.__stdout__, + # but os.isatty(0) is False, + # such as when piping output to less(1) + @as_subprocess + def child(): + with mock.patch("os.isatty") as mock_isatty: + mock_isatty.return_value = False + term = TestTerminal() + assert term.keyboard_fd is None child() @@ -224,6 +239,7 @@ def child(): with mock.patch("tty.setraw") as mock_setraw: with term.raw(): assert not mock_setraw.called + assert term.keyboard_fd is None child() From 39c1df9393a18e666765816566403ebc5c6fb2d2 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 4 May 2014 14:48:15 -0700 Subject: [PATCH 176/459] remove leftover comment --- bin/worms.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bin/worms.py b/bin/worms.py index 6e669313..49416793 100755 --- a/bin/worms.py +++ b/bin/worms.py @@ -155,7 +155,6 @@ def main(): if len(worm) > worm_length: echo(term.move(*worm.pop(0))) echo(color_bg(u' ')) -# print(worm_length) # compute head location head = worm.pop() From 420962b1a4759e7ce1ea8eee05d2454a8b699b1f Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 4 May 2014 14:48:36 -0700 Subject: [PATCH 177/459] add python 3.3 and 3.4 to classifiers --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index 8add74b2..7f829635 100755 --- a/setup.py +++ b/setup.py @@ -52,6 +52,8 @@ def main(): 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', 'Topic :: Software Development :: Libraries', 'Topic :: Software Development :: User Interfaces', 'Topic :: Terminals' From 45dad2bddb87bcd16d4625f0f701fe126e4c8d54 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 4 May 2014 19:50:20 -0700 Subject: [PATCH 178/459] indentation fix --- README.rst | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.rst b/README.rst index d38486ca..f4a46bd4 100644 --- a/README.rst +++ b/README.rst @@ -1,22 +1,22 @@ .. image:: http://img.shields.io/travis/jquast/blessed.svg - :target: https://travis-ci.org/jquast/blessed - :alt: Travis Continous Integration + :target: https://travis-ci.org/jquast/blessed + :alt: Travis Continous Integration .. image:: http://img.shields.io/coveralls/jquast/blessed/badge.svg - :target: http://coveralls.io/r/jquast/blessed - :alt: Coveralls Code Coveraage + :target: http://coveralls.io/r/jquast/blessed + :alt: Coveralls Code Coveraage .. image:: http://img.shields.io/pypi/v/blessed/badge.svg - :target: https://pypi.python.org/pypi/blessed/ - :alt: Latest Version + :target: https://pypi.python.org/pypi/blessed/ + :alt: Latest Version .. image:: https://pypip.in/license/blessed/badge.svg - :target: https://pypi.python.org/pypi/blessed/ - :alt: License + :target: https://pypi.python.org/pypi/blessed/ + :alt: License .. image:: http://img.shields.io/pypi/dm/blessed/badge.svg - :target: https://pypi.python.org/pypi/blessed/ - :alt: Downloads + :target: https://pypi.python.org/pypi/blessed/ + :alt: Downloads ======= Blessed From 6004abb11191616af4af2cf5c16343123890c9dd Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 4 May 2014 19:52:34 -0700 Subject: [PATCH 179/459] badge svg fix --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index f4a46bd4..9fefbf8b 100644 --- a/README.rst +++ b/README.rst @@ -2,11 +2,11 @@ :target: https://travis-ci.org/jquast/blessed :alt: Travis Continous Integration -.. image:: http://img.shields.io/coveralls/jquast/blessed/badge.svg +.. image:: http://img.shields.io/coveralls/jquast/blessed.svg :target: http://coveralls.io/r/jquast/blessed :alt: Coveralls Code Coveraage -.. image:: http://img.shields.io/pypi/v/blessed/badge.svg +.. image:: http://img.shields.io/pypi/v/blessed.svg :target: https://pypi.python.org/pypi/blessed/ :alt: Latest Version @@ -14,7 +14,7 @@ :target: https://pypi.python.org/pypi/blessed/ :alt: License -.. image:: http://img.shields.io/pypi/dm/blessed/badge.svg +.. image:: http://img.shields.io/pypi/dm/blessed.svg :target: https://pypi.python.org/pypi/blessed/ :alt: Downloads From d8741a499a4ca18577be91270aea9fe90a4b7206 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 4 May 2014 19:55:19 -0700 Subject: [PATCH 180/459] more badge fixes --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 9fefbf8b..289d319b 100644 --- a/README.rst +++ b/README.rst @@ -4,7 +4,7 @@ .. image:: http://img.shields.io/coveralls/jquast/blessed.svg :target: http://coveralls.io/r/jquast/blessed - :alt: Coveralls Code Coveraage + :alt: Coveralls Code Coverage .. image:: http://img.shields.io/pypi/v/blessed.svg :target: https://pypi.python.org/pypi/blessed/ From 73a8067131e85121cd40949ee2f972f1ea274b4c Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 4 May 2014 20:02:15 -0700 Subject: [PATCH 181/459] :target: seems to break pypi badges, remove --- README.rst | 5 ----- 1 file changed, 5 deletions(-) diff --git a/README.rst b/README.rst index 289d319b..f82a827e 100644 --- a/README.rst +++ b/README.rst @@ -1,21 +1,16 @@ .. image:: http://img.shields.io/travis/jquast/blessed.svg - :target: https://travis-ci.org/jquast/blessed :alt: Travis Continous Integration .. image:: http://img.shields.io/coveralls/jquast/blessed.svg - :target: http://coveralls.io/r/jquast/blessed :alt: Coveralls Code Coverage .. image:: http://img.shields.io/pypi/v/blessed.svg - :target: https://pypi.python.org/pypi/blessed/ :alt: Latest Version .. image:: https://pypip.in/license/blessed/badge.svg - :target: https://pypi.python.org/pypi/blessed/ :alt: License .. image:: http://img.shields.io/pypi/dm/blessed.svg - :target: https://pypi.python.org/pypi/blessed/ :alt: Downloads ======= From 9375bffd32a80bb284daffa7ddc8644c964e2ffc Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 4 May 2014 20:03:40 -0700 Subject: [PATCH 182/459] also, non-https seemed to have broken .! --- README.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index f82a827e..b37d1262 100644 --- a/README.rst +++ b/README.rst @@ -1,16 +1,16 @@ -.. image:: http://img.shields.io/travis/jquast/blessed.svg +.. image:: https://img.shields.io/travis/jquast/blessed.svg :alt: Travis Continous Integration -.. image:: http://img.shields.io/coveralls/jquast/blessed.svg +.. image:: https://img.shields.io/coveralls/jquast/blessed.svg :alt: Coveralls Code Coverage -.. image:: http://img.shields.io/pypi/v/blessed.svg +.. image:: https://img.shields.io/pypi/v/blessed.svg :alt: Latest Version .. image:: https://pypip.in/license/blessed/badge.svg :alt: License -.. image:: http://img.shields.io/pypi/dm/blessed.svg +.. image:: https://img.shields.io/pypi/dm/blessed.svg :alt: Downloads ======= From 76d4df538b3377c92d00c9c5f1e2f824285c3874 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 4 May 2014 20:48:58 -0700 Subject: [PATCH 183/459] pin cov-core, regression in subprocess coverage filed issue https://github.com/schlamar/pytest-cov/issues/8 --- tox.ini | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 21fabd6f..96b4664e 100644 --- a/tox.ini +++ b/tox.ini @@ -30,7 +30,10 @@ commands = /bin/bash -c {envbindir}/py.test -v \ # for python27, measure coverage usedevelop = True deps = pytest +# https://github.com/schlamar/pytest-cov/issues/8 + cov-core==1.10 pytest-cov + pytest-xdist pytest-pep8 pytest-flakes mock @@ -44,8 +47,8 @@ commands = /bin/bash -c {envbindir}/py.test -v \ # 2. w/tty, w/coverage {envbindir}/py.test -v \ -x --strict --pep8 --flakes \ + --cov-report term-missing \ --cov blessed {posargs} - coverage report -m # for python3, test the version of blessed that is *installed*, # and not from source. This is because we use the 2to3 tool. From fe77f8d062e901bcf5682f71ee6aa17bc7aebea4 Mon Sep 17 00:00:00 2001 From: jquast Date: Mon, 5 May 2014 00:00:19 -0700 Subject: [PATCH 184/459] subprocess coverage returns with latest cov-core --- tox.ini | 3 --- 1 file changed, 3 deletions(-) diff --git a/tox.ini b/tox.ini index 96b4664e..952ed282 100644 --- a/tox.ini +++ b/tox.ini @@ -30,10 +30,7 @@ commands = /bin/bash -c {envbindir}/py.test -v \ # for python27, measure coverage usedevelop = True deps = pytest -# https://github.com/schlamar/pytest-cov/issues/8 - cov-core==1.10 pytest-cov - pytest-xdist pytest-pep8 pytest-flakes mock From c259ee6a318c8fa12af5a624a6f9865b368733c9 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 11 May 2014 12:20:26 -0700 Subject: [PATCH 185/459] polish readme --- README.rst | 52 +++++++++++++++++++++++++--------------------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/README.rst b/README.rst index b37d1262..d813e6b6 100644 --- a/README.rst +++ b/README.rst @@ -495,15 +495,15 @@ from Tao Te Ching, word-wrapped to 25 columns:: from blessed import Terminal - t = Terminal() + term = Terminal() + + poem = (term.bold_blue('Plan difficult tasks'), + term.blue('through the simplest tasks'), + term.bold_cyan('Achieve large tasks'), + term.cyan('through the smallest tasks')) - poem = u''.join((term.bold_blue('Plan difficult tasks '), - term.bold_black('through the simplest tasks'), - term.bold_cyan('Achieve large tasks '), - term.cyan('through the smallest tasks')) for line in poem: - print('\n'.join(term.wrap(line, width=25, - subsequent_indent=' ' * 4))) + print('\n'.join(term.wrap(line, width=25, subsequent_indent=' ' * 4))) Keyboard Input -------------- @@ -648,11 +648,11 @@ Devlopers, Bugs =============== Bugs or suggestions? Visit the `issue tracker`_. +`API Documentation`_ is available. -For patches, please construct a test case if possible. To test, -install and execute python package command *tox*. +For patches, please construct a test case if possible. -For the keenly interested, `API` Documentation is available. +To test, install and execute python package command ``tox``. License @@ -669,8 +669,8 @@ Version History that it may be overridden by custom terminal implementers. * enhancement: allow ``inkey()`` and ``kbhit()`` to return early when interrupted by signal by passing argument ``_intr_continue=False``. - * enhancement: allow ``hpa`` and ``vpa`` (move_x, move_y) to work on tmux(1) - or screen(1) by forcibly emulating their support by a proxy. + * enhancement: allow ``hpa`` and ``vpa`` (*move_x*, *move_y*) to work on + tmux(1) or screen(1) by forcibly emulating their support by a proxy. * enhancement: ``setup.py develop`` ensures virtualenv and installs tox, and ``setup.py test`` calls tox. Requires pythons defined by tox.ini. * bugfix: if ``locale.getpreferredencoding()`` returns empty string or an @@ -690,16 +690,15 @@ Version History to ``tty.setcbreak()`` and ``tty.setraw()``, allowing input from stdin to be read as each key is pressed. * introduced: ``inkey()`` and ``kbhit()``, which will return 1 or more - characters as a unicode sequence, with attributes ``.code`` and ``.name`` - non-None when a multibyte sequence is received, allowing arrow keys and - such to be detected. Optional value ``timeout`` allows timed polling or - blocking. + characters as a unicode sequence, with attributes ``.code`` and ``.name``, + with value non-``None`` when a multibyte sequence is received, allowing + application keys (such as UP/DOWN) to be detected. Optional value ``timeout`` + allows timed asynchronous polling or blocking. * introduced: ``center()``, ``rjust()``, ``ljust()``, ``strip()``, and ``strip_seqs()`` methods. Allows text containing sequences to be aligned to screen, or ``width`` specified. * introduced: ``wrap()`` method. Allows text containing sequences to be - word-wrapped without breaking mid-sequence and honoring their printable - width. + word-wrapped without breaking mid-sequence, honoring their printable width. * bugfix: cannot call ``setupterm()`` more than once per process -- issue a warning about what terminal kind subsequent calls will use. * bugfix: resolved issue where ``number_of_colors`` fails when @@ -796,22 +795,21 @@ Version History * Let ``location()`` operate on just an x *or* y coordinate. 1.0 - * Extracted Blessings from nose-progressive, my `progress-bar-having, - traceback-shortcutting, rootin', tootin' testrunner`_. It provided the - tootin' functionality. + * Extracted Blessings from `nose-progressive`_. -.. _`progress-bar-having, traceback-shortcutting, rootin', tootin' testrunner`: http://pypi.python.org/pypi/nose-progressive/ +.. _`nose-progressive`: http://pypi.python.org/pypi/nose-progressive/ .. _`erikrose/blessings`: https://github.com/erikrose/blessings .. _`jquast/blessed`: https://github.com/jquast/blessed -.. _curses: http://docs.python.org/library/curses.html -.. _couleur: http://pypi.python.org/pypi/couleur +.. _`issue tracker`: https://github.com/jquast/blessed/issues/ +.. _curses: https://docs.python.org/library/curses.html +.. _couleur: https://pypi.python.org/pypi/couleur +.. _colorama: https://pypi.python.org/pypi/colorama +.. _wcwidth: https://pypi.python.org/pypi/wcwidth .. _`cbreak(3)`: http://www.openbsd.org/cgi-bin/man.cgi?query=cbreak&apropos=0&sektion=3 .. _`curs_getch(3)`: http://www.openbsd.org/cgi-bin/man.cgi?query=curs_getch&apropos=0&sektion=3 .. _`termios(4)`: http://www.openbsd.org/cgi-bin/man.cgi?query=termios&apropos=0&sektion=4 .. _`terminfo(5)`: http://www.openbsd.org/cgi-bin/man.cgi?query=terminfo&apropos=0&sektion=5 -.. _colorama: http://pypi.python.org/pypi/colorama/0.2.4 .. _tigetstr: http://www.openbsd.org/cgi-bin/man.cgi?query=tigetstr&sektion=3 .. _tparm: http://www.openbsd.org/cgi-bin/man.cgi?query=tparm&sektion=3 .. _SIGWINCH: https://en.wikipedia.org/wiki/SIGWINCH -.. _`issue tracker`: https://github.com/jquast/blessed/issues/ -.. _API: http://blessed.rtfd.org +.. _`API Documentation`: http://blessed.rtfd.org From 764eb013b4450507924f59cb2e4747db71a15e99 Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 17 May 2014 20:41:03 -0700 Subject: [PATCH 186/459] add rjust and ljust, cleanup docs and comments --- README.rst | 2 + blessed/sequences.py | 111 +++++++++++++++++--------- blessed/terminal.py | 25 ++++-- blessed/tests/test_length_sequence.py | 70 +++++++++------- blessed/tests/test_sequences.py | 13 +++ 5 files changed, 149 insertions(+), 72 deletions(-) diff --git a/README.rst b/README.rst index d813e6b6..3fde3bc4 100644 --- a/README.rst +++ b/README.rst @@ -673,6 +673,8 @@ Version History tmux(1) or screen(1) by forcibly emulating their support by a proxy. * enhancement: ``setup.py develop`` ensures virtualenv and installs tox, and ``setup.py test`` calls tox. Requires pythons defined by tox.ini. + * enhancement: add ``rstrip()`` and ``lstrip()``, strips both sequences + and trailing or leading whitespace, respectively. * bugfix: if ``locale.getpreferredencoding()`` returns empty string or an encoding that is not a valid codec for ``codecs.getincrementaldecoder``, fallback to ascii and emit a warning. diff --git a/blessed/sequences.py b/blessed/sequences.py index dbc3a37b..a0f576e5 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -1,3 +1,4 @@ +# encoding: utf-8 " This sub-module provides 'sequence awareness' for blessed." __author__ = 'Jeff Quast ' @@ -268,9 +269,7 @@ def init_sequence_patterns(term): _re_will_move = re.compile('(%s)' % ('|'.join(_will_move))) _re_wont_move = re.compile('(%s)' % ('|'.join(_wont_move))) - # # static pattern matching for horizontal_distance(ucs, term) - # bnc = functools.partial(_build_numeric_capability, term) # parm_right_cursor: Move #1 characters to the right @@ -294,19 +293,6 @@ def init_sequence_patterns(term): '_cuf1': _cuf1, '_cub1': _cub1, } -# TODO(jquast): for some reason, 'screen' does not offer hpa and vpa, -# although they function perfectly fine ! We need some kind -# of patching function we may apply for such terminals .. -# -# if term._kind == 'screen' and term.hpa == u'': -# def screen_hpa(*args): -# return u'\x1b[{}G'.format(len(args) and args[0] + 1 or 1) -# term.hpa = screen_hpa -# if term._kind == 'screen' and term.vpa == u'': -# def screen_vpa(*args): -# return u'\x1b[{}d'.format(len(args) and args[0] + 1 or 1) -# term.vpa = screen_vpa - class SequenceTextWrapper(textwrap.TextWrapper): def __init__(self, width, term, **kwargs): @@ -318,7 +304,7 @@ def __init__(self, width, term, **kwargs): def _wrap_chunks(self, chunks): """ - escape-sequence aware varient of _wrap_chunks. Though + escape-sequence aware variant of _wrap_chunks. Though movement sequences, such as term.left() are certainly not honored, sequences such as term.bold() are, and are not broken mid-sequence. @@ -428,44 +414,91 @@ def length(self): # and http://www.gossamer-threads.com/lists/python/bugs/972834 return len(self.strip_seqs()) - def strip(self): - """S.strip() -> unicode + def strip(self, chars=None): + """S.strip([chars]) -> unicode + + Return a copy of the string S with terminal sequences removed, and + leading and trailing whitespace removed. + + If chars is given and not None, remove characters in chars instead. + """ + return self.strip_seqs().strip(chars) + + def lstrip(self, chars=None): + """S.lstrip([chars]) -> unicode + + Return a copy of the string S with terminal sequences and leading + whitespace removed. + + If chars is given and not None, remove characters in chars instead. + """ + return self.strip_seqs().lstrip(chars) + + def rstrip(self, chars=None): + """S.rstrip([chars]) -> unicode + + Return a copy of the string S with terminal sequences and trailing + whitespace removed. - Returns string derived from unicode string ``S``, stripped of - whitespace and terminal sequences. + If chars is given and not None, remove characters in chars instead. """ - return self.strip_seqs().strip() + return self.strip_seqs().rstrip(chars) def strip_seqs(self): """S.strip_seqs() -> unicode Return a string without sequences for a string that contains - (most types) of (escape) sequences for the Terminal with which - they were created. + sequences for the Terminal with which they were created. + + Where sequence ``move_right(n)`` is detected, it is replaced with + ``n * u' '``, and where ``move_left()`` or ``\\b`` is detected, + those last-most characters are destroyed. + + All other sequences are simply removed. An example, + >>> from blessed import Terminal + >>> from blessed.sequences import Sequence + >>> term = Terminal() + >>> Sequence(term.clear + term.red(u'test')).strip_seqs() + u'test' """ # nxt: points to first character beyond current escape sequence. # width: currently estimated display length. + input = self.padd() + outp = u'' + nxt = 0 + for idx in range(0, len(input)): + if idx == nxt: + # at sequence, point beyond it, + nxt = idx + measure_length(input[idx:], self._term) + if nxt <= idx: + # append non-sequence to outp, + outp += input[idx] + # point beyond next sequence, if any, + # otherwise point to next character + nxt = idx + measure_length(input[idx:], self._term) + 1 + return outp + + def padd(self): + """S.padd() -> unicode + Make non-destructive space or backspace into destructive ones. + + Where sequence ``move_right(n)`` is detected, it is replaced with + ``n * u' '``. Where sequence ``move_left(n)`` or ``\\b`` is + detected, those last-most characters are destroyed. + """ outp = u'' nxt = 0 for idx in range(0, unicode.__len__(self)): - # account for width of sequences that contain padding (a sort of - # SGR-equivalent cheat for the python equivalent of ' '*N, for - # very large values of N that may xmit fewer bytes than many raw - # spaces. It should be noted, however, that this is a - # non-destructive space. width = horizontal_distance(self[idx:], self._term) - if width > 0: - outp += u' ' * horizontal_distance(self[idx:], self._term) - elif width < 0: - # \b causes the previous character to be trimmed - outp = outp[:width] - if idx == nxt: - # point beyond this sequence + if width != 0: nxt = idx + measure_length(self[idx:], self._term) + if width > 0: + outp += u' ' * width + elif width < 0: + outp = outp[:width] if nxt <= idx: outp += self[idx] - # point beyond next sequence, if any, otherwise next character - nxt = idx + measure_length(self[idx:], self._term) + 1 + nxt = idx + 1 return outp @@ -551,11 +584,13 @@ def horizontal_distance(ucs, term): return -1 elif ucs.startswith('\t'): - # as best as I can prove it, a tabstop is always 8 by default. + # As best as I can prove it, a tabstop is always 8 by default. # Though, given that blessings is: + # # 1. unaware of the output device's current cursor position, and # 2. unaware of the location the callee may chose to output any # given string, + # # It is not possible to determine how many cells any particular # \t would consume on the output device! return 8 diff --git a/blessed/terminal.py b/blessed/terminal.py index 808fce75..99488a93 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -452,16 +452,29 @@ def length(self, text): """ return Sequence(text, self).length() - def strip(self, text): + def strip(self, text, chars=None): """T.strip(text) -> unicode - Return string ``text`` stripped of its whitespace *and* sequences. + Return string ``text`` with terminal sequences removed, and leading + and trailing whitespace removed. + """ + return Sequence(text, self).strip(chars) + + def rstrip(self, text, chars=None): + """T.rstrip(text) -> unicode + + Return string ``text`` with terminal sequences and trailing whitespace + removed. + """ + return Sequence(text, self).rstrip(chars) + + def lstrip(self, text, chars=None): + """T.lstrip(text) -> unicode - Text containing backspace or term.left will "overstrike", so that - the string ``u"_\\b"`` or ``u"__\\b\\b="`` becomes ``u"x"``, - not ``u"="`` (as would actually be printed on a terminal). + Return string ``text`` with terminal sequences and leading whitespace + removed. """ - return Sequence(text, self).strip() + return Sequence(text, self).lstrip(chars) def strip_seqs(self, text): """T.strip_seqs(text) -> unicode diff --git a/blessed/tests/test_length_sequence.py b/blessed/tests/test_length_sequence.py index 593d6685..60017eb0 100644 --- a/blessed/tests/test_length_sequence.py +++ b/blessed/tests/test_length_sequence.py @@ -32,65 +32,79 @@ def child(kind): # terminal sequences. Then, compare the length of # each, the basic plain_text.__len__ vs. the Terminal # method length. They should be equal. - plain_text = ('The softest things of the world ' - 'Override the hardest things of the world ' - 'That which has no substance ' - 'Enters into that which has no openings') + plain_text = (u'The softest things of the world ' + u'Override the hardest things of the world ' + u'That which has no substance ' + u'Enters into that which has no openings') if t.bold: assert (t.length(t.bold) == 0) - assert (t.length(t.bold('x')) == 1) + assert (t.length(t.bold(u'x')) == 1) assert (t.length(t.bold_red) == 0) - assert (t.length(t.bold_red('x')) == 1) + assert (t.length(t.bold_red(u'x')) == 1) assert (t.strip(t.bold) == u'') - assert (t.strip(t.bold(' x ')) == u'x') + assert (t.rstrip(t.bold) == u'') + assert (t.lstrip(t.bold) == u'') + assert (t.strip(t.bold(u' x ')) == u'x') + assert (t.strip(t.bold(u'z x q'), 'zq') == u' x ') + assert (t.rstrip(t.bold(u' x ')) == u' x') + assert (t.lstrip(t.bold(u' x ')) == u'x ') assert (t.strip(t.bold_red) == u'') - assert (t.strip(t.bold_red(' x ')) == u'x') + assert (t.rstrip(t.bold_red) == u'') + assert (t.lstrip(t.bold_red) == u'') + assert (t.strip(t.bold_red(u' x ')) == u'x') + assert (t.rstrip(t.bold_red(u' x ')) == u' x') + assert (t.lstrip(t.bold_red(u' x ')) == u'x ') assert (t.strip_seqs(t.bold) == u'') - assert (t.strip_seqs(t.bold(' x ')) == u' x ') + assert (t.strip_seqs(t.bold(u' x ')) == u' x ') assert (t.strip_seqs(t.bold_red) == u'') - assert (t.strip_seqs(t.bold_red(' x ')) == u' x ') + assert (t.strip_seqs(t.bold_red(u' x ')) == u' x ') if t.underline: assert (t.length(t.underline) == 0) - assert (t.length(t.underline('x')) == 1) + assert (t.length(t.underline(u'x')) == 1) assert (t.length(t.underline_red) == 0) - assert (t.length(t.underline_red('x')) == 1) + assert (t.length(t.underline_red(u'x')) == 1) assert (t.strip(t.underline) == u'') - assert (t.strip(t.underline(' x ')) == u'x') + assert (t.strip(t.underline(u' x ')) == u'x') assert (t.strip(t.underline_red) == u'') - assert (t.strip(t.underline_red(' x ')) == u'x') + assert (t.strip(t.underline_red(u' x ')) == u'x') + assert (t.rstrip(t.underline_red(u' x ')) == u' x') + assert (t.lstrip(t.underline_red(u' x ')) == u'x ') assert (t.strip_seqs(t.underline) == u'') - assert (t.strip_seqs(t.underline(' x ')) == u' x ') + assert (t.strip_seqs(t.underline(u' x ')) == u' x ') assert (t.strip_seqs(t.underline_red) == u'') - assert (t.strip_seqs(t.underline_red(' x ')) == u' x ') + assert (t.strip_seqs(t.underline_red(u' x ')) == u' x ') if t.reverse: assert (t.length(t.reverse) == 0) - assert (t.length(t.reverse('x')) == 1) + assert (t.length(t.reverse(u'x')) == 1) assert (t.length(t.reverse_red) == 0) - assert (t.length(t.reverse_red('x')) == 1) + assert (t.length(t.reverse_red(u'x')) == 1) assert (t.strip(t.reverse) == u'') - assert (t.strip(t.reverse(' x ')) == u'x') + assert (t.strip(t.reverse(u' x ')) == u'x') assert (t.strip(t.reverse_red) == u'') - assert (t.strip(t.reverse_red(' x ')) == u'x') + assert (t.strip(t.reverse_red(u' x ')) == u'x') + assert (t.rstrip(t.reverse_red(u' x ')) == u' x') + assert (t.lstrip(t.reverse_red(u' x ')) == u'x ') assert (t.strip_seqs(t.reverse) == u'') - assert (t.strip_seqs(t.reverse(' x ')) == u' x ') + assert (t.strip_seqs(t.reverse(u' x ')) == u' x ') assert (t.strip_seqs(t.reverse_red) == u'') - assert (t.strip_seqs(t.reverse_red(' x ')) == u' x ') + assert (t.strip_seqs(t.reverse_red(u' x ')) == u' x ') if t.blink: assert (t.length(t.blink) == 0) - assert (t.length(t.blink('x')) == 1) + assert (t.length(t.blink(u'x')) == 1) assert (t.length(t.blink_red) == 0) - assert (t.length(t.blink_red('x')) == 1) + assert (t.length(t.blink_red(u'x')) == 1) assert (t.strip(t.blink) == u'') - assert (t.strip(t.blink(' x ')) == u'x') + assert (t.strip(t.blink(u' x ')) == u'x') + assert (t.strip(t.blink(u'z x q'), u'zq') == u' x ') assert (t.strip(t.blink_red) == u'') - assert (t.strip(t.blink_red(' x ')) == u'x') + assert (t.strip(t.blink_red(u' x ')) == u'x') assert (t.strip_seqs(t.blink) == u'') - assert (t.strip_seqs(t.blink(' x ')) == u' x ') + assert (t.strip_seqs(t.blink(u' x ')) == u' x ') assert (t.strip_seqs(t.blink_red) == u'') - assert (t.strip_seqs(t.blink_red(' x ')) == u' x ') + assert (t.strip_seqs(t.blink_red(u' x ')) == u' x ') if t.home: assert (t.length(t.home) == 0) diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index bd8a7a54..77dbc5f0 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -503,3 +503,16 @@ def test_bna_parameter_emits_warning(): else: assert False, 'Previous stmt should have raised exception.' warnings.resetwarnings() + + +def test_padd(): + """ Test terminal.padd(seq). """ + @as_subprocess + def child(): + from blessed.sequences import Sequence + from blessed import Terminal + term = Terminal('xterm-256color') + assert Sequence('xyz\b', term).padd() == u'xy' + assert Sequence('xxxx\x1b[3Dzz', term).padd() == u'xzz' + + child() From dddceec7cb74c367670559c4df7fe1e2c670b4ab Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 17 May 2014 20:41:38 -0700 Subject: [PATCH 187/459] Conflicts: README.rst --- README.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 3fde3bc4..d7618847 100644 --- a/README.rst +++ b/README.rst @@ -665,8 +665,8 @@ shares the same. See the LICENSE file. Version History =============== 1.8 - * enhancement: export keyboard-read function as public method ``getch()``, so - that it may be overridden by custom terminal implementers. + * enhancement: export keyboard-read function as public method ``getch()``, + so that it may be overridden by custom terminal implementers. * enhancement: allow ``inkey()`` and ``kbhit()`` to return early when interrupted by signal by passing argument ``_intr_continue=False``. * enhancement: allow ``hpa`` and ``vpa`` (*move_x*, *move_y*) to work on @@ -675,6 +675,9 @@ Version History and ``setup.py test`` calls tox. Requires pythons defined by tox.ini. * enhancement: add ``rstrip()`` and ``lstrip()``, strips both sequences and trailing or leading whitespace, respectively. + * enhancement: include wcwidth_ library support for ``length()``, the + printable width of many kinds of CJK (Chinese, Japanese, Korean) ideographs + are more correctly determined. * bugfix: if ``locale.getpreferredencoding()`` returns empty string or an encoding that is not a valid codec for ``codecs.getincrementaldecoder``, fallback to ascii and emit a warning. From 8900c08ea1388a3d3bcf28c0fdb3f84884d09c89 Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 17 May 2014 20:41:58 -0700 Subject: [PATCH 188/459] add wcwidth (my own library), support for CJK characters --- blessed/sequences.py | 29 ++++++++++++++++++++------- blessed/tests/test_length_sequence.py | 15 ++++++++++++++ docs/conf.py | 2 +- requirements.txt | 1 + setup.py | 12 +++++++---- tox.ini | 2 ++ 6 files changed, 49 insertions(+), 12 deletions(-) create mode 100644 requirements.txt diff --git a/blessed/sequences.py b/blessed/sequences.py index a0f576e5..96bb8d03 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -6,12 +6,16 @@ __all__ = ['init_sequence_patterns', 'Sequence', 'SequenceTextWrapper'] +# built-ins import functools import textwrap import warnings import math import re +# 3rd-party +import wcwidth # https://github.com/jquast/wcwidth + _BINTERM_UNSUPPORTED = ('kermit', 'avatar') _BINTERM_UNSUPPORTED_MSG = ('sequence-awareness for terminals emitting ' 'binary-packed capabilities are not supported.') @@ -401,18 +405,29 @@ def length(self): terminal sequences. Although accounted for, strings containing sequences such as - ``term.clear`` will not give accurate returns, it is considered - un-lengthy (length of 0). + ``term.clear`` will not give accurate returns, it is not + considered lengthy (a length of 0). Combining characters, + are also not considered lengthy. Strings containing ``term.left`` or ``\b`` will cause "overstrike", but a length less than 0 is not ever returned. So ``_\b+`` is a length of 1 (``+``), but ``\b`` is simply a length of 0. + + Some characters may consume more than one cell, mainly those CJK + Unified Ideographs (Chinese, Japanese, Korean) defined by Unicode + as half or full-width characters. + + For example: + >>> from blessed import Terminal + >>> from blessed.sequences import Sequence + >>> term = Terminal() + >>> Sequence(term.clear + term.red(u'コンニチハ')).length() + 5 """ - # TODO(jquast): Should we implement the terminal printable - # width of 'East Asian Fullwidth' and 'East Asian Wide' characters, - # which can take 2 cells, see http://www.unicode.org/reports/tr11/ - # and http://www.gossamer-threads.com/lists/python/bugs/972834 - return len(self.strip_seqs()) + # because combining characters may return -1, "clip" their length to 0. + clip = functools.partial(max, 0) + return sum(clip(wcwidth.wcwidth(w_char)) + for w_char in self.strip_seqs()) def strip(self, chars=None): """S.strip([chars]) -> unicode diff --git a/blessed/tests/test_length_sequence.py b/blessed/tests/test_length_sequence.py index 60017eb0..0bbb8bff 100644 --- a/blessed/tests/test_length_sequence.py +++ b/blessed/tests/test_length_sequence.py @@ -1,3 +1,4 @@ +# encoding: utf-8 import itertools import platform import termios @@ -22,6 +23,20 @@ import pytest +def test_length_cjk(): + + @as_subprocess + def child(): + term = TestTerminal(kind='xterm-256color') + + # given, + given = term.bold_red(u'コンニチハ, セカイ!') + expected = sum((2, 2, 2, 2, 2, 1, 1, 2, 2, 2, 1,)) + assert term.length(given) == expected + + child() + + def test_sequence_length(all_terms): """Ensure T.length(string containing sequence) is correct.""" @as_subprocess diff --git a/docs/conf.py b/docs/conf.py index 547e231d..b6193419 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -53,7 +53,7 @@ # built documents. # # The short X.Y version. -version = '1.8.6' +version = '1.8.7' # The full version, including alpha/beta/rc tags. release = version diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..f05e0d33 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +wcwidth>=0.1.0 diff --git a/setup.py b/setup.py index 7f829635..b3cfd23b 100755 --- a/setup.py +++ b/setup.py @@ -19,17 +19,21 @@ def run(self): def main(): - extra = {} + extra = { + 'install_requires': [ + 'wcwidth>=0.1.0', + ] + } if sys.version_info < (2, 7,): - extra.update({'install_requires': 'ordereddict'}) + extra['install_requires'].extend(['ordereddict>=1.1']) elif sys.version_info >= (3,): - extra.update({'use_2to3': True}) + extra['use_2to3'] = True here = os.path.dirname(__file__) setuptools.setup( name='blessed', - version='1.8.6', + version='1.8.7', description="A feature-filled fork of Erik Rose's blessings project", long_description=open(os.path.join(here, 'README.rst')).read(), author='Jeff Quast', diff --git a/tox.ini b/tox.ini index 952ed282..41e5150d 100644 --- a/tox.ini +++ b/tox.ini @@ -13,6 +13,7 @@ deps = pytest pytest-pep8 pytest-flakes mock + -rrequirements.txt whitelist_externals = /bin/bash @@ -34,6 +35,7 @@ deps = pytest pytest-pep8 pytest-flakes mock + -rrequirements.txt # run each test twice -- 1. w/o tty, From dc8ec2df32efbfbf5526fc25118e878c8f5c4c13 Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 17 May 2014 22:36:29 -0700 Subject: [PATCH 189/459] better ecma-48 detection in aixterm found that the printable length of \x1b[32;4m was not detected by xterm ... why is that? because xterm's own would generate \x1b(B\x1b[32;4m. Rather problematic, we use an ordered list to add this various alternate types as a 'fall through', that is, at the end. --- MANIFEST.in | 1 + README.rst | 2 ++ blessed/sequences.py | 27 +++++++++++++++++++++------ blessed/tests/test_length_sequence.py | 25 +++++++++++++++++++++++-- blessed/tests/wall.ans | 7 +++++++ docs/conf.py | 2 +- setup.py | 2 +- 7 files changed, 56 insertions(+), 10 deletions(-) create mode 100644 blessed/tests/wall.ans diff --git a/MANIFEST.in b/MANIFEST.in index 3f4fbd70..8891ea07 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ include README.rst include LICENSE include tox.ini +include blessed/tests/wall.ans diff --git a/README.rst b/README.rst index d7618847..0c329dee 100644 --- a/README.rst +++ b/README.rst @@ -678,6 +678,8 @@ Version History * enhancement: include wcwidth_ library support for ``length()``, the printable width of many kinds of CJK (Chinese, Japanese, Korean) ideographs are more correctly determined. + * enhancement: better support for detecting the length or sequences of + externally-generated *ecma-48* codes when using ``xterm`` or ``aixterm``. * bugfix: if ``locale.getpreferredencoding()`` returns empty string or an encoding that is not a valid codec for ``codecs.getincrementaldecoder``, fallback to ascii and emit a warning. diff --git a/blessed/sequences.py b/blessed/sequences.py index 96bb8d03..a08c9261 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -39,8 +39,8 @@ def _build_numeric_capability(term, cap, optional=False, if _cap: args = (base_num,) * nparams cap_re = re.escape(_cap(*args)) - for num in range(base_num-1, base_num+2): - # search for matching ascii, n-1 through n+2 + for num in range(base_num - 1, base_num + 2): + # search for matching ascii, n-1 through n+1 if str(num) in cap_re: # modify & return n to matching digit expression cap_re = cap_re.replace(str(num), r'(\d+)%s' % (opt,)) @@ -261,13 +261,28 @@ def init_sequence_patterns(term): # Build will_move, a list of terminal capabilities that have # indeterminate effects on the terminal cursor position. - _will_move = _merge_sequences(get_movement_sequence_patterns(term) - ) if term.does_styling else set() + _will_move = set() + if term.does_styling: + _will_move = _merge_sequences(get_movement_sequence_patterns(term)) # Build wont_move, a list of terminal capabilities that mainly affect # video attributes, for use with measure_length(). - _wont_move = _merge_sequences(get_wontmove_sequence_patterns(term) - ) if term.does_styling else set() + _wont_move = set() + if term.does_styling: + _wont_move = _merge_sequences(get_wontmove_sequence_patterns(term)) + _wont_move += [ + # some last-ditch match efforts; well, xterm and aixterm is going + # to throw \x1b(B and other oddities all around, so, when given + # input such as ansi art (see test using wall.ans), and well, + # theres no reason a vt220 terminal shouldn't be able to recognize + # blue_on_red, even if it didn't cause it to be generated. these + # are final "ok, i will match this, anyway" + re.escape(u'\x1b') + r'\[(\d+)m', + re.escape(u'\x1b') + r'\[(\d+)\;(\d+)m', + re.escape(u'\x1b') + r'\[(\d+)\;(\d+)\;(\d+)m', + re.escape(u'\x1b') + r'\[(\d+)\;(\d+)\;(\d+)\;(\d+)m', + re.escape(u'\x1b(B'), + ] # compile as regular expressions, OR'd. _re_will_move = re.compile('(%s)' % ('|'.join(_will_move))) diff --git a/blessed/tests/test_length_sequence.py b/blessed/tests/test_length_sequence.py index 0bbb8bff..e93c5f22 100644 --- a/blessed/tests/test_length_sequence.py +++ b/blessed/tests/test_length_sequence.py @@ -24,7 +24,6 @@ def test_length_cjk(): - @as_subprocess def child(): term = TestTerminal(kind='xterm-256color') @@ -32,17 +31,39 @@ def child(): # given, given = term.bold_red(u'コンニチハ, セカイ!') expected = sum((2, 2, 2, 2, 2, 1, 1, 2, 2, 2, 1,)) + + # exercise, assert term.length(given) == expected child() +def test_length_ansiart(): + @as_subprocess + def child(): + import codecs + from blessed.sequences import Sequence + term = TestTerminal(kind='xterm-256color') + # this 'ansi' art contributed by xzip!impure for another project, + # unlike most CP-437 DOS ansi art, this is actually utf-8 encoded. + fname = os.path.join(os.path.dirname(__file__), 'wall.ans') + lines = codecs.open(fname, 'r', 'utf-8').readlines() + assert term.length(lines[0]) == 67 # ^[[64C^[[34m▄▓▄ + assert term.length(lines[1]) == 75 + assert term.length(lines[2]) == 78 + assert term.length(lines[3]) == 78 + assert term.length(lines[4]) == 78 + assert term.length(lines[5]) == 78 + assert term.length(lines[6]) == 77 + child() + + def test_sequence_length(all_terms): """Ensure T.length(string containing sequence) is correct.""" @as_subprocess def child(kind): t = TestTerminal(kind=kind) - # Create a list of ascii characters, to be seperated + # Create a list of ascii characters, to be separated # by word, to be zipped up with a cycling list of # terminal sequences. Then, compare the length of # each, the basic plain_text.__len__ vs. the Terminal diff --git a/blessed/tests/wall.ans b/blessed/tests/wall.ans new file mode 100644 index 00000000..081b4d28 --- /dev/null +++ b/blessed/tests/wall.ans @@ -0,0 +1,7 @@ +▄▓▄ + ░░ █▀▀█▀██▀████████▀█▀▀█ █▀▀█▀██▀▀█▀█xz(imp) + ▄▄▄ ▓█████ ▄▄ █▀▀█░ ▀▀▀▀▀ █████▓ ▓█████ ░▓ █▀▀█░ ▓█████ ░▓ █▀▀█░ ░▄ ░░ + ▐▄░▄▀ ▀▀▀▀▀▀ █▓ ▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀ ██ ▀▀▀▀▀ ▀▀▀▀▀▀ ██ ▀▀▀▀▀ █▄▌▌▄▄ + █▓██ ░▓████ ▐ ████░ ░████ ▄█ ████▓░ ░▓████ ██ ████░ ░▓████ ██ ████░ █▐▄▄█░ + ██▓▀ ░▓████ █ ████░ ░████ ▐ ████▓░ ░▓████ ▄█▌████░ ░▓████ ▄█▌████░ ▌█████ + ▀ ░▓████▄█▄▄███ ░ ░ ███▄▄▄▄████▓░ ░▓████▄▄▄▄███ ░ ░▓████▄▄▄▄███ ░ ▀▀▀▓ diff --git a/docs/conf.py b/docs/conf.py index b6193419..bb5c999d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -53,7 +53,7 @@ # built documents. # # The short X.Y version. -version = '1.8.7' +version = '1.8.8' # The full version, including alpha/beta/rc tags. release = version diff --git a/setup.py b/setup.py index b3cfd23b..b5abb414 100755 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ def main(): here = os.path.dirname(__file__) setuptools.setup( name='blessed', - version='1.8.7', + version='1.8.8', description="A feature-filled fork of Erik Rose's blessings project", long_description=open(os.path.join(here, 'README.rst')).read(), author='Jeff Quast', From 90a4add5f1c109464eb2dff3174c14ec1962ae63 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Wed, 21 May 2014 19:53:51 -0700 Subject: [PATCH 190/459] re-introduce target hyperlinks to "badges" silly stuff but maybe useful for quick discovery/navigation --- README.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.rst b/README.rst index 0c329dee..36b73ea8 100644 --- a/README.rst +++ b/README.rst @@ -1,14 +1,18 @@ .. image:: https://img.shields.io/travis/jquast/blessed.svg :alt: Travis Continous Integration + :target: https://travis-ci.org/jquast/blessed .. image:: https://img.shields.io/coveralls/jquast/blessed.svg :alt: Coveralls Code Coverage + :target: https://coveralls.io/r/jquast/blessed .. image:: https://img.shields.io/pypi/v/blessed.svg :alt: Latest Version + :target: https://pypi.python.org/pypi/blessed .. image:: https://pypip.in/license/blessed/badge.svg :alt: License + :target: http://opensource.org/licenses/MIT .. image:: https://img.shields.io/pypi/dm/blessed.svg :alt: Downloads From c3fd7f2bedc51960913c8fb3a09d0d8708de2f64 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 8 Jun 2014 15:10:24 -0700 Subject: [PATCH 191/459] Bugfix reported by hellbeard!impure `\x1bOH' was incorrectly mapped as KEY_LEFT, when it should be KEY_HOME. Thanks hellboard! --- README.rst | 1 + bin/test_keyboard_keys.py | 34 ++++++++++++++-------------------- blessed/keyboard.py | 2 +- blessed/terminal.py | 17 +++++++++-------- setup.py | 2 +- 5 files changed, 26 insertions(+), 30 deletions(-) diff --git a/README.rst b/README.rst index 0c329dee..1f90201a 100644 --- a/README.rst +++ b/README.rst @@ -689,6 +689,7 @@ Version History * **change**: ``term.keyboard_fd`` is set ``None`` if ``stream`` or ``sys.stdout`` is not a tty, making ``term.inkey()``, ``term.cbreak()``, ``term.raw()``, no-op. + * bugfix: ``\x1bOH`` (KEY_HOME) was incorrectly mapped as KEY_LEFT. 1.7 * Forked github project `erikrose/blessings`_ to `jquast/blessed`_, this diff --git a/bin/test_keyboard_keys.py b/bin/test_keyboard_keys.py index d751af21..af258139 100755 --- a/bin/test_keyboard_keys.py +++ b/bin/test_keyboard_keys.py @@ -96,26 +96,20 @@ def add_score(score, pts, level): inps.append(inp) with term.cbreak(): - sys.stdout.write(u''.join(( - term.move(term.height), - term.clear_eol, - u'Your final score was %s' % (score,), - u' at level %s' % (level,), - term.clear_eol, - u'\n', - term.clear_eol, - u'You hit %s' % (hit_highbit,), - u' 8-bit (extended ascii) characters', - term.clear_eol, - u'\n', - term.clear_eol, - u'You hit %s' % (hit_unicode,), - u' unicode characters.', - term.clear_eol, - u'\n', - term.clear_eol, - u'press any key', - term.clear_eol,)) + sys.stdout.write(term.move(term.height)) + sys.stdout.write( + u'{term.clear_eol}Your final score was {score} ' + u'at level {level}{term.clear_eol}\n' + u'{term.clear_eol}\n' + u'{term.clear_eol}You hit {hit_highbit} ' + u' 8-bit characters\n{term.clear_eol}\n' + u'{term.clear_eol}You hit {hit_unicode} ' + u' unicode characters.\n{term.clear_eol}\n' + u'{term.clear_eol}press any key\n'.format( + term=term, + score=score, level=level, + hit_highbit=hit_highbit, + hit_unicode=hit_unicode) ) term.inkey() diff --git a/blessed/keyboard.py b/blessed/keyboard.py index 880e4399..0d60582b 100644 --- a/blessed/keyboard.py +++ b/blessed/keyboard.py @@ -192,7 +192,7 @@ def resolve_sequence(text, mapper, codes): (u"\x1bOB", curses.KEY_DOWN), (u"\x1bOC", curses.KEY_RIGHT), (u"\x1bOD", curses.KEY_LEFT), - (u"\x1bOH", curses.KEY_LEFT), + (u"\x1bOH", curses.KEY_HOME), (u"\x1bOF", curses.KEY_END), (u"\x1bOP", curses.KEY_F1), (u"\x1bOQ", curses.KEY_F2), diff --git a/blessed/terminal.py b/blessed/terminal.py index 99488a93..c3b1fe76 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -484,19 +484,20 @@ def strip_seqs(self, text): return Sequence(text, self).strip_seqs() def wrap(self, text, width=None, **kwargs): - """T.wrap(text, [width=None, indent=u'', ...]) -> unicode + """T.wrap(text, [width=None, **kwargs ..]) -> list[unicode] - Wrap paragraphs containing escape sequences, ``text``, to the full - width of Terminal instance T, unless width is specified, wrapped by - the virtual printable length, irregardless of the video attribute - sequences it may contain. + Wrap paragraphs containing escape sequences ``text`` to the full + ``width`` of Terminal instance *T*, unless ``width`` is specified. + Wrapped by the virtual printable length, irregardless of the video + attribute sequences it may contain, allowing text containing colors, + bold, underline, etc. to be wrapped. Returns a list of strings that may contain escape sequences. See - textwrap.TextWrapper class for available additional kwargs to - customize wrapping behavior. + ``textwrap.TextWrapper`` for all available additional kwargs to + customize wrapping behavior such as ``subsequent_indent``. Note that the keyword argument ``break_long_words`` may not be set, - it is not sequence-safe. + it is not sequence-safe! """ _blw = 'break_long_words' diff --git a/setup.py b/setup.py index b5abb414..a188a781 100755 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ def main(): here = os.path.dirname(__file__) setuptools.setup( name='blessed', - version='1.8.8', + version='1.8.9', description="A feature-filled fork of Erik Rose's blessings project", long_description=open(os.path.join(here, 'README.rst')).read(), author='Jeff Quast', From 07a4df55583e5ed720542106fe26afb34556fcaf Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 29 Jun 2014 00:50:27 -0700 Subject: [PATCH 192/459] release version 1.9 * workaround: ignore 'tparm() returned NULL', this occurs on win32 platforms using PDCurses_ without a termcap or termlib. * bugfix: term.center('text') should not padd right side with whitespace. --- README.rst | 6 ++++++ blessed/formatters.py | 6 ++++++ blessed/sequences.py | 4 +--- docs/conf.py | 4 ++-- setup.py | 2 +- tox.ini | 8 ++------ 6 files changed, 18 insertions(+), 12 deletions(-) diff --git a/README.rst b/README.rst index dd35516c..f6258fff 100644 --- a/README.rst +++ b/README.rst @@ -668,6 +668,11 @@ shares the same. See the LICENSE file. Version History =============== +1.9 + * workaround: ignore 'tparm() returned NULL', this occurs on win32 + platforms using PDCurses_ without a termcap or termlib. + * bugfix: term.center('text') should not padd right side with whitespace. + 1.8 * enhancement: export keyboard-read function as public method ``getch()``, so that it may be overridden by custom terminal implementers. @@ -825,3 +830,4 @@ Version History .. _tparm: http://www.openbsd.org/cgi-bin/man.cgi?query=tparm&sektion=3 .. _SIGWINCH: https://en.wikipedia.org/wiki/SIGWINCH .. _`API Documentation`: http://blessed.rtfd.org +.. _`PDCurses`: http://www.lfd.uci.edu/~gohlke/pythonlibs/#curses diff --git a/blessed/formatters.py b/blessed/formatters.py index b33ca215..1af64710 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -65,6 +65,12 @@ def __call__(self, *args): # Somebody passed a non-string; I don't feel confident # guessing what they were trying to do. raise + except Exception, err: + # ignore 'tparm() returned NULL', you won't get any styling, + # even if does_styling is True. This happens on win32 platforms + # with http://www.lfd.uci.edu/~gohlke/pythonlibs/#curses installed + if "tparm() returned NULL" not in err: + raise class ParameterizingProxyString(unicode): diff --git a/blessed/sequences.py b/blessed/sequences.py index a08c9261..b7a8a798 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -409,9 +409,7 @@ def center(self, width, fillchar=u' '): split = max(0.0, float(width) - self.length()) / 2 leftside = fillchar * int((max(0.0, math.floor(split))) / float(len(fillchar))) - rightside = fillchar * int((max(0.0, math.ceil(split))) - / float(len(fillchar))) - return u''.join((leftside, self, rightside)) + return u''.join((leftside, self)) def length(self): """S.length() -> int diff --git a/docs/conf.py b/docs/conf.py index bb5c999d..de243fa6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -53,12 +53,12 @@ # built documents. # # The short X.Y version. -version = '1.8.8' +version = '1.9.0' # The full version, including alpha/beta/rc tags. release = version -# The language for content autogenerated by Sphinx. Refer to documentation +# The language for content auto-generated by Sphinx. Refer to documentation # for a list of supported languages. # language = None diff --git a/setup.py b/setup.py index a188a781..cc8915aa 100755 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ def main(): here = os.path.dirname(__file__) setuptools.setup( name='blessed', - version='1.8.9', + version='1.9.0', description="A feature-filled fork of Erik Rose's blessings project", long_description=open(os.path.join(here, 'README.rst')).read(), author='Jeff Quast', diff --git a/tox.ini b/tox.ini index 41e5150d..23379ee8 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,6 @@ [tox] envlist = py26, py27, - py33, py34, pypy @@ -16,6 +15,7 @@ deps = pytest -rrequirements.txt whitelist_externals = /bin/bash +setenv = PYTHONIOENCODING=UTF8 # run each test twice -- 1. w/o tty commands = /bin/bash -c {envbindir}/py.test -v \ @@ -37,6 +37,7 @@ deps = pytest mock -rrequirements.txt +setenv = PYTHONIOENCODING=UTF8 # run each test twice -- 1. w/o tty, commands = /bin/bash -c {envbindir}/py.test -v \ @@ -54,11 +55,6 @@ commands = /bin/bash -c {envbindir}/py.test -v \ # # some issue with py.test & python 3 does not allow non-tty testing. -[testenv:py33] -changedir = {toxworkdir} -commands = {envbindir}/py.test -x --strict --pep8 --flakes \ - {envsitepackagesdir}/blessed/tests {posargs} - [testenv:py34] changedir = {toxworkdir} commands = {envbindir}/py.test -x --strict --pep8 --flakes \ From c74b8c0a179fcf2fda532a5f87d4980b18d14154 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 29 Jun 2014 01:06:10 -0700 Subject: [PATCH 193/459] revert the center change after some thought * it is incompatible with str.center() * considering expectations of term.center(term.reverse('x')) --- README.rst | 3 +-- blessed/sequences.py | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index f6258fff..f672672d 100644 --- a/README.rst +++ b/README.rst @@ -670,8 +670,7 @@ Version History =============== 1.9 * workaround: ignore 'tparm() returned NULL', this occurs on win32 - platforms using PDCurses_ without a termcap or termlib. - * bugfix: term.center('text') should not padd right side with whitespace. + platforms using PDCurses_ where tparm() is not implemented. 1.8 * enhancement: export keyboard-read function as public method ``getch()``, diff --git a/blessed/sequences.py b/blessed/sequences.py index b7a8a798..a08c9261 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -409,7 +409,9 @@ def center(self, width, fillchar=u' '): split = max(0.0, float(width) - self.length()) / 2 leftside = fillchar * int((max(0.0, math.floor(split))) / float(len(fillchar))) - return u''.join((leftside, self)) + rightside = fillchar * int((max(0.0, math.ceil(split))) + / float(len(fillchar))) + return u''.join((leftside, self, rightside)) def length(self): """S.length() -> int From e47d0004a4eb6cb57af8769ba2624ea64bca8335 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 29 Jun 2014 01:11:38 -0700 Subject: [PATCH 194/459] except Exception, err -> Exception as err --- blessed/formatters.py | 4 ++-- blessed/terminal.py | 2 +- blessed/tests/test_core.py | 4 ++-- blessed/tests/test_formatters.py | 4 ++-- blessed/tests/test_wrap.py | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/blessed/formatters.py b/blessed/formatters.py index 1af64710..f0733d2a 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -53,7 +53,7 @@ def __call__(self, *args): # concats work. attr = curses.tparm(self.encode('latin1'), *args).decode('latin1') return FormattingString(attr, self._normal) - except TypeError, err: + except TypeError as err: # If the first non-int (i.e. incorrect) arg was a string, suggest # something intelligent: if len(args) and isinstance(args[0], basestring): @@ -65,7 +65,7 @@ def __call__(self, *args): # Somebody passed a non-string; I don't feel confident # guessing what they were trying to do. raise - except Exception, err: + except Exception as err: # ignore 'tparm() returned NULL', you won't get any styling, # even if does_styling is True. This happens on win32 platforms # with http://www.lfd.uci.edu/~gohlke/pythonlibs/#curses installed diff --git a/blessed/terminal.py b/blessed/terminal.py index c3b1fe76..3d6179a0 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -203,7 +203,7 @@ def __init__(self, kind=None, stream=None, force_styling=False): try: self._keyboard_decoder = codecs.getincrementaldecoder( self._encoding)() - except LookupError, err: + except LookupError as err: warnings.warn('%s, fallback to ASCII for keyboard.' % (err,)) self._encoding = 'ascii' self._keyboard_decoder = codecs.getincrementaldecoder( diff --git a/blessed/tests/test_core.py b/blessed/tests/test_core.py index 72988c26..8cfe281e 100644 --- a/blessed/tests/test_core.py +++ b/blessed/tests/test_core.py @@ -230,7 +230,7 @@ def test_missing_ordereddict_uses_module(monkeypatch): try: imp.reload(blessed.keyboard) - except ImportError, err: + except ImportError as err: assert err.args[0] in ("No module named ordereddict", # py2 "No module named 'ordereddict'") # py3 sys.modules['ordereddict'] = mock.Mock() @@ -255,7 +255,7 @@ def test_python3_2_raises_exception(monkeypatch): try: imp.reload(blessed) - except ImportError, err: + except ImportError as err: assert err.args[0] == ( 'Blessed needs Python 3.2.3 or greater for Python 3 ' 'support due to http://bugs.python.org/issue10570.') diff --git a/blessed/tests/test_formatters.py b/blessed/tests/test_formatters.py index 15a18b95..68af2e92 100644 --- a/blessed/tests/test_formatters.py +++ b/blessed/tests/test_formatters.py @@ -85,7 +85,7 @@ def tparm_raises_TypeError(*args): try: pstr('XYZ') assert False, "previous call should have raised TypeError" - except TypeError, err: + except TypeError as err: assert (err.args[0] == ( # py3x "A native or nonexistent capability template, " "'cap-name' received invalid argument ('XYZ',): " @@ -101,7 +101,7 @@ def tparm_raises_TypeError(*args): try: pstr(0) assert False, "previous call should have raised TypeError" - except TypeError, err: + except TypeError as err: assert err.args[0] == "custom_err" diff --git a/blessed/tests/test_wrap.py b/blessed/tests/test_wrap.py index 3d18f1d2..d15073e5 100644 --- a/blessed/tests/test_wrap.py +++ b/blessed/tests/test_wrap.py @@ -24,7 +24,7 @@ def child(): t = TestTerminal() try: my_wrapped = t.wrap(u'------- -------------', WIDTH) - except ValueError, err: + except ValueError as err: assert err.args[0] == ( "invalid width %r(%s) (must be integer > 0)" % ( WIDTH, type(WIDTH))) From 0883703df43a6c8fdeb34594cf0fd2fe001efd28 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 29 Jun 2014 01:15:07 -0700 Subject: [PATCH 195/459] bring back py33, seems travis still has it --- tox.ini | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tox.ini b/tox.ini index 23379ee8..9d06f52d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,7 @@ [tox] envlist = py26, py27, + py33, py34, pypy @@ -55,6 +56,11 @@ commands = /bin/bash -c {envbindir}/py.test -v \ # # some issue with py.test & python 3 does not allow non-tty testing. +[testenv:py33] +changedir = {toxworkdir} +commands = {envbindir}/py.test -x --strict --pep8 --flakes \ + {envsitepackagesdir}/blessed/tests {posargs} + [testenv:py34] changedir = {toxworkdir} commands = {envbindir}/py.test -x --strict --pep8 --flakes \ From b52fe13ec6fe70e79a6ebe8d72e80d853cc8ee1d Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 29 Jun 2014 01:33:14 -0700 Subject: [PATCH 196/459] Support platforms without fcntl/termios/tty --- blessed/terminal.py | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/blessed/terminal.py b/blessed/terminal.py index 3d6179a0..d1db30aa 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -4,18 +4,31 @@ import contextlib import functools import warnings -import termios import codecs import curses import locale import select import struct -import fcntl import time -import tty import sys import os +try: + import termios + import fcntl + import tty +except ImportError: + tty_methods = ('setraw', 'cbreak', 'kbhit', 'height', 'width') + msg_nosupport = ( + 'One or more of the modules termios, fcntl, and tty were ' + 'not found on your platform {0}. The following methods are ' + 'dummy/no-op unless a deriving class overrides them: ' + '{1}'.format(sys.platform.lower(), ', '.join(tty_methods))) + warnings.warn(msg_nosupport) + HAS_TTY = False +else: + HAS_TTY = True + try: from io import UnsupportedOperation as IOUnsupportedOperation except ImportError: @@ -271,8 +284,10 @@ def _winsize(fd): May raise exception IOError. """ - data = fcntl.ioctl(fd, termios.TIOCGWINSZ, WINSZ._BUF) - return WINSZ(*struct.unpack(WINSZ._FMT, data)) + if HAS_TTY: + data = fcntl.ioctl(fd, termios.TIOCGWINSZ, WINSZ._BUF) + return WINSZ(*struct.unpack(WINSZ._FMT, data)) + return WINSZ(80, 24, 0, 0) def _height_and_width(self): """Return a tuple of (terminal height, terminal width). @@ -546,10 +561,10 @@ def kbhit(self, timeout=None, _intr_continue=True): # beyond timeout, then False is returned. Otherwise, when timeout is 0, # we continue to block indefinitely (default). stime = time.time() - check_w, check_x = [], [] + check_w, check_x, ready_r = [], [], [None, ] check_r = [self.keyboard_fd] if self.keyboard_fd is not None else [] - while True: + while HAS_TTY and True: try: ready_r, ready_w, ready_x = select.select( check_r, check_w, check_x, timeout) @@ -589,7 +604,7 @@ def cbreak(self): Note also that setcbreak sets VMIN = 1 and VTIME = 0, http://www.unixwiz.net/techtips/termios-vmin-vtime.html """ - if self.keyboard_fd is not None: + if HAS_TTY and self.keyboard_fd is not None: # save current terminal mode, save_mode = termios.tcgetattr(self.keyboard_fd) tty.setcbreak(self.keyboard_fd, termios.TCSANOW) @@ -611,7 +626,7 @@ def raw(self): suspend, and flow control characters are all passed through as their raw character values instead of generating a signal. """ - if self.keyboard_fd is not None: + if HAS_TTY and self.keyboard_fd is not None: # save current terminal mode, save_mode = termios.tcgetattr(self.keyboard_fd) tty.setraw(self.keyboard_fd, termios.TCSANOW) From 48a2434771b4bdd36c9b7cff6bc76dfa78ed7319 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 29 Jun 2014 01:35:14 -0700 Subject: [PATCH 197/459] return NullCallableString for WIN32 tparm failures --- blessed/formatters.py | 1 + 1 file changed, 1 insertion(+) diff --git a/blessed/formatters.py b/blessed/formatters.py index f0733d2a..157b70c2 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -71,6 +71,7 @@ def __call__(self, *args): # with http://www.lfd.uci.edu/~gohlke/pythonlibs/#curses installed if "tparm() returned NULL" not in err: raise + return NullCallableString() class ParameterizingProxyString(unicode): From 87a2ec6781864d9d3df34f0b79f49aa80cf26021 Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 29 Jun 2014 01:42:53 -0700 Subject: [PATCH 198/459] cut 1.9.1 release ignores failures to import termios, fcntl, tty with warning about which features will not work, with HAS_TTY wrappers --- docs/conf.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index de243fa6..ca3dcdaa 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -53,7 +53,7 @@ # built documents. # # The short X.Y version. -version = '1.9.0' +version = '1.9.1' # The full version, including alpha/beta/rc tags. release = version diff --git a/setup.py b/setup.py index cc8915aa..e87e1b4f 100755 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ def main(): here = os.path.dirname(__file__) setuptools.setup( name='blessed', - version='1.9.0', + version='1.9.1', description="A feature-filled fork of Erik Rose's blessings project", long_description=open(os.path.join(here, 'README.rst')).read(), author='Jeff Quast', From e541b17dfd557832fe7d1033a7314a570a19116f Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 24 Aug 2014 01:43:20 -0700 Subject: [PATCH 199/459] v1.9.2: Implement keypad / directional key support This also improves the bin/editor.py to be able to save and make and example the use of these directional keys. Fixes issue #17: Numpad input not recognised --- README.rst | 90 +++++--- bin/dumb_fse.py | 59 ------ bin/editor.py | 221 ++++++++++++++++++++ bin/{test_keyboard_keys.py => keymatrix.py} | 9 +- blessed/keyboard.py | 140 ++++++++----- blessed/terminal.py | 22 ++ blessed/tests/test_core.py | 18 ++ blessed/tests/test_keyboard.py | 78 ++++++- docs/conf.py | 2 +- setup.py | 2 +- 10 files changed, 493 insertions(+), 148 deletions(-) delete mode 100755 bin/dumb_fse.py create mode 100755 bin/editor.py rename bin/{test_keyboard_keys.py => keymatrix.py} (95%) diff --git a/README.rst b/README.rst index f672672d..1cef6c68 100644 --- a/README.rst +++ b/README.rst @@ -591,32 +591,45 @@ has elapsed, an empty string is returned. A timeout value of 0 is nonblocking. keyboard codes ~~~~~~~~~~~~~~ -The return value of the *Terminal* method ``inkey`` may be inspected for ts property -*is_sequence*. When *True*, it means the value is a *multibyte sequence*, -representing an application key of your terminal. - -The *code* property (int) may then be compared with any of the following -attributes of the *Terminal* instance, which are equivalent to the same -available in `curs_getch(3)`_, with the following exceptions: - -* use ``KEY_DELETE`` instead of ``KEY_DC`` (chr(127)) -* use ``KEY_INSERT`` instead of ``KEY_IC`` -* use ``KEY_PGUP`` instead of ``KEY_PPAGE`` -* use ``KEY_PGDOWN`` instead of ``KEY_NPAGE`` -* use ``KEY_ESCAPE`` instead of ``KEY_EXIT`` -* use ``KEY_SUP`` instead of ``KEY_SR`` (shift + up) -* use ``KEY_SDOWN`` instead of ``KEY_SF`` (shift + down) - -Additionally, use any of the following common attributes: - -* ``KEY_BACKSPACE`` (chr(8)). -* ``KEY_TAB`` (chr(9)). -* ``KEY_DOWN``, ``KEY_UP``, ``KEY_LEFT``, ``KEY_RIGHT``. -* ``KEY_SLEFT`` (shift + left). -* ``KEY_SRIGHT`` (shift + right). -* ``KEY_HOME``, ``KEY_END``. -* ``KEY_F1`` through ``KEY_F22``. - +The return value of the *Terminal* method ``inkey`` is an instance of the +class ``Keystroke``, and may be inspected for its property *is_sequence* +(bool). When *True*, it means the value is a *multibyte sequence*, +representing a special non-alphanumeric key of your keyboard. + +The *code* property (int) may then be compared with attributes of the +*Terminal* instance, which are equivalent to the same of those listed +by `curs_getch(3)`_ or the curses_ module, with the following helpful +aliases: + +* use ``KEY_DELETE`` for ``KEY_DC`` (chr(127)). +* use ``KEY_TAB`` for chr(9). +* use ``KEY_INSERT`` for ``KEY_IC``. +* use ``KEY_PGUP`` for ``KEY_PPAGE``. +* use ``KEY_PGDOWN`` for ``KEY_NPAGE``. +* use ``KEY_ESCAPE`` for ``KEY_EXIT``. +* use ``KEY_SUP`` for ``KEY_SR`` (shift + up). +* use ``KEY_SDOWN`` for ``KEY_SF`` (shift + down). +* use ``KEY_DOWN_LEFT`` for ``KEY_C1`` (keypad lower-left). +* use ``KEY_UP_RIGHT`` for ``KEY_A1`` (keypad upper-left). +* use ``KEY_DOWN_RIGHT`` for ``KEY_C3`` (keypad lower-left). +* use ``KEY_UP_RIGHT`` for ``KEY_A3`` (keypad lower-right). +* use ``KEY_CENTER`` for ``KEY_B2`` (keypad center). +* use ``KEY_BEGIN`` for ``KEY_BEG``. + +The *name* property of the return value of ``inkey()`` will prefer +these aliases over the built-in curses_ names. + +The following are not available in the curses_ module, but provided +for distinguishing a keypress of those keypad keys where num lock is +enabled and the ``keypad()`` context manager is used: + +* ``KEY_KP_MULTIPLY`` +* ``KEY_KP_ADD`` +* ``KEY_KP_SEPARATOR`` +* ``KEY_KP_SUBTRACT`` +* ``KEY_KP_DECIMAL`` +* ``KEY_KP_DIVIDE`` +* ``KEY_KP_0`` through ``KEY_KP_9`` Shopping List ============= @@ -644,8 +657,10 @@ detail and behavior in edge cases make a difference. Here are some ways Blessed does not provide... -* Native color support on the Windows command prompt. However, it should work - when used in concert with colorama_. Patches welcome! +* Native color support on the Windows command prompt. A PDCurses_ build + of python for windows provides only partial support at this time -- there + are plans to merge with the ansi_ module in concert with colorama_ to + resolve this. Patches welcome! Devlopers, Bugs @@ -656,7 +671,9 @@ Bugs or suggestions? Visit the `issue tracker`_. For patches, please construct a test case if possible. -To test, install and execute python package command ``tox``. +To test, execute ``./setup.py develop`` followed by command ``tox``. + +Pull requests are tested by Travis-CI. License @@ -671,6 +688,11 @@ Version History 1.9 * workaround: ignore 'tparm() returned NULL', this occurs on win32 platforms using PDCurses_ where tparm() is not implemented. + * enhancement: new context manager ``keypad()``, which enables + keypad application keys such as the diagonal keys on the numpad. + * bugfix: translate keypad application keys correctly to their + diagonal movement directions ``KEY_LL``, ``KEY_LR``, ``KEY_UL``, + ``KEY_LR``, and ``KEY_CENTER``. 1.8 * enhancement: export keyboard-read function as public method ``getch()``, @@ -689,7 +711,7 @@ Version History * enhancement: better support for detecting the length or sequences of externally-generated *ecma-48* codes when using ``xterm`` or ``aixterm``. * bugfix: if ``locale.getpreferredencoding()`` returns empty string or an - encoding that is not a valid codec for ``codecs.getincrementaldecoder``, + encoding that is not a valid encoding for ``codecs.getincrementaldecoder``, fallback to ascii and emit a warning. * bugfix: ensure ``FormattingString`` and ``ParameterizingString`` may be pickled. @@ -703,13 +725,13 @@ Version History * Forked github project `erikrose/blessings`_ to `jquast/blessed`_, this project was previously known as **blessings** version 1.6 and prior. * introduced: context manager ``cbreak()`` and ``raw()``, which is equivalent - to ``tty.setcbreak()`` and ``tty.setraw()``, allowing input from stdin to be - read as each key is pressed. + to ``tty.setcbreak()`` and ``tty.setraw()``, allowing input from stdin to + be read as each key is pressed. * introduced: ``inkey()`` and ``kbhit()``, which will return 1 or more characters as a unicode sequence, with attributes ``.code`` and ``.name``, with value non-``None`` when a multibyte sequence is received, allowing - application keys (such as UP/DOWN) to be detected. Optional value ``timeout`` - allows timed asynchronous polling or blocking. + application keys (such as UP/DOWN) to be detected. Optional value + ``timeout`` allows timed asynchronous polling or blocking. * introduced: ``center()``, ``rjust()``, ``ljust()``, ``strip()``, and ``strip_seqs()`` methods. Allows text containing sequences to be aligned to screen, or ``width`` specified. diff --git a/bin/dumb_fse.py b/bin/dumb_fse.py deleted file mode 100755 index 462e3d15..00000000 --- a/bin/dumb_fse.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python -# Dumb full-screen editor. It doesn't save anything but to the screen. -# -# "Why wont python let me read memory -# from screen like assembler? That's dumb." -hellbeard -from __future__ import division, print_function -import collections -import functools -from blessed import Terminal - -echo_xy = lambda cursor, text: functools.partial( - print, end='', flush=True)(cursor.term.move(cursor.y, cursor.x) + text) - -Cursor = collections.namedtuple('Point', ('y', 'x', 'term')) - -above = lambda b, n: Cursor( - max(0, b.y - n), b.x, b.term) -below = lambda b, n: Cursor( - min(b.term.height - 1, b.y + n), b.x, b.term) -right_of = lambda b, n: Cursor( - b.y, min(b.term.width - 1, b.x + n), b.term) -left_of = lambda b, n: Cursor( - b.y, max(0, b.x - n), b.term) -home = lambda b: Cursor( - b.y, 1, b.term) - -lookup_move = lambda inp_code, b: { - # arrows - b.term.KEY_LEFT: left_of(b, 1), - b.term.KEY_RIGHT: right_of(b, 1), - b.term.KEY_DOWN: below(b, 1), - b.term.KEY_UP: above(b, 1), - # shift + arrows - b.term.KEY_SLEFT: left_of(b, 10), - b.term.KEY_SRIGHT: right_of(b, 10), - b.term.KEY_SDOWN: below(b, 10), - b.term.KEY_SUP: above(b, 10), - # carriage return - b.term.KEY_ENTER: home(below(b, 1)), - b.term.KEY_HOME: home(b), -}.get(inp_code, b) - -term = Terminal() -csr = Cursor(1, 1, term) -with term.hidden_cursor(), term.raw(), term.location(), term.fullscreen(): - inp = None - while True: - echo_xy(csr, term.reverse(u' ')) - inp = term.inkey() - if inp.code == term.KEY_ESCAPE or inp == chr(3): - break - echo_xy(csr, u' ') - n_csr = lookup_move(inp.code, csr) - if n_csr != csr: - echo_xy(n_csr, u' ') - csr = n_csr - elif not inp.is_sequence: - echo_xy(csr, inp) - csr = right_of(csr, 1) diff --git a/bin/editor.py b/bin/editor.py new file mode 100755 index 00000000..4dff2b70 --- /dev/null +++ b/bin/editor.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 +# Dumb full-screen editor. It doesn't save anything but to the screen. +# +# "Why wont python let me read memory +# from screen like assembler? That's dumb." -hellbeard +# +# This program makes example how to deal with a keypad for directional +# movement, with both numlock on and off. +from __future__ import division, print_function +import collections +import functools +from blessed import Terminal + +echo = lambda text: ( + functools.partial(print, end='', flush=True)(text)) + +echo_yx = lambda cursor, text: ( + echo(cursor.term.move(cursor.y, cursor.x) + text)) + +Cursor = collections.namedtuple('Point', ('y', 'x', 'term')) + +above = lambda csr, n: ( + Cursor(y=max(0, csr.y - n), + x=csr.x, + term=csr.term)) + +below = lambda csr, n: ( + Cursor(y=min(csr.term.height - 1, csr.y + n), + x=csr.x, + term=csr.term)) + +right_of = lambda csr, n: ( + Cursor(y=csr.y, + x=min(csr.term.width - 1, csr.x + n), + term=csr.term)) + +left_of = lambda csr, n: ( + Cursor(y=csr.y, + x=max(0, csr.x - n), + term=csr.term)) + +home = lambda csr: ( + Cursor(y=csr.y, + x=0, + term=csr.term)) + +end = lambda csr: ( + Cursor(y=csr.y, + x=csr.term.width - 1, + term=csr.term)) + +bottom = lambda csr: ( + Cursor(y=csr.term.height - 1, + x=csr.x, + term=csr.term)) + +top = lambda csr: ( + Cursor(y=1, + x=csr.x, + term=csr.term)) + +center = lambda csr: Cursor( + csr.term.height // 2, + csr.term.width // 2, + csr.term) + + +lookup_move = lambda inp_code, csr, term: { + # arrows, including angled directionals + csr.term.KEY_END: below(left_of(csr, 1), 1), + csr.term.KEY_KP_1: below(left_of(csr, 1), 1), + + csr.term.KEY_DOWN: below(csr, 1), + csr.term.KEY_KP_2: below(csr, 1), + + csr.term.KEY_PGDOWN: below(right_of(csr, 1), 1), + csr.term.KEY_LR: below(right_of(csr, 1), 1), + csr.term.KEY_KP_3: below(right_of(csr, 1), 1), + + csr.term.KEY_LEFT: left_of(csr, 1), + csr.term.KEY_KP_4: left_of(csr, 1), + + csr.term.KEY_CENTER: center(csr), + csr.term.KEY_KP_5: center(csr), + + csr.term.KEY_RIGHT: right_of(csr, 1), + csr.term.KEY_KP_6: right_of(csr, 1), + + csr.term.KEY_HOME: above(left_of(csr, 1), 1), + csr.term.KEY_KP_7: above(left_of(csr, 1), 1), + + csr.term.KEY_UP: above(csr, 1), + csr.term.KEY_KP_8: above(csr, 1), + + csr.term.KEY_PGUP: above(right_of(csr, 1), 1), + csr.term.KEY_KP_9: above(right_of(csr, 1), 1), + + # shift + arrows + csr.term.KEY_SLEFT: left_of(csr, 10), + csr.term.KEY_SRIGHT: right_of(csr, 10), + csr.term.KEY_SDOWN: below(csr, 10), + csr.term.KEY_SUP: above(csr, 10), + + # carriage return + csr.term.KEY_ENTER: home(below(csr, 1)), +}.get(inp_code, csr) + + +def readline(term, width=20): + # a rudimentary readline function + string = u'' + while True: + inp = term.inkey() + if inp.code == term.KEY_ENTER: + break + elif inp.code == term.KEY_ESCAPE or inp == chr(3): + string = None + break + elif not inp.is_sequence and len(string) < width: + string += inp + echo(inp) + elif inp.code in (term.KEY_BACKSPACE, term.KEY_DELETE): + string = string[:-1] + echo('\b \b') + return string + + +def save(screen, fname): + if not fname: + return + with open(fname, 'w') as fp: + cur_row = cur_col = 0 + for (row, col) in sorted(screen): + char = screen[(row, col)] + while row != cur_row: + cur_row += 1 + cur_col = 0 + fp.write(u'\n') + while col > cur_col: + cur_col += 1 + fp.write(u' ') + fp.write(char) + cur_col += 1 + fp.write(u'\n') + + +def redraw(term, screen, start=None, end=None): + if start is None and end is None: + echo(term.clear) + start, end = (Cursor(y=min([y for (y, x) in screen or [(0, 0)]]), + x=min([x for (y, x) in screen or [(0, 0)]]), + term=term), + Cursor(y=max([y for (y, x) in screen or [(0, 0)]]), + x=max([x for (y, x) in screen or [(0, 0)]]), + term=term)) + lastcol, lastrow = -1, -1 + for row, col in sorted(screen): + if (row >= start.y and row <= end.y and + col >= start.x and col <= end.x): + if col >= term.width or row >= term.height: + # out of bounds + continue + if not (row == lastrow and col == lastcol + 1): + # use cursor movement + echo_yx(Cursor(row, col, term), screen[row, col]) + else: + # just write past last one + echo(screen[row, col]) + + +def main(): + term = Terminal() + csr = Cursor(0, 0, term) + screen = {} + with term.hidden_cursor(), \ + term.raw(), \ + term.location(), \ + term.fullscreen(), \ + term.keypad(): + inp = None + while True: + echo_yx(csr, term.reverse(screen.get((csr.y, csr.x), u' '))) + inp = term.inkey() + + if inp == chr(3): + # ^c exits + break + + elif inp == chr(19): + # ^s saves + echo_yx(home(bottom(csr)), + term.ljust(term.bold_white('Filename: '))) + echo_yx(right_of(home(bottom(csr)), len('Filename: ')), u'') + save(screen, readline(term)) + echo_yx(home(bottom(csr)), term.clear_eol) + redraw(term=term, screen=screen, + start=home(bottom(csr)), + end=end(bottom(csr))) + continue + + elif inp == chr(12): + # ^l refreshes + redraw(term=term, screen=screen) + + n_csr = lookup_move(inp.code, csr, term) + if n_csr != csr: + # erase old cursor, + echo_yx(csr, screen.get((csr.y, csr.x), u' ')) + csr = n_csr + + elif not inp.is_sequence and inp.isprintable(): + echo_yx(csr, inp) + screen[(csr.y, csr.x)] = inp.__str__() + n_csr = right_of(csr, 1) + if n_csr == csr: + # wrap around margin + n_csr = home(below(csr, 1)) + csr = n_csr + +if __name__ == '__main__': + main() diff --git a/bin/test_keyboard_keys.py b/bin/keymatrix.py similarity index 95% rename from bin/test_keyboard_keys.py rename to bin/keymatrix.py index af258139..daaad990 100755 --- a/bin/test_keyboard_keys.py +++ b/bin/keymatrix.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +from __future__ import division from blessed import Terminal import sys @@ -31,11 +32,11 @@ def refresh(term, board, level, score, inp): sys.stdout.flush() if bottom >= (term.height - 5): sys.stderr.write( - '\n' * (term.height / 2) + + ('\n' * (term.height // 2)) + term.center(term.red_underline('cheater!')) + '\n') sys.stderr.write( term.center("(use a larger screen)") + - '\n' * (term.height / 2)) + ('\n' * (term.height // 2))) sys.exit(1) for row, inp in enumerate(inps[(term.height - (bottom + 2)) * -1:]): sys.stdout.write(term.move(bottom + row+1)) @@ -72,9 +73,9 @@ def add_score(score, pts, level): gb = build_gameboard(term) inps = [] - with term.raw(): + with term.raw(), term.keypad(), term.location(): inp = term.inkey(timeout=0) - while inp.upper() != 'Q': + while inp != chr(3): if dirty: refresh(term, gb, level, score, inps) dirty = False diff --git a/blessed/keyboard.py b/blessed/keyboard.py index 0d60582b..ec02e219 100644 --- a/blessed/keyboard.py +++ b/blessed/keyboard.py @@ -11,7 +11,7 @@ if hasattr(collections, 'OrderedDict'): OrderedDict = collections.OrderedDict else: - # python 2.6 + # python 2.6 requires 3rd party library import ordereddict OrderedDict = ordereddict.OrderedDict @@ -21,9 +21,34 @@ if keyname.startswith('KEY_')) ) -# Inject missing KEY_TAB -if not hasattr(curses, 'KEY_TAB'): - curses.KEY_TAB = max(get_curses_keycodes().values()) + 1 +# override a few curses constants with easier mnemonics, +# there may only be a 1:1 mapping, so for those who desire +# to use 'KEY_DC' from, perhaps, ported code, recommend +# that they simply compare with curses.KEY_DC. +CURSES_KEYCODE_OVERRIDE_MIXIN = ( + ('KEY_DELETE', curses.KEY_DC), + ('KEY_INSERT', curses.KEY_IC), + ('KEY_PGUP', curses.KEY_PPAGE), + ('KEY_PGDOWN', curses.KEY_NPAGE), + ('KEY_ESCAPE', curses.KEY_EXIT), + ('KEY_SUP', curses.KEY_SR), + ('KEY_SDOWN', curses.KEY_SF), + ('KEY_UP_LEFT', curses.KEY_A1), + ('KEY_UP_RIGHT', curses.KEY_A3), + ('KEY_CENTER', curses.KEY_B2), + ('KEY_BEGIN', curses.KEY_BEG), +) + +# Inject KEY_{names} that we think would be useful, there are no curses +# definitions for the keypad keys. We need keys that generate multibyte +# sequences, though it is useful to have some aliases for basic control +# characters such as TAB. +_lastval = max(get_curses_keycodes().values()) +for key in ('TAB', 'KP_MULTIPLY', 'KP_ADD', 'KP_SEPARATOR', 'KP_SUBTRACT', + 'KP_DECIMAL', 'KP_DIVIDE', 'KP_EQUAL', 'KP_0', 'KP_1', 'KP_2', + 'KP_3', 'KP_4', 'KP_5', 'KP_6', 'KP_7', 'KP_8', 'KP_9'): + _lastval += 1 + setattr(curses, 'KEY_{0}'.format(key), _lastval) class Keystroke(unicode): @@ -156,27 +181,22 @@ def resolve_sequence(text, mapper, codes): return Keystroke(ucs=sequence, code=code, name=codes[code]) return Keystroke(ucs=text and text[0] or u'') -# override a few curses constants with easier mnemonics, -# there may only be a 1:1 mapping, so for those who desire -# to use 'KEY_DC' from, perhaps, ported code, recommend -# that they simply compare with curses.KEY_DC. -CURSES_KEYCODE_OVERRIDE_MIXIN = ( - ('KEY_DELETE', curses.KEY_DC), - ('KEY_INSERT', curses.KEY_IC), - ('KEY_PGUP', curses.KEY_PPAGE), - ('KEY_PGDOWN', curses.KEY_NPAGE), - ('KEY_ESCAPE', curses.KEY_EXIT), - ('KEY_SUP', curses.KEY_SR), - ('KEY_SDOWN', curses.KEY_SF), -) - """In a perfect world, terminal emulators would always send exactly what the terminfo(5) capability database plans for them, accordingly by the value of the ``TERM`` name they declare. But this isn't a perfect world. Many vt220-derived terminals, such as those declaring 'xterm', will continue to send vt220 codes instead of -their native-declared codes. This goes for many: rxvt, putty, iTerm.""" +their native-declared codes, for backwards-compatibility. + +This goes for many: rxvt, putty, iTerm. + +These "mixins" are used for *all* terminals, regardless of their type. + +Furthermore, curses does not provide sequences sent by the keypad, +at least, it does not provide a way to distinguish between keypad 0 +and numeric 0. +""" DEFAULT_SEQUENCE_MIXIN = ( # these common control characters (and 127, ctrl+'?') mapped to # an application key definition. @@ -186,38 +206,64 @@ def resolve_sequence(text, mapper, codes): (unichr(9), curses.KEY_TAB), (unichr(27), curses.KEY_EXIT), (unichr(127), curses.KEY_DC), - # vt100 application keys are still sent by xterm & friends, even if - # their reports otherwise; possibly, for compatibility reasons? - (u"\x1bOA", curses.KEY_UP), - (u"\x1bOB", curses.KEY_DOWN), - (u"\x1bOC", curses.KEY_RIGHT), - (u"\x1bOD", curses.KEY_LEFT), - (u"\x1bOH", curses.KEY_HOME), - (u"\x1bOF", curses.KEY_END), - (u"\x1bOP", curses.KEY_F1), - (u"\x1bOQ", curses.KEY_F2), - (u"\x1bOR", curses.KEY_F3), - (u"\x1bOS", curses.KEY_F4), - # typical for vt220-derived terminals, just in case our terminal - # database reported something different, + (u"\x1b[A", curses.KEY_UP), (u"\x1b[B", curses.KEY_DOWN), (u"\x1b[C", curses.KEY_RIGHT), (u"\x1b[D", curses.KEY_LEFT), - (u"\x1b[U", curses.KEY_NPAGE), - (u"\x1b[V", curses.KEY_PPAGE), - (u"\x1b[H", curses.KEY_HOME), (u"\x1b[F", curses.KEY_END), + (u"\x1b[H", curses.KEY_HOME), + # not sure where these are from .. please report (u"\x1b[K", curses.KEY_END), - # atypical, - # (u"\x1bA", curses.KEY_UP), - # (u"\x1bB", curses.KEY_DOWN), - # (u"\x1bC", curses.KEY_RIGHT), - # (u"\x1bD", curses.KEY_LEFT), - # rxvt, - (u"\x1b?r", curses.KEY_DOWN), - (u"\x1b?x", curses.KEY_UP), - (u"\x1b?v", curses.KEY_RIGHT), - (u"\x1b?t", curses.KEY_LEFT), - (u"\x1b[@", curses.KEY_IC), + (u"\x1b[U", curses.KEY_NPAGE), + (u"\x1b[V", curses.KEY_PPAGE), + + # keys sent after term.smkx (keypad_xmit) is emitted, source: + # http://www.xfree86.org/current/ctlseqs.html#PC-Style%20Function%20Keys + # http://fossies.org/linux/rxvt/doc/rxvtRef.html#KeyCodes + # + # keypad, numlock on + (u"\x1bOM", curses.KEY_ENTER), # return + (u"\x1bOj", curses.KEY_KP_MULTIPLY), # * + (u"\x1bOk", curses.KEY_KP_ADD), # + + (u"\x1bOl", curses.KEY_KP_SEPARATOR), # , + (u"\x1bOm", curses.KEY_KP_SUBTRACT), # - + (u"\x1bOn", curses.KEY_KP_DECIMAL), # . + (u"\x1bOo", curses.KEY_KP_DIVIDE), # / + (u"\x1bOX", curses.KEY_KP_EQUAL), # = + (u"\x1bOp", curses.KEY_KP_0), # 0 + (u"\x1bOq", curses.KEY_KP_1), # 1 + (u"\x1bOr", curses.KEY_KP_2), # 2 + (u"\x1bOs", curses.KEY_KP_3), # 3 + (u"\x1bOt", curses.KEY_KP_4), # 4 + (u"\x1bOu", curses.KEY_KP_5), # 5 + (u"\x1bOv", curses.KEY_KP_6), # 6 + (u"\x1bOw", curses.KEY_KP_7), # 7 + (u"\x1bOx", curses.KEY_KP_8), # 8 + (u"\x1bOy", curses.KEY_KP_9), # 9 + + # + # keypad, numlock off + (u"\x1b[1~", curses.KEY_FIND), # find + (u"\x1b[2~", curses.KEY_IC), # insert (0) + (u"\x1b[3~", curses.KEY_DC), # delete (.), "Execute" + (u"\x1b[4~", curses.KEY_SELECT), # select + (u"\x1b[5~", curses.KEY_PPAGE), # pgup (9) + (u"\x1b[6~", curses.KEY_NPAGE), # pgdown (3) + (u"\x1b[7~", curses.KEY_HOME), # home + (u"\x1b[8~", curses.KEY_END), # end + (u"\x1b[OA", curses.KEY_UP), # up (8) + (u"\x1b[OB", curses.KEY_DOWN), # down (2) + (u"\x1b[OC", curses.KEY_RIGHT), # right (6) + (u"\x1b[OD", curses.KEY_LEFT), # left (4) + (u"\x1b[OF", curses.KEY_END), # end (1) + (u"\x1b[OH", curses.KEY_HOME), # home (7) + + # The vt220 placed F1-F4 above the keypad, in place of actual + # F1-F4 were local functions (hold screen, print screen, + # set up, data/talk, break). + (u"\x1bOP", curses.KEY_F1), + (u"\x1bOQ", curses.KEY_F2), + (u"\x1bOR", curses.KEY_F3), + (u"\x1bOS", curses.KEY_F4), ) diff --git a/blessed/terminal.py b/blessed/terminal.py index d1db30aa..478c559f 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -640,6 +640,28 @@ def raw(self): else: yield + @contextlib.contextmanager + def keypad(self): + """ + Context manager that enables keypad input (*keyboard_transmit* mode). + + This enables the effect of calling the curses function keypad(3x): + display terminfo(5) capability `keypad_xmit` (smkx) upon entering, + and terminfo(5) capability `keypad_local` (rmkx) upon exiting. + + On an IBM-PC keypad of ttype *xterm*, with numlock off, the + lower-left diagonal key transmits sequence ``\\x1b[F``, ``KEY_END``. + + However, upon entering keypad mode, ``\\x1b[OF`` is transmitted, + translating to ``KEY_LL`` (lower-left key), allowing diagonal + direction keys to be determined. + """ + try: + self.stream.write(self.smkx) + yield + finally: + self.stream.write(self.rmkx) + def inkey(self, timeout=None, esc_delay=0.35, _intr_continue=True): """T.inkey(timeout=None, [esc_delay, [_intr_continue]]) -> Keypress() diff --git a/blessed/tests/test_core.py b/blessed/tests/test_core.py index 8cfe281e..08f2560b 100644 --- a/blessed/tests/test_core.py +++ b/blessed/tests/test_core.py @@ -59,6 +59,24 @@ def child(kind): child(all_terms) +def test_yield_keypad(): + "Ensure ``keypad()`` writes keyboard_xmit and keyboard_local." + @as_subprocess + def child(kind): + # given, + t = TestTerminal(stream=StringIO(), force_styling=True) + expected_output = u''.join((t.smkx, t.rmkx)) + + # exercise, + with t.keypad(): + pass + + # verify. + assert (t.stream.getvalue() == expected_output) + + child(kind='xterm') + + def test_null_fileno(): "Make sure ``Terminal`` works when ``fileno`` is ``None``." @as_subprocess diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index b4b5eb7b..6645ac20 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- "Tests for keyboard support." +import functools import tempfile import StringIO import signal @@ -269,7 +270,7 @@ def child(): def test_inkey_0s_cbreak_noinput_nokb(): - "0-second inkey without input or keyboard." + "0-second inkey without data in input stream and no keyboard/tty." @as_subprocess def child(): term = TestTerminal(stream=StringIO.StringIO()) @@ -549,7 +550,7 @@ def test_esc_delay_cbreak_035(): assert key_name == u'KEY_ESCAPE' assert os.WEXITSTATUS(status) == 0 assert math.floor(time.time() - stime) == 0.0 - assert 35 <= int(duration_ms) <= 45, duration_ms + assert 34 <= int(duration_ms) <= 45, duration_ms def test_esc_delay_cbreak_135(): @@ -819,3 +820,76 @@ def test_resolve_sequence(): assert ks.code is 6 assert ks.is_sequence is True assert repr(ks) in (u"KEY_L", "KEY_L") + + +def test_keypad_mixins_and_aliases(): + """ Test PC-Style function key translations when in ``keypad`` mode.""" + # Key plain app modified + # Up ^[[A ^[OA ^[[1;mA + # Down ^[[B ^[OB ^[[1;mB + # Right ^[[C ^[OC ^[[1;mC + # Left ^[[D ^[OD ^[[1;mD + # End ^[[F ^[OF ^[[1;mF + # Home ^[[H ^[OH ^[[1;mH + @as_subprocess + def child(kind): + term = TestTerminal(kind=kind, force_styling=True) + from blessed.keyboard import resolve_sequence + + resolve = functools.partial(resolve_sequence, + mapper=term._keymap, + codes=term._keycodes) + + assert resolve(unichr(10)).name == "KEY_ENTER" + assert resolve(unichr(13)).name == "KEY_ENTER" + assert resolve(unichr(8)).name == "KEY_BACKSPACE" + assert resolve(unichr(9)).name == "KEY_TAB" + assert resolve(unichr(27)).name == "KEY_ESCAPE" + assert resolve(unichr(127)).name == "KEY_DELETE" + assert resolve(u"\x1b[A").name == "KEY_UP" + assert resolve(u"\x1b[B").name == "KEY_DOWN" + assert resolve(u"\x1b[C").name == "KEY_RIGHT" + assert resolve(u"\x1b[D").name == "KEY_LEFT" + assert resolve(u"\x1b[U").name == "KEY_PGDOWN" + assert resolve(u"\x1b[V").name == "KEY_PGUP" + assert resolve(u"\x1b[H").name == "KEY_HOME" + assert resolve(u"\x1b[F").name == "KEY_END" + assert resolve(u"\x1b[K").name == "KEY_END" + assert resolve(u"\x1bOM").name == "KEY_ENTER" + assert resolve(u"\x1bOj").name == "KEY_KP_MULTIPLY" + assert resolve(u"\x1bOk").name == "KEY_KP_ADD" + assert resolve(u"\x1bOl").name == "KEY_KP_SEPARATOR" + assert resolve(u"\x1bOm").name == "KEY_KP_SUBTRACT" + assert resolve(u"\x1bOn").name == "KEY_KP_DECIMAL" + assert resolve(u"\x1bOo").name == "KEY_KP_DIVIDE" + assert resolve(u"\x1bOX").name == "KEY_KP_EQUAL" + assert resolve(u"\x1bOp").name == "KEY_KP_0" + assert resolve(u"\x1bOq").name == "KEY_KP_1" + assert resolve(u"\x1bOr").name == "KEY_KP_2" + assert resolve(u"\x1bOs").name == "KEY_KP_3" + assert resolve(u"\x1bOt").name == "KEY_KP_4" + assert resolve(u"\x1bOu").name == "KEY_KP_5" + assert resolve(u"\x1bOv").name == "KEY_KP_6" + assert resolve(u"\x1bOw").name == "KEY_KP_7" + assert resolve(u"\x1bOx").name == "KEY_KP_8" + assert resolve(u"\x1bOy").name == "KEY_KP_9" + assert resolve(u"\x1b[1~").name == "KEY_FIND" + assert resolve(u"\x1b[2~").name == "KEY_INSERT" + assert resolve(u"\x1b[3~").name == "KEY_DELETE" + assert resolve(u"\x1b[4~").name == "KEY_SELECT" + assert resolve(u"\x1b[5~").name == "KEY_PGUP" + assert resolve(u"\x1b[6~").name == "KEY_PGDOWN" + assert resolve(u"\x1b[7~").name == "KEY_HOME" + assert resolve(u"\x1b[8~").name == "KEY_END" + assert resolve(u"\x1b[OA").name == "KEY_UP" + assert resolve(u"\x1b[OB").name == "KEY_DOWN" + assert resolve(u"\x1b[OC").name == "KEY_RIGHT" + assert resolve(u"\x1b[OD").name == "KEY_LEFT" + assert resolve(u"\x1b[OF").name == "KEY_END" + assert resolve(u"\x1b[OH").name == "KEY_HOME" + assert resolve(u"\x1bOP").name == "KEY_F1" + assert resolve(u"\x1bOQ").name == "KEY_F2" + assert resolve(u"\x1bOR").name == "KEY_F3" + assert resolve(u"\x1bOS").name == "KEY_F4" + + child('xterm') diff --git a/docs/conf.py b/docs/conf.py index ca3dcdaa..20c2a233 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -53,7 +53,7 @@ # built documents. # # The short X.Y version. -version = '1.9.1' +version = '1.9.2' # The full version, including alpha/beta/rc tags. release = version diff --git a/setup.py b/setup.py index e87e1b4f..faae3313 100755 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ def main(): here = os.path.dirname(__file__) setuptools.setup( name='blessed', - version='1.9.1', + version='1.9.2', description="A feature-filled fork of Erik Rose's blessings project", long_description=open(os.path.join(here, 'README.rst')).read(), author='Jeff Quast', From 1c81bfa553ea4eb7341bf16390cd732e75c1ee2d Mon Sep 17 00:00:00 2001 From: jquast Date: Sun, 24 Aug 2014 02:26:27 -0700 Subject: [PATCH 200/459] README updates --- README.rst | 103 ++++++++++++++++++++++++++++------------------------- 1 file changed, 54 insertions(+), 49 deletions(-) diff --git a/README.rst b/README.rst index 1cef6c68..6be8a312 100644 --- a/README.rst +++ b/README.rst @@ -255,11 +255,11 @@ when used as a callable, without any video attributes. If the **TERM** defines a terminal that does support colors, but actually does not, they are usually harmless. -Colorless terminals, such as the amber or monochrome *vt220*, do not support -colors but do support reverse video. For this reason, it may be desirable in -some applications, such as a selection bar, to simply select a foreground -color, followed by reverse video to achieve the desired background color -effect:: +Colorless terminals, such as the amber or green monochrome *vt220*, do not +support colors but do support reverse video. For this reason, it may be +desirable in some applications, such as a selection bar, to simply select +a foreground color, followed by reverse video to achieve the desired +background color effect:: from blessed import Terminal @@ -269,13 +269,15 @@ effect:: standout=term.green_reverse('standout'))) Which appears as *bright white on green* on color terminals, or *black text -on amber or green* on monochrome terminals. You can check whether the terminal -definition used supports colors, and how many, using the ``number_of_colors`` -property, which returns any of *0* *8* or *256* for terminal types -such as *vt220*, *ansi*, and *xterm-256color*, respectively. +on amber or green* on monochrome terminals. -**NOTE**: On most color terminals, *bright_black* is actually a very dark -shade of gray! +You can check whether the terminal definition used supports colors, and how +many, using the ``number_of_colors`` property, which returns any of *0*, +*8* or *256* for terminal types such as *vt220*, *ansi*, and +*xterm-256color*, respectively. + +**NOTE**: On most color terminals, unlink *black*, *bright_black* is not +invisible -- it is actually a very dark shade of gray! Compound Formatting ------------------- @@ -320,15 +322,19 @@ Moving The Cursor ----------------- When you want to move the cursor, you have a few choices, the -``location(y=None, x=None)`` context manager, ``move(y, x)``, ``move_y(row)``, +``location(x=None, y=None)`` context manager, ``move(y, x)``, ``move_y(row)``, and ``move_x(col)`` attributes. +**NOTE**: The ``location()`` method receives arguments in form of *(x, y)*, +whereas the ``move()`` argument receives arguments in form of *(y, x)*. This +is a flaw in the original `erikrose/blessings`_ implementation, but remains +for compatibility. Moving Temporarily ~~~~~~~~~~~~~~~~~~ -A context manager, ``location`` is provided to move the cursor to a *(x, y)* -screen position and restore the previous position upon exit:: +A context manager, ``location()`` is provided to move the cursor to an +*(x, y)* screen position and restore the previous position upon exit:: from blessed import Terminal @@ -337,7 +343,7 @@ screen position and restore the previous position upon exit:: print('Here is the bottom.') print('This is back where I came from.') -Parameters to *location()* are **optional** *x* and/or *y*:: +Parameters to ``location()`` are **optional** *x* and/or *y*:: with term.location(y=10): print('We changed just the row.') @@ -348,7 +354,8 @@ When omitted, it saves the cursor position and restore it upon exit:: print(term.move(1, 1) + 'Hi') print(term.move(9, 9) + 'Mom') -*NOTE*: calls to *location* may not be nested, as only one location may be saved. +**NOTE**: calls to ``location()`` may not be nested, as only one location +may be saved. Moving Permanently @@ -369,12 +376,6 @@ this:: ``move_y`` Position the cursor at given vertical column. -*NOTE*: The *location* method receives arguments in form of *(x, y)*, -where the *move* argument receives arguments in form of *(y, x)*. This is a -flaw in the original `erikrose/blessings`_ implementation, kept for -compatibility. - - One-Notch Movement ~~~~~~~~~~~~~~~~~~ @@ -446,12 +447,13 @@ screen (such as your shell prompt) after exiting, you're seeing the There's also a context manager you can use as a shortcut:: + from __future__ import division from blessed import Terminal term = Terminal() with term.fullscreen(): - print(term.move_y(term.height/2) + - term.center('press any key')) + print(term.move_y(term.height // 2) + + term.center('press any key').rstrip()) term.inkey() Pipe Savvy @@ -474,7 +476,7 @@ bars and other frippery and just stick to content:: term = Terminal() if term.does_styling: - with term.location(0, term.height - 1): + with term.location(x=0, y=term.height - 1): print('Progress: [=======> ]') print(term.bold('Important stuff')) @@ -512,13 +514,15 @@ from Tao Te Ching, word-wrapped to 25 columns:: Keyboard Input -------------- -The built-in python *raw_input* function does not return a value until the return -key is pressed, and is not suitable for detecting each individual keypress, much -less arrow or function keys that emit multibyte sequences. Special `termios(4)`_ -routines are required to enter Non-canonical, known in curses as `cbreak(3)`_. -These functions also receive bytes, which must be incrementally decoded to unicode. +The built-in python function ``raw_input`` function does not return a value until +the return key is pressed, and is not suitable for detecting each individual +keypress, much less arrow or function keys that emit multibyte sequences. + +Special `termios(4)`_ routines are required to enter Non-canonical mode, known +in curses as `cbreak(3)`_. When calling read on input stream, only bytes are +received, which must be decoded to unicode. -Blessed handles all of these special cases with the following simple calls. +Blessed handles all of these special cases!! cbreak ~~~~~~ @@ -547,7 +551,7 @@ inkey ~~~~~ The method ``inkey`` resolves many issues with terminal input by returning -a unicode-derived *Keypress* instance. Although its return value may be +a unicode-derived *Keypress* instance. Although its return value may be printed, joined with, or compared to other unicode strings, it also provides the special attributes ``is_sequence`` (bool), ``code`` (int), and ``name`` (str):: @@ -584,21 +588,22 @@ Its output might appear as:: got q. bye! -A *timeout* value of None (default) will block forever. Any other value specifies -the length of time to poll for input, if no input is received after such time -has elapsed, an empty string is returned. A timeout value of 0 is nonblocking. +A ``timeout`` value of *None* (default) will block forever. Any other value +specifies the length of time to poll for input, if no input is received after +such time has elapsed, an empty string is returned. A ``timeout`` value of *0* +is non-blocking. keyboard codes ~~~~~~~~~~~~~~ The return value of the *Terminal* method ``inkey`` is an instance of the -class ``Keystroke``, and may be inspected for its property *is_sequence* -(bool). When *True*, it means the value is a *multibyte sequence*, -representing a special non-alphanumeric key of your keyboard. +class ``Keystroke``, and may be inspected for its property ``is_sequence`` +(bool). When *True*, the value is a **multibyte sequence**, representing +a special non-alphanumeric key of your keyboard. -The *code* property (int) may then be compared with attributes of the -*Terminal* instance, which are equivalent to the same of those listed -by `curs_getch(3)`_ or the curses_ module, with the following helpful +The ``code`` property (int) may then be compared with attributes of +*Terminal*, which are duplicated from those seen in the manpage +`curs_getch(3)`_ or the curses_ module, with the following helpful aliases: * use ``KEY_DELETE`` for ``KEY_DC`` (chr(127)). @@ -619,9 +624,9 @@ aliases: The *name* property of the return value of ``inkey()`` will prefer these aliases over the built-in curses_ names. -The following are not available in the curses_ module, but provided -for distinguishing a keypress of those keypad keys where num lock is -enabled and the ``keypad()`` context manager is used: +The following are **not** available in the curses_ module, but +provided for keypad support, especially where the ``keypad()`` +context manager is used: * ``KEY_KP_MULTIPLY`` * ``KEY_KP_ADD`` @@ -686,13 +691,12 @@ shares the same. See the LICENSE file. Version History =============== 1.9 - * workaround: ignore 'tparm() returned NULL', this occurs on win32 - platforms using PDCurses_ where tparm() is not implemented. + * workaround: ignore curses.error 'tparm() returned NULL', this occurs + on win32 platforms using PDCurses_ where ``tparm()`` is not + implemented. * enhancement: new context manager ``keypad()``, which enables keypad application keys such as the diagonal keys on the numpad. - * bugfix: translate keypad application keys correctly to their - diagonal movement directions ``KEY_LL``, ``KEY_LR``, ``KEY_UL``, - ``KEY_LR``, and ``KEY_CENTER``. + * bugfix: translate keypad application keys correctly. 1.8 * enhancement: export keyboard-read function as public method ``getch()``, @@ -852,3 +856,4 @@ Version History .. _SIGWINCH: https://en.wikipedia.org/wiki/SIGWINCH .. _`API Documentation`: http://blessed.rtfd.org .. _`PDCurses`: http://www.lfd.uci.edu/~gohlke/pythonlibs/#curses +.. _`ansi`: https://github.com/tehmaze/ansi From 6b483e1a6d701bcb33dd668bcf7c5bafe04527cb Mon Sep 17 00:00:00 2001 From: jquast Date: Mon, 1 Sep 2014 15:31:36 -0700 Subject: [PATCH 201/459] Remove the need to use to 2to3 conversion utility blessed is now naturally native for both python 2 and python 3, removing the need to use the '2to3' conversion utility or execute tests in a round-about way for python 3. --- README.rst | 5 +++- blessed/__init__.py | 2 +- blessed/formatters.py | 23 +++++++++++------ blessed/keyboard.py | 24 ++++++++++++------ blessed/sequences.py | 14 ++++++++--- blessed/terminal.py | 6 ++--- blessed/tests/accessories.py | 14 ++++++++--- blessed/tests/test_core.py | 5 +++- blessed/tests/test_keyboard.py | 17 +++++++++---- blessed/tests/test_sequences.py | 2 +- setup.py | 3 --- tox.ini | 44 ++++----------------------------- 12 files changed, 82 insertions(+), 77 deletions(-) diff --git a/README.rst b/README.rst index 6be8a312..e159f910 100644 --- a/README.rst +++ b/README.rst @@ -697,6 +697,9 @@ Version History * enhancement: new context manager ``keypad()``, which enables keypad application keys such as the diagonal keys on the numpad. * bugfix: translate keypad application keys correctly. + * enhancement: no longer depend on the '2to3' tool for python 3 support. + * enhancement: allow ``civis`` and ``cnorm`` (*hide_cursor*, *normal_hide*) + to work with terminal-type *ansi* by emulating support by proxy. 1.8 * enhancement: export keyboard-read function as public method ``getch()``, @@ -704,7 +707,7 @@ Version History * enhancement: allow ``inkey()`` and ``kbhit()`` to return early when interrupted by signal by passing argument ``_intr_continue=False``. * enhancement: allow ``hpa`` and ``vpa`` (*move_x*, *move_y*) to work on - tmux(1) or screen(1) by forcibly emulating their support by a proxy. + tmux(1) or screen(1) by emulating support by proxy. * enhancement: ``setup.py develop`` ensures virtualenv and installs tox, and ``setup.py test`` calls tox. Requires pythons defined by tox.ini. * enhancement: add ``rstrip()`` and ``lstrip()``, strips both sequences diff --git a/blessed/__init__.py b/blessed/__init__.py index e6737e37..a1693a57 100644 --- a/blessed/__init__.py +++ b/blessed/__init__.py @@ -9,6 +9,6 @@ 'support due to http://bugs.python.org/issue10570.') -from terminal import Terminal +from .terminal import Terminal __all__ = ['Terminal'] diff --git a/blessed/formatters.py b/blessed/formatters.py index 157b70c2..f90666f8 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -1,5 +1,6 @@ "This sub-module provides formatting functions." import curses +import sys _derivatives = ('on', 'bright', 'on_bright',) @@ -15,8 +16,14 @@ #: All valid compoundable names. COMPOUNDABLES = (COLORS | _compoundables) +if sys.version_info[0] == 3: + text_type = str + basestring = str +else: + text_type = unicode # noqa -class ParameterizingString(unicode): + +class ParameterizingString(text_type): """A Unicode string which can be called as a parameterizing termcap. For example:: @@ -35,7 +42,7 @@ def __new__(cls, *args): :arg name: name of this terminal capability. """ assert len(args) and len(args) < 4, args - new = unicode.__new__(cls, args[0]) + new = text_type.__new__(cls, args[0]) new._normal = len(args) > 1 and args[1] or u'' new._name = len(args) > 2 and args[2] or u'' return new @@ -74,7 +81,7 @@ def __call__(self, *args): return NullCallableString() -class ParameterizingProxyString(unicode): +class ParameterizingProxyString(text_type): """A Unicode string which can be called to proxy missing termcap entries. For example:: @@ -102,7 +109,7 @@ def __new__(cls, *args): assert len(args) and len(args) < 4, args assert type(args[0]) is tuple, args[0] assert callable(args[0][1]), args[0][1] - new = unicode.__new__(cls, args[0][0]) + new = text_type.__new__(cls, args[0][0]) new._fmt_args = args[0][1] new._normal = len(args) > 1 and args[1] or u'' new._name = len(args) > 2 and args[2] or u'' @@ -135,7 +142,7 @@ def get_proxy_string(term, attr): return None -class FormattingString(unicode): +class FormattingString(text_type): """A Unicode string which can be called using ``text``, returning a new string, ``attr`` + ``text`` + ``normal``:: @@ -150,7 +157,7 @@ def __new__(cls, *args): :arg normal: terminating sequence for this attribute. """ assert 1 <= len(args) <= 2, args - new = unicode.__new__(cls, args[0]) + new = text_type.__new__(cls, args[0]) new._normal = len(args) > 1 and args[1] or u'' return new @@ -165,12 +172,12 @@ def __call__(self, text): return text -class NullCallableString(unicode): +class NullCallableString(text_type): """A dummy callable Unicode to stand in for ``FormattingString`` and ``ParameterizingString`` for terminals that cannot perform styling. """ def __new__(cls): - new = unicode.__new__(cls, u'') + new = text_type.__new__(cls, u'') return new def __call__(self, *args): diff --git a/blessed/keyboard.py b/blessed/keyboard.py index ec02e219..0a3b7416 100644 --- a/blessed/keyboard.py +++ b/blessed/keyboard.py @@ -5,9 +5,11 @@ __all__ = ['Keystroke', 'get_keyboard_codes', 'get_keyboard_sequences'] -import curses import curses.has_key import collections +import curses +import sys + if hasattr(collections, 'OrderedDict'): OrderedDict = collections.OrderedDict else: @@ -50,8 +52,14 @@ _lastval += 1 setattr(curses, 'KEY_{0}'.format(key), _lastval) +if sys.version_info[0] == 3: + text_type = str + unichr = chr +else: + text_type = unicode # noqa + -class Keystroke(unicode): +class Keystroke(text_type): """A unicode-derived class for describing keyboard input returned by the ``inkey()`` method of ``Terminal``, which may, at times, be a multibyte sequence, providing properties ``is_sequence`` as ``True`` @@ -60,7 +68,7 @@ class Keystroke(unicode): such as ``KEY_LEFT``. """ def __new__(cls, ucs='', code=None, name=None): - new = unicode.__new__(cls, ucs) + new = text_type.__new__(cls, ucs) new._name = name new._code = code return new @@ -71,8 +79,8 @@ def is_sequence(self): return self._code is not None def __repr__(self): - return self._name is None and unicode.__repr__(self) or self._name - __repr__.__doc__ = unicode.__doc__ + return self._name is None and text_type.__repr__(self) or self._name + __repr__.__doc__ = text_type.__doc__ @property def name(self): @@ -152,7 +160,7 @@ def get_keyboard_sequences(term): (seq.decode('latin1'), val) for (seq, val) in ( (curses.tigetstr(cap), val) - for (val, cap) in capability_names.iteritems() + for (val, cap) in capability_names.items() ) if seq ) if term.does_styling else ()) @@ -164,7 +172,7 @@ def get_keyboard_sequences(term): # over simple sequences such as ('\x1b', KEY_EXIT). return OrderedDict(( (seq, sequence_map[seq]) for seq in sorted( - sequence_map, key=len, reverse=True))) + sequence_map.keys(), key=len, reverse=True))) def resolve_sequence(text, mapper, codes): @@ -176,7 +184,7 @@ def resolve_sequence(text, mapper, codes): their integer value (260), and ``codes`` is a dict of integer values (260) paired by their mnemonic name, 'KEY_LEFT'. """ - for sequence, code in mapper.iteritems(): + for sequence, code in mapper.items(): if text.startswith(sequence): return Keystroke(ucs=sequence, code=code, name=codes[code]) return Keystroke(ucs=text and text[0] or u'') diff --git a/blessed/sequences.py b/blessed/sequences.py index a08c9261..c838789e 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -11,6 +11,7 @@ import textwrap import warnings import math +import sys import re # 3rd-party @@ -20,6 +21,11 @@ _BINTERM_UNSUPPORTED_MSG = ('sequence-awareness for terminals emitting ' 'binary-packed capabilities are not supported.') +if sys.version_info[0] == 3: + text_type = str +else: + text_type = unicode # noqa + def _merge_sequences(inp): """Merge a list of input sequence patterns for use in a regular expression. @@ -221,7 +227,7 @@ def get_wontmove_sequence_patterns(term): # ( not *exactly* legal, being extra forgiving. ) bna(cap='sgr', nparams=_num) for _num in range(1, 10) # reset_{1,2,3}string: Reset string - ] + map(re.escape, (term.r1, term.r2, term.r3,))) + ] + list(map(re.escape, (term.r1, term.r2, term.r3,)))) def init_sequence_patterns(term): @@ -366,7 +372,7 @@ def _wrap_chunks(self, chunks): SequenceTextWrapper.__doc__ = textwrap.TextWrapper.__doc__ -class Sequence(unicode): +class Sequence(text_type): """ This unicode-derived class understands the effect of escape sequences of printable length, allowing a properly implemented .rjust(), .ljust(), @@ -379,7 +385,7 @@ def __new__(cls, sequence_text, term): :arg sequence_text: A string containing sequences. :arg term: Terminal instance this string was created with. """ - new = unicode.__new__(cls, sequence_text) + new = text_type.__new__(cls, sequence_text) new._term = term return new @@ -518,7 +524,7 @@ def padd(self): """ outp = u'' nxt = 0 - for idx in range(0, unicode.__len__(self)): + for idx in range(0, text_type.__len__(self)): width = horizontal_distance(self[idx:], self._term) if width != 0: nxt = idx + measure_length(self[idx:], self._term) diff --git a/blessed/terminal.py b/blessed/terminal.py index 478c559f..93ab5d84 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -44,20 +44,20 @@ class IOUnsupportedOperation(Exception): InterruptedError = select.error # local imports -from formatters import ( +from .formatters import ( ParameterizingString, NullCallableString, resolve_capability, resolve_attribute, ) -from sequences import ( +from .sequences import ( init_sequence_patterns, SequenceTextWrapper, Sequence, ) -from keyboard import ( +from .keyboard import ( get_keyboard_sequences, get_keyboard_codes, resolve_sequence, diff --git a/blessed/tests/accessories.py b/blessed/tests/accessories.py index 667d3900..6785827e 100644 --- a/blessed/tests/accessories.py +++ b/blessed/tests/accessories.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """Accessories for automated py.test runner.""" +# std from __future__ import with_statement import contextlib import functools @@ -11,10 +12,17 @@ import pty import os +# local from blessed import Terminal +# 3rd import pytest +if sys.version_info[0] == 3: + text_type = str +else: + text_type = unicode # noqa + TestTerminal = functools.partial(Terminal, kind='xterm-256color') SEND_SEMAPHORE = SEMAPHORE = b'SEMAPHORE\n' RECV_SEMAPHORE = b'SEMAPHORE\r\n' @@ -78,7 +86,7 @@ def __call__(self, *args, **kwargs): cov.save() os._exit(0) - exc_output = unicode() + exc_output = text_type() decoder = codecs.getincrementaldecoder(self.encoding)() while True: try: @@ -119,7 +127,7 @@ def read_until_semaphore(fd, semaphore=RECV_SEMAPHORE, # process will read xyz\\r\\n -- this is how pseudo terminals # behave; a virtual terminal requires both carriage return and # line feed, it is only for convenience that \\n does both. - outp = unicode() + outp = text_type() decoder = codecs.getincrementaldecoder(encoding)() semaphore = semaphore.decode('ascii') while not outp.startswith(semaphore): @@ -139,7 +147,7 @@ def read_until_semaphore(fd, semaphore=RECV_SEMAPHORE, def read_until_eof(fd, encoding='utf8'): """Read file descriptor ``fd`` until EOF. Return decoded string.""" decoder = codecs.getincrementaldecoder(encoding)() - outp = unicode() + outp = text_type() while True: try: _exc = os.read(fd, 100) diff --git a/blessed/tests/test_core.py b/blessed/tests/test_core.py index 08f2560b..3e7d53a9 100644 --- a/blessed/tests/test_core.py +++ b/blessed/tests/test_core.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- "Core blessed Terminal() tests." + +# std try: from StringIO import StringIO except ImportError: from io import StringIO - import collections import warnings import platform @@ -13,6 +14,7 @@ import imp import os +# local from accessories import ( as_subprocess, TestTerminal, @@ -20,6 +22,7 @@ all_terms ) +# 3rd party import mock import pytest diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index 6645ac20..3c5d6cb1 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -2,7 +2,11 @@ "Tests for keyboard support." import functools import tempfile -import StringIO +try: + from StringIO import StringIO +except ImportError: + import io + StringIO = io.StringIO import signal import curses import time @@ -27,6 +31,9 @@ import mock +if sys.version_info[0] == 3: + unichr = chr + def test_kbhit_interrupted(): "kbhit() should not be interrupted with a signal handler." @@ -248,7 +255,7 @@ def test_kbhit_no_kb(): "kbhit() always immediately returns False without a keyboard." @as_subprocess def child(): - term = TestTerminal(stream=StringIO.StringIO()) + term = TestTerminal(stream=StringIO()) stime = time.time() assert term.keyboard_fd is None assert term.kbhit(timeout=1.1) is False @@ -273,7 +280,7 @@ def test_inkey_0s_cbreak_noinput_nokb(): "0-second inkey without data in input stream and no keyboard/tty." @as_subprocess def child(): - term = TestTerminal(stream=StringIO.StringIO()) + term = TestTerminal(stream=StringIO()) with term.cbreak(): stime = time.time() inp = term.inkey(timeout=0) @@ -299,7 +306,7 @@ def test_inkey_1s_cbreak_noinput_nokb(): "1-second inkey without input or keyboard." @as_subprocess def child(): - term = TestTerminal(stream=StringIO.StringIO()) + term = TestTerminal(stream=StringIO()) with term.cbreak(): stime = time.time() inp = term.inkey(timeout=1) @@ -752,7 +759,7 @@ def test_get_keyboard_sequence(monkeypatch): term._cub1 = SEQ_ALT_CUB1.decode('latin1') keymap = blessed.keyboard.get_keyboard_sequences(term) - assert keymap.items() == [ + assert list(keymap.items()) == [ (SEQ_LARGE.decode('latin1'), KEY_LARGE), (SEQ_ALT_CUB1.decode('latin1'), curses.KEY_LEFT), (SEQ_ALT_CUF1.decode('latin1'), curses.KEY_RIGHT), diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index 77dbc5f0..ee2c078f 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -59,7 +59,7 @@ def child(): def test_parametrization(): - """Test parametrizing a capability.""" + """Test parameterizing a capability.""" @as_subprocess def child(): assert TestTerminal().cup(3, 4) == unicode_parm('cup', 3, 4) diff --git a/setup.py b/setup.py index faae3313..73fa6151 100755 --- a/setup.py +++ b/setup.py @@ -27,9 +27,6 @@ def main(): if sys.version_info < (2, 7,): extra['install_requires'].extend(['ordereddict>=1.1']) - elif sys.version_info >= (3,): - extra['use_2to3'] = True - here = os.path.dirname(__file__) setuptools.setup( name='blessed', diff --git a/tox.ini b/tox.ini index 9d06f52d..edee73c1 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,9 @@ envlist = py26, [testenv] # for any python, run simple pytest # with pep8 and pyflake checking +usedevelop = True deps = pytest + pytest-cov pytest-pep8 pytest-flakes mock @@ -21,51 +23,15 @@ setenv = PYTHONIOENCODING=UTF8 # run each test twice -- 1. w/o tty commands = /bin/bash -c {envbindir}/py.test -v \ -x --strict --pep8 --flakes \ + --cov-report term-missing \ blessed/tests {posargs} \ < /dev/null 2>&1 | tee /dev/null -# 2. w/tty +# -- 2. w/tty {envbindir}/py.test -v \ -x --strict --pep8 --flakes \ + --cov-report term-missing \ blessed/tests {posargs} -[testenv:py27] -# for python27, measure coverage -usedevelop = True -deps = pytest - pytest-cov - pytest-pep8 - pytest-flakes - mock - -rrequirements.txt - -setenv = PYTHONIOENCODING=UTF8 - -# run each test twice -- 1. w/o tty, -commands = /bin/bash -c {envbindir}/py.test -v \ - -x --strict --pep8 --flakes \ - blessed/tests {posargs} \ - < /dev/null 2>&1 | tee /dev/null -# 2. w/tty, w/coverage - {envbindir}/py.test -v \ - -x --strict --pep8 --flakes \ - --cov-report term-missing \ - --cov blessed {posargs} - -# for python3, test the version of blessed that is *installed*, -# and not from source. This is because we use the 2to3 tool. -# -# some issue with py.test & python 3 does not allow non-tty testing. - -[testenv:py33] -changedir = {toxworkdir} -commands = {envbindir}/py.test -x --strict --pep8 --flakes \ - {envsitepackagesdir}/blessed/tests {posargs} - -[testenv:py34] -changedir = {toxworkdir} -commands = {envbindir}/py.test -x --strict --pep8 --flakes \ - {envsitepackagesdir}/blessed/tests {posargs} - [pytest] # py.test fixtures conflict with pyflakes flakes-ignore = From 35c88ae9fe476eb4653e101fda3eaa7575f2e01b Mon Sep 17 00:00:00 2001 From: jquast Date: Mon, 1 Sep 2014 15:56:37 -0700 Subject: [PATCH 202/459] proxy support for civis and cnorm on 'ansi' terms. Although many 'ansi' emulating terminals support show/hide cursor (fe. SyncTerm), the terminfo db does not contain any entry. Provide proxy support in the same way that move_{x,y} is provided for terminals of type 'screen' --- blessed/formatters.py | 38 ++++++++++++++++++++++----------- blessed/tests/test_sequences.py | 16 ++++++++++++++ 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/blessed/formatters.py b/blessed/formatters.py index f90666f8..a6ed0070 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -128,18 +128,30 @@ def __call__(self, *args): def get_proxy_string(term, attr): - """ Returns an instance of ParameterizingProxyString - for (some kinds) of terminals and attributes. + """ Proxy and return callable StringClass for proxied attributes. + + We know that some kinds of terminal kinds support sequences that the + terminfo database often doesn't report -- such as the 'move_x' attribute + for terminal type 'screen', or 'hide_cursor' for 'ansi'. + + Returns instance of ParameterizingProxyString or NullCallableString. """ - if term._kind == 'screen' and attr in ('hpa', 'vpa'): - if attr == 'hpa': - fmt = u'\x1b[{0}G' - elif attr == 'vpa': - fmt = u'\x1b[{0}d' - fmt_arg = lambda *arg: (arg[0] + 1,) - return ParameterizingProxyString((fmt, fmt_arg), - term.normal, 'hpa') - return None + return { + 'screen': { + # proxy move_x/move_y for 'screen' terminal type. + 'hpa': ParameterizingProxyString( + (u'\x1b[{0}G', lambda *arg: (arg[0] + 1,)), term.normal, attr), + 'vpa': ParameterizingProxyString( + (u'\x1b[{0}d', lambda *arg: (arg[0] + 1,)), term.normal, attr), + }, + 'ansi': { + # proxy show/hide cursor for 'ansi' terminal type. + 'civis': ParameterizingProxyString( + (u'\x1b[?25l', lambda *arg: ()), term.normal, attr), + 'cnorm': ParameterizingProxyString( + (u'\x1b[?25h', lambda *arg: ()), term.normal, attr), + } + }.get(term._kind, {}).get(attr, None) class FormattingString(text_type): @@ -299,8 +311,8 @@ def resolve_attribute(term, attr): return FormattingString(u''.join(resolution), term.normal) else: # and, for special terminals, such as 'screen', provide a Proxy - # ParameterizingString for attributes they do not claim to support, but - # actually do! (such as 'hpa' and 'vpa'). + # ParameterizingString for attributes they do not claim to support, + # but actually do! (such as 'hpa' and 'vpa'). proxy = get_proxy_string(term, term._sugar.get(attr, attr)) if proxy is not None: return proxy diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index ee2c078f..4c86080e 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -240,6 +240,22 @@ def child(kind): child('screen') +def test_inject_civis_and_cnorm_for_ansi(): + """Test injection of cvis attribute for ansi.""" + @as_subprocess + def child(kind): + t = TestTerminal(kind=kind, stream=StringIO(), force_styling=True) + with t.hidden_cursor(): + pass + expected_output = u''.join( + (unicode_cap('sc'), + u'\x1b[?25l\x1b[?25h', + unicode_cap('rc'))) + assert (t.stream.getvalue() == expected_output) + + child('ansi') + + def test_zero_location(all_standard_terms): """Make sure ``location()`` pays attention to 0-valued args.""" @as_subprocess From edaa38bb8f0b1228f1186d15ae268155ce33df2c Mon Sep 17 00:00:00 2001 From: jquast Date: Mon, 1 Sep 2014 16:00:08 -0700 Subject: [PATCH 203/459] release version 1.9.3 --- docs/conf.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 20c2a233..a00a775f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -53,7 +53,7 @@ # built documents. # # The short X.Y version. -version = '1.9.2' +version = '1.9.3' # The full version, including alpha/beta/rc tags. release = version diff --git a/setup.py b/setup.py index 73fa6151..95691ab2 100755 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def main(): here = os.path.dirname(__file__) setuptools.setup( name='blessed', - version='1.9.2', + version='1.9.3', description="A feature-filled fork of Erik Rose's blessings project", long_description=open(os.path.join(here, 'README.rst')).read(), author='Jeff Quast', From dfc1cb42a5e4082df7aa0b298248b52888f37d59 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Tue, 2 Sep 2014 10:25:21 -0700 Subject: [PATCH 204/459] use python2 and 3 compatible print example per @signalpillar --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index e159f910..2f09ce17 100644 --- a/README.rst +++ b/README.rst @@ -90,7 +90,7 @@ The same program with *Blessed* is simply:: term = Terminal() with term.location(0, term.height - 1): - print('This is', term.underline('pretty!')) + print('This is' + term.underline('pretty!')) Screenshots From 9b8cbbadcf61b612d64f4470ef668c84148547c2 Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 6 Sep 2014 13:32:38 -0700 Subject: [PATCH 205/459] Fixes move_x() for 'screen-256color' --- blessed/formatters.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/blessed/formatters.py b/blessed/formatters.py index a6ed0070..7ba844d8 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -136,6 +136,12 @@ def get_proxy_string(term, attr): Returns instance of ParameterizingProxyString or NullCallableString. """ + + # normalize 'screen-256color', or 'ansi.sys' to its basic names + _normalize = ('screen', 'ansi',) + term = next(iter(_simp for _simp in _normalize + if term.startswith(_simp)), term) + return { 'screen': { # proxy move_x/move_y for 'screen' terminal type. From 64d1e7b2449ae123be7de25833a3ea6d7f723413 Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 6 Sep 2014 13:38:48 -0700 Subject: [PATCH 206/459] Release 1.9.4 with additional tests --- blessed/tests/test_sequences.py | 3 +++ docs/conf.py | 2 +- setup.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index 4c86080e..895e118f 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -221,6 +221,7 @@ def child(kind): assert (t.stream.getvalue() == expected_output) child('screen') + child('screen-256color') def test_inject_move_y_for_screen(): @@ -238,6 +239,7 @@ def child(kind): assert (t.stream.getvalue() == expected_output) child('screen') + child('screen-256color') def test_inject_civis_and_cnorm_for_ansi(): @@ -254,6 +256,7 @@ def child(kind): assert (t.stream.getvalue() == expected_output) child('ansi') + child('ansi.sys') def test_zero_location(all_standard_terms): diff --git a/docs/conf.py b/docs/conf.py index a00a775f..4ab6f413 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -53,7 +53,7 @@ # built documents. # # The short X.Y version. -version = '1.9.3' +version = '1.9.4' # The full version, including alpha/beta/rc tags. release = version diff --git a/setup.py b/setup.py index 95691ab2..15ed551f 100755 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def main(): here = os.path.dirname(__file__) setuptools.setup( name='blessed', - version='1.9.3', + version='1.9.4', description="A feature-filled fork of Erik Rose's blessings project", long_description=open(os.path.join(here, 'README.rst')).read(), author='Jeff Quast', From 9eb00f05eae3d0c924be997d6aad0719226a1aaf Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 6 Sep 2014 13:44:27 -0700 Subject: [PATCH 207/459] Introduce new terminal property, ``kind`` --- README.rst | 1 + blessed/formatters.py | 4 ++-- blessed/sequences.py | 2 +- blessed/terminal.py | 5 +++++ blessed/tests/test_core.py | 2 +- 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 2f09ce17..7949ded9 100644 --- a/README.rst +++ b/README.rst @@ -700,6 +700,7 @@ Version History * enhancement: no longer depend on the '2to3' tool for python 3 support. * enhancement: allow ``civis`` and ``cnorm`` (*hide_cursor*, *normal_hide*) to work with terminal-type *ansi* by emulating support by proxy. + * enhancement: new public attribute: ``kind``. 1.8 * enhancement: export keyboard-read function as public method ``getch()``, diff --git a/blessed/formatters.py b/blessed/formatters.py index 7ba844d8..38bd4299 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -140,7 +140,7 @@ def get_proxy_string(term, attr): # normalize 'screen-256color', or 'ansi.sys' to its basic names _normalize = ('screen', 'ansi',) term = next(iter(_simp for _simp in _normalize - if term.startswith(_simp)), term) + if term.kind.startswith(_simp)), term) return { 'screen': { @@ -157,7 +157,7 @@ def get_proxy_string(term, attr): 'cnorm': ParameterizingProxyString( (u'\x1b[?25h', lambda *arg: ()), term.normal, attr), } - }.get(term._kind, {}).get(attr, None) + }.get(term.kind, {}).get(attr, None) class FormattingString(text_type): diff --git a/blessed/sequences.py b/blessed/sequences.py index c838789e..bafce537 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -262,7 +262,7 @@ def init_sequence_patterns(term): containing sequences generated by this terminal, to determine the printable length of a string. """ - if term._kind in _BINTERM_UNSUPPORTED: + if term.kind in _BINTERM_UNSUPPORTED: warnings.warn(_BINTERM_UNSUPPORTED_MSG) # Build will_move, a list of terminal capabilities that have diff --git a/blessed/terminal.py b/blessed/terminal.py index 93ab5d84..9ddf9f3f 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -248,6 +248,11 @@ def __getattr__(self, attr): setattr(self, attr, val) return val + @property + def kind(self): + """Name of this terminal type as string.""" + return self._kind + @property def does_styling(self): """Whether this instance will emit terminal sequences (bool).""" diff --git a/blessed/tests/test_core.py b/blessed/tests/test_core.py index 3e7d53a9..a779e532 100644 --- a/blessed/tests/test_core.py +++ b/blessed/tests/test_core.py @@ -232,7 +232,7 @@ def child(): warnings.filterwarnings("ignore", category=UserWarning) term = TestTerminal(kind='unknown', force_styling=True) - assert term._kind is None + assert term.kind is None assert term.does_styling is False assert term.number_of_colors == 0 warnings.resetwarnings() From f0ceed10c147be4ab85ef09c92673cebd14863f6 Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 6 Sep 2014 13:53:30 -0700 Subject: [PATCH 208/459] bugfix term kind normalization --- blessed/formatters.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/blessed/formatters.py b/blessed/formatters.py index 38bd4299..c63f2422 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -136,12 +136,9 @@ def get_proxy_string(term, attr): Returns instance of ParameterizingProxyString or NullCallableString. """ - # normalize 'screen-256color', or 'ansi.sys' to its basic names - _normalize = ('screen', 'ansi',) - term = next(iter(_simp for _simp in _normalize - if term.kind.startswith(_simp)), term) - + term_kind = next(iter(_kind for _kind in ('screen', 'ansi',) + if term.kind.startswith(_kind)), term) return { 'screen': { # proxy move_x/move_y for 'screen' terminal type. @@ -157,7 +154,7 @@ def get_proxy_string(term, attr): 'cnorm': ParameterizingProxyString( (u'\x1b[?25h', lambda *arg: ()), term.normal, attr), } - }.get(term.kind, {}).get(attr, None) + }.get(term_kind, {}).get(attr, None) class FormattingString(text_type): From 046adad509c8a16470369db2d3a9188665c9830f Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 6 Sep 2014 13:59:27 -0700 Subject: [PATCH 209/459] unit/travis-ci fixes --- blessed/tests/test_sequences.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index 895e118f..8babad44 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -119,7 +119,7 @@ def test_unit_binpacked_unittest(unsupported_sequence_terminals): init_sequence_patterns) warnings.filterwarnings("error", category=UserWarning) term = mock.Mock() - term._kind = unsupported_sequence_terminals + term.kind = unsupported_sequence_terminals try: init_sequence_patterns(term) @@ -256,7 +256,6 @@ def child(kind): assert (t.stream.getvalue() == expected_output) child('ansi') - child('ansi.sys') def test_zero_location(all_standard_terms): From a0458cbf31cd0bdef1ada4aaefd7726a10766be1 Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 6 Sep 2014 16:54:42 -0700 Subject: [PATCH 210/459] use relative imports for tests.accessories.py --- blessed/tests/__init__.py | 0 blessed/tests/test_core.py | 2 +- blessed/tests/test_keyboard.py | 6 +++--- blessed/tests/test_length_sequence.py | 2 +- blessed/tests/test_sequences.py | 2 +- blessed/tests/test_wrap.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) create mode 100644 blessed/tests/__init__.py diff --git a/blessed/tests/__init__.py b/blessed/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/blessed/tests/test_core.py b/blessed/tests/test_core.py index a779e532..37c4d92c 100644 --- a/blessed/tests/test_core.py +++ b/blessed/tests/test_core.py @@ -15,7 +15,7 @@ import os # local -from accessories import ( +from .accessories import ( as_subprocess, TestTerminal, unicode_cap, diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index 3c5d6cb1..e079c886 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -16,7 +16,7 @@ import sys import os -from accessories import ( +from .accessories import ( read_until_eof, read_until_semaphore, SEND_SEMAPHORE, @@ -592,7 +592,7 @@ def test_esc_delay_cbreak_135(): assert key_name == u'KEY_ESCAPE' assert os.WEXITSTATUS(status) == 0 assert math.floor(time.time() - stime) == 1.0 - assert 135 <= int(duration_ms) <= 145, int(duration_ms) + assert 134 <= int(duration_ms) <= 145, int(duration_ms) def test_esc_delay_cbreak_timout_0(): @@ -627,7 +627,7 @@ def test_esc_delay_cbreak_timout_0(): assert key_name == u'KEY_ESCAPE' assert os.WEXITSTATUS(status) == 0 assert math.floor(time.time() - stime) == 0.0 - assert 35 <= int(duration_ms) <= 45, int(duration_ms) + assert 34 <= int(duration_ms) <= 45, int(duration_ms) def test_keystroke_default_args(): diff --git a/blessed/tests/test_length_sequence.py b/blessed/tests/test_length_sequence.py index e93c5f22..93aa2d1f 100644 --- a/blessed/tests/test_length_sequence.py +++ b/blessed/tests/test_length_sequence.py @@ -11,7 +11,7 @@ except ImportError: from io import StringIO -from accessories import ( +from .accessories import ( all_standard_terms, as_subprocess, TestTerminal, diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index 8babad44..ff010f74 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -8,7 +8,7 @@ import sys import os -from accessories import ( +from .accessories import ( unsupported_sequence_terminals, all_standard_terms, as_subprocess, diff --git a/blessed/tests/test_wrap.py b/blessed/tests/test_wrap.py index d15073e5..36e752d9 100644 --- a/blessed/tests/test_wrap.py +++ b/blessed/tests/test_wrap.py @@ -5,7 +5,7 @@ import fcntl import sys -from accessories import ( +from .accessories import ( as_subprocess, TestTerminal, many_columns, From 207530c4d9af765e771eccc49e3934d410604cc7 Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 6 Sep 2014 16:55:06 -0700 Subject: [PATCH 211/459] travis, tox, and coveralls -- combine coverage though, it is not possible to combine coverage across python interpreters in travis, as to each their own. --- .travis.yml | 12 ++++++++---- setup.py | 6 ++++-- tox.ini | 34 ++++++++++++++-------------------- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/.travis.yml b/.travis.yml index 096938bd..5471d121 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,8 +4,8 @@ env: - TOXENV=py26 - TOXENV=py27 - TOXENV=py33 - - TOXENV=py34 - TOXENV=pypy + - TOXENV=py34 install: - pip install -q tox @@ -13,13 +13,13 @@ install: # for python versions <27, we must install ordereddict # mimicking the same dynamically generated 'requires=' # in setup.py - - if [[ $TOXENV == "py25" ]] || [[ $TOXENV == "py26" ]]; then + - if [[ "${TOXENV}" == "py25" ]] || [[ "${TOXENV}" == "py26" ]]; then pip install -q ordereddict; fi # for python version =27, install coverage, coveralls. # (coverage only measured and published for one version) - - if [[ $TOXENV == "py27" ]]; then + - if [[ "${TOXENV}" == "py34" ]]; then pip install -q coverage coveralls; fi @@ -28,7 +28,11 @@ script: - tox -e $TOXENV after_success: - - if [[ "${TOXENV}" == "py27" ]]; then + - if [[ "${TOXENV}" == "py34" ]]; then + for f in ._coverage*; do + mv $f `echo $f | tr -d '_'` + done + coverage combine coveralls; fi diff --git a/setup.py b/setup.py index 15ed551f..f1275b4b 100755 --- a/setup.py +++ b/setup.py @@ -5,11 +5,14 @@ import setuptools.command.develop import setuptools.command.test +here = os.path.dirname(__file__) + class SetupDevelop(setuptools.command.develop.develop): def run(self): assert os.getenv('VIRTUAL_ENV'), 'You should be in a virtualenv' - self.spawn(('pip', 'install', '-U', 'tox',)) + self.spawn(('pip', 'install', '-U', '-r', + os.path.join(here, 'dev-requirements.txt'))) setuptools.command.develop.develop.run(self) @@ -27,7 +30,6 @@ def main(): if sys.version_info < (2, 7,): extra['install_requires'].extend(['ordereddict>=1.1']) - here = os.path.dirname(__file__) setuptools.setup( name='blessed', version='1.9.4', diff --git a/tox.ini b/tox.ini index edee73c1..8b9e3b1f 100644 --- a/tox.ini +++ b/tox.ini @@ -5,32 +5,26 @@ envlist = py26, py34, pypy +skip_missing_interpreters = true [testenv] -# for any python, run simple pytest -# with pep8 and pyflake checking -usedevelop = True -deps = pytest - pytest-cov - pytest-pep8 - pytest-flakes - mock - -rrequirements.txt - -whitelist_externals = /bin/bash +whitelist_externals = /bin/bash /bin/mv setenv = PYTHONIOENCODING=UTF8 +usedevelop=True -# run each test twice -- 1. w/o tty -commands = /bin/bash -c {envbindir}/py.test -v \ +# run each test twice +# -- 1. w/o tty +commands = /bin/bash -c '{envbindir}/py.test \ -x --strict --pep8 --flakes \ - --cov-report term-missing \ - blessed/tests {posargs} \ - < /dev/null 2>&1 | tee /dev/null -# -- 2. w/tty - {envbindir}/py.test -v \ + --cov blessed blessed/tests {posargs} \ + < /dev/null 2>&1 | tee /dev/null' + /bin/mv {toxinidir}/.coverage {toxinidir}/._coverage.{envname}.notty + +# -- 2. w/tty + /bin/bash -c '{envbindir}/py.test \ -x --strict --pep8 --flakes \ - --cov-report term-missing \ - blessed/tests {posargs} + --cov blessed blessed/tests {posargs}' + /bin/mv {toxinidir}/.coverage {toxinidir}/._coverage.{envname}.withtty [pytest] # py.test fixtures conflict with pyflakes From b9ead8c3cc062a45f7e9277ffd48dece933024d4 Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 6 Sep 2014 18:04:05 -0700 Subject: [PATCH 212/459] add test cov. for win32 paltform using PDCurses fix minor bug, that height/width of 80/24, not 24/80 was returned for such platforms. We don't really support it anyway (yet), so its not a big deal. --- blessed/formatters.py | 4 +-- blessed/terminal.py | 12 ++++---- blessed/tests/test_core.py | 48 ++++++++++++++++++++++++++++++++ blessed/tests/test_formatters.py | 43 ++++++++++++++++++++++++++++ 4 files changed, 99 insertions(+), 8 deletions(-) diff --git a/blessed/formatters.py b/blessed/formatters.py index c63f2422..7070323d 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -72,11 +72,11 @@ def __call__(self, *args): # Somebody passed a non-string; I don't feel confident # guessing what they were trying to do. raise - except Exception as err: + except curses.error as err: # ignore 'tparm() returned NULL', you won't get any styling, # even if does_styling is True. This happens on win32 platforms # with http://www.lfd.uci.edu/~gohlke/pythonlibs/#curses installed - if "tparm() returned NULL" not in err: + if "tparm() returned NULL" not in text_type(err): raise return NullCallableString() diff --git a/blessed/terminal.py b/blessed/terminal.py index 9ddf9f3f..8f65321e 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -20,10 +20,10 @@ except ImportError: tty_methods = ('setraw', 'cbreak', 'kbhit', 'height', 'width') msg_nosupport = ( - 'One or more of the modules termios, fcntl, and tty were ' - 'not found on your platform {0}. The following methods are ' - 'dummy/no-op unless a deriving class overrides them: ' - '{1}'.format(sys.platform.lower(), ', '.join(tty_methods))) + "One or more of the modules: 'termios', 'fcntl', and 'tty' " + "are not found on your platform '{0}'. The following methods " + "of Terminal are dummy/no-op unless a deriving class overrides " + "them: {1}".format(sys.platform.lower(), ', '.join(tty_methods))) warnings.warn(msg_nosupport) HAS_TTY = False else: @@ -292,7 +292,7 @@ def _winsize(fd): if HAS_TTY: data = fcntl.ioctl(fd, termios.TIOCGWINSZ, WINSZ._BUF) return WINSZ(*struct.unpack(WINSZ._FMT, data)) - return WINSZ(80, 24, 0, 0) + return WINSZ(ws_row=24, ws_col=80, ws_xpixel=0, ws_ypixel=0) def _height_and_width(self): """Return a tuple of (terminal height, terminal width). @@ -763,7 +763,7 @@ def _timeleft(stime, timeout): # # if (!initialised_setupterm && setupterm(termstr,fd,&err) == ERR) { # -# Python - perhaps wrongly - will not allow a re-initialisation of new +# Python - perhaps wrongly - will not allow for re-initialisation of new # terminals through setupterm(), so the value of cur_term cannot be changed # once set: subsequent calls to setupterm() have no effect. # diff --git a/blessed/tests/test_core.py b/blessed/tests/test_core.py index 37c4d92c..34032332 100644 --- a/blessed/tests/test_core.py +++ b/blessed/tests/test_core.py @@ -399,3 +399,51 @@ def child(): assert "fallback to ASCII" in str(warned[-1].message) child() + + +def test_win32_missing_tty_modules(monkeypatch): + "Ensure dummy exception is used when io is without UnsupportedOperation." + @as_subprocess + def child(): + try: + import builtins as __builtins__ + except ImportError: + import __builtins__ + + original_import = __builtins__.__import__ + + tty_modules = ('termios', 'fcntl', 'tty') + + def __import__(name, *args): + if name in tty_modules: + raise ImportError + return original_import(name, *args) + + for module in tty_modules: + sys.modules.pop(module, None) + + warnings.filterwarnings("error", category=UserWarning) + try: + __builtins__.__import__ = __import__ + try: + import blessed.terminal + imp.reload(blessed.terminal) + except UserWarning: + err = sys.exc_info()[1] + assert err.args[0] == blessed.terminal.msg_nosupport + + warnings.filterwarnings("ignore", category=UserWarning) + import blessed.terminal + imp.reload(blessed.terminal) + assert blessed.terminal.HAS_TTY is False + term = blessed.terminal.Terminal('ansi') + assert term.height == 24 + assert term.width == 80 + + finally: + __builtins__.__import__ = original_import + warnings.resetwarnings() + import blessed.terminal + imp.reload(blessed.terminal) + + child() diff --git a/blessed/tests/test_formatters.py b/blessed/tests/test_formatters.py index 68af2e92..b2e5f0c2 100644 --- a/blessed/tests/test_formatters.py +++ b/blessed/tests/test_formatters.py @@ -350,3 +350,46 @@ def test_pickled_parameterizing_string(monkeypatch): assert zero == pickle.loads(pickle.dumps(zero, protocol=proto_num)) w.send(zero) r.recv() == zero + + +def test_tparm_returns_null(monkeypatch): + """ Test 'tparm() returned NULL' is caught (win32 PDCurses systems). """ + # on win32, any calls to tparm raises curses.error with message, + # "tparm() returned NULL", function PyCurses_tparm of _cursesmodule.c + from blessed.formatters import ParameterizingString, NullCallableString + + def tparm(*args): + raise curses.error("tparm() returned NULL") + + monkeypatch.setattr(curses, 'tparm', tparm) + + term = mock.Mock() + term.normal = 'seq-normal' + + pstr = ParameterizingString(u'cap', u'norm', u'seq-name') + + value = pstr(u'x') + assert type(value) is NullCallableString + + +def test_tparm_other_exception(monkeypatch): + """ Test 'tparm() returned NULL' is caught (win32 PDCurses systems). """ + # on win32, any calls to tparm raises curses.error with message, + # "tparm() returned NULL", function PyCurses_tparm of _cursesmodule.c + from blessed.formatters import ParameterizingString, NullCallableString + + def tparm(*args): + raise curses.error("unexpected error in tparm()") + + monkeypatch.setattr(curses, 'tparm', tparm) + + term = mock.Mock() + term.normal = 'seq-normal' + + pstr = ParameterizingString(u'cap', u'norm', u'seq-name') + + try: + pstr(u'x') + assert False, "previous call should have raised curses.error" + except curses.error: + pass From 019312011267db39893a0eae2d31290fde1cb2e4 Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 6 Sep 2014 18:06:06 -0700 Subject: [PATCH 213/459] add new 'dev-requirements.txt' file --- dev-requirements.txt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 dev-requirements.txt diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 00000000..12bda575 --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1,6 @@ +pytest-flakes +pytest-pep8 +pytest-cov +pytest +mock +tox From afc562d21fcbbc3e489a4cf78fd85eb9de7810c5 Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 6 Sep 2014 18:18:40 -0700 Subject: [PATCH 214/459] patching __import__ of more versions of python --- blessed/tests/test_core.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/blessed/tests/test_core.py b/blessed/tests/test_core.py index 34032332..e21a1676 100644 --- a/blessed/tests/test_core.py +++ b/blessed/tests/test_core.py @@ -405,26 +405,29 @@ def test_win32_missing_tty_modules(monkeypatch): "Ensure dummy exception is used when io is without UnsupportedOperation." @as_subprocess def child(): + OLD_STYLE = False try: - import builtins as __builtins__ - except ImportError: - import __builtins__ - - original_import = __builtins__.__import__ + original_import = getattr(__builtins__, '__import__') + OLD_STYLE = True + except AttributeError: + original_import = __builtins__['__import__'] tty_modules = ('termios', 'fcntl', 'tty') - def __import__(name, *args): + def __import__(name, *args, **kwargs): if name in tty_modules: raise ImportError - return original_import(name, *args) + return original_import(name, *args, **kwargs) for module in tty_modules: sys.modules.pop(module, None) warnings.filterwarnings("error", category=UserWarning) try: - __builtins__.__import__ = __import__ + if OLD_STYLE: + __builtins__.__import__ = __import__ + else: + __builtins__['__import__'] = __import__ try: import blessed.terminal imp.reload(blessed.terminal) @@ -441,7 +444,10 @@ def __import__(name, *args): assert term.width == 80 finally: - __builtins__.__import__ = original_import + if OLD_STYLE: + setattr(__builtins__, '__import__', original_import) + else: + __builtins__['__import__'] = original_import warnings.resetwarnings() import blessed.terminal imp.reload(blessed.terminal) From cec390ae3ab9f3aff1dccced4d775af8da69438c Mon Sep 17 00:00:00 2001 From: jquast Date: Sat, 6 Sep 2014 18:35:49 -0700 Subject: [PATCH 215/459] travis scripting for coveralls fix? --- .travis.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5471d121..a716b6be 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,13 +28,7 @@ script: - tox -e $TOXENV after_success: - - if [[ "${TOXENV}" == "py34" ]]; then - for f in ._coverage*; do - mv $f `echo $f | tr -d '_'` - done - coverage combine - coveralls; - fi + - if [[ "${TOXENV}" == "py34" ]]; then for f in ._coverage*; do mv $f `echo $f | tr -d '_'`; done; coverage combine; coveralls; fi notifications: email: From 1e22efd601f080eaa62e3bd021083b9dae7025d4 Mon Sep 17 00:00:00 2001 From: jquast Date: Mon, 8 Sep 2014 07:08:21 -0700 Subject: [PATCH 216/459] only provide proxy where db is unmatched (u'') and, proxy hpa and vpa for 'ansi' terminals. I've noticed on OSX, 'ansi' *does* provide hpa and vpa, but on linux, it does not. This change covers both. --- blessed/formatters.py | 21 +++++++++++++-------- blessed/tests/test_sequences.py | 12 ++++++++---- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/blessed/formatters.py b/blessed/formatters.py index 7070323d..e77adcdb 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -131,8 +131,8 @@ def get_proxy_string(term, attr): """ Proxy and return callable StringClass for proxied attributes. We know that some kinds of terminal kinds support sequences that the - terminfo database often doesn't report -- such as the 'move_x' attribute - for terminal type 'screen', or 'hide_cursor' for 'ansi'. + terminfo database always report -- such as the 'move_x' attribute for + terminal type 'screen' and 'ansi', or 'hide_cursor' for 'ansi'. Returns instance of ParameterizingProxyString or NullCallableString. """ @@ -153,6 +153,10 @@ def get_proxy_string(term, attr): (u'\x1b[?25l', lambda *arg: ()), term.normal, attr), 'cnorm': ParameterizingProxyString( (u'\x1b[?25h', lambda *arg: ()), term.normal, attr), + 'hpa': ParameterizingProxyString( + (u'\x1b[{0}G', lambda *arg: (arg[0] + 1,)), term.normal, attr), + 'vpa': ParameterizingProxyString( + (u'\x1b[{0}d', lambda *arg: (arg[0] + 1,)), term.normal, attr), } }.get(term_kind, {}).get(attr, None) @@ -313,15 +317,16 @@ def resolve_attribute(term, attr): resolution = (resolve_attribute(term, fmt) for fmt in formatters) return FormattingString(u''.join(resolution), term.normal) else: - # and, for special terminals, such as 'screen', provide a Proxy - # ParameterizingString for attributes they do not claim to support, - # but actually do! (such as 'hpa' and 'vpa'). - proxy = get_proxy_string(term, term._sugar.get(attr, attr)) - if proxy is not None: - return proxy # otherwise, this is our end-game: given a sequence such as 'csr' # (change scrolling region), return a ParameterizingString instance, # that when called, performs and returns the final string after curses # capability lookup is performed. tparm_capseq = resolve_capability(term, attr) + if not tparm_capseq: + # and, for special terminals, such as 'screen', provide a Proxy + # ParameterizingString for attributes they do not claim to support, + # but actually do! (such as 'hpa' and 'vpa'). + proxy = get_proxy_string(term, term._sugar.get(attr, attr)) + if proxy is not None: + return proxy return ParameterizingString(tparm_capseq, term.normal, attr) diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index ff010f74..7a14ba02 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -206,8 +206,8 @@ def child(kind): child(all_standard_terms) -def test_inject_move_x_for_screen(): - """Test injection of hpa attribute for screen (issue #55).""" +def test_inject_move_x(): + """Test injection of hpa attribute for screen/ansi (issue #55).""" @as_subprocess def child(kind): t = TestTerminal(kind=kind, stream=StringIO(), force_styling=True) @@ -219,13 +219,15 @@ def child(kind): u'\x1b[{0}G'.format(COL + 1), unicode_cap('rc'))) assert (t.stream.getvalue() == expected_output) + assert (t.move_x(COL) == u'\x1b[{0}G'.format(COL + 1)) child('screen') child('screen-256color') + child('ansi') -def test_inject_move_y_for_screen(): - """Test injection of vpa attribute for screen (issue #55).""" +def test_inject_move_y(): + """Test injection of vpa attribute for screen/ansi (issue #55).""" @as_subprocess def child(kind): t = TestTerminal(kind=kind, stream=StringIO(), force_styling=True) @@ -237,9 +239,11 @@ def child(kind): u'\x1b[{0}d'.format(ROW + 1), unicode_cap('rc'))) assert (t.stream.getvalue() == expected_output) + assert (t.move_y(ROW) == u'\x1b[{0}d'.format(ROW + 1)) child('screen') child('screen-256color') + child('ansi') def test_inject_civis_and_cnorm_for_ansi(): From 423f819645b6957215a311ec7444cc2803828c52 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Tue, 2 Dec 2014 20:01:32 -0500 Subject: [PATCH 217/459] inkey returns immediately with invalid MBS prefix input --- blessed/terminal.py | 3 +- blessed/tests/test_keyboard.py | 74 ++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/blessed/terminal.py b/blessed/terminal.py index 8f65321e..f9bdd9d2 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -739,11 +739,12 @@ def _timeleft(stime, timeout): # handle escape key (KEY_ESCAPE) vs. escape sequence (which begins # with KEY_ESCAPE, \x1b[, \x1bO, or \x1b?), up to esc_delay when # received. This is not optimal, but causes least delay when - # (currently unhandled, and rare) "meta sends escape" is used, + # "meta sends escape" is used, # or when an unsupported sequence is sent. if ks.code is self.KEY_ESCAPE: esctime = time.time() while (ks.code is self.KEY_ESCAPE and + (len(ucs) == 1 or ucs[1] in (u'[', u'O', u'?')) and self.kbhit(_timeleft(esctime, esc_delay))): ucs += self.getch() ks = resolve(text=ucs) diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index e079c886..517254fd 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -630,6 +630,80 @@ def test_esc_delay_cbreak_timout_0(): assert 34 <= int(duration_ms) <= 45, int(duration_ms) +def test_esc_delay_cbreak_nonprefix_sequence(): + "ESC a (\\x1ba) will return an ESC immediately" + pid, master_fd = pty.fork() + if pid is 0: # child + try: + cov = __import__('cov_core_init').init() + except ImportError: + cov = None + term = TestTerminal() + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.cbreak(): + stime = time.time() + esc = term.inkey(timeout=5) + inp = term.inkey(timeout=5) + measured_time = (time.time() - stime) * 100 + os.write(sys.__stdout__.fileno(), ( + '%s %s %i' % (esc.name, inp, measured_time,)).encode('ascii')) + sys.stdout.flush() + if cov is not None: + cov.stop() + cov.save() + os._exit(0) + + with echo_off(master_fd): + read_until_semaphore(master_fd) + stime = time.time() + os.write(master_fd, u'\x1ba'.encode('ascii')) + key1_name, key2, duration_ms = read_until_eof(master_fd).split() + + pid, status = os.waitpid(pid, 0) + assert key1_name == u'KEY_ESCAPE' + assert key2 == u'a' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 0.0 + assert -1 <= int(duration_ms) <= 15, duration_ms + + +def test_esc_delay_cbreak_prefix_sequence(): + "An unfinished multibyte sequence (\\x1b[) will delay an ESC by .35 " + pid, master_fd = pty.fork() + if pid is 0: # child + try: + cov = __import__('cov_core_init').init() + except ImportError: + cov = None + term = TestTerminal() + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.cbreak(): + stime = time.time() + esc = term.inkey(timeout=5) + inp = term.inkey(timeout=5) + measured_time = (time.time() - stime) * 100 + os.write(sys.__stdout__.fileno(), ( + '%s %s %i' % (esc.name, inp, measured_time,)).encode('ascii')) + sys.stdout.flush() + if cov is not None: + cov.stop() + cov.save() + os._exit(0) + + with echo_off(master_fd): + read_until_semaphore(master_fd) + stime = time.time() + os.write(master_fd, u'\x1b['.encode('ascii')) + key1_name, key2, duration_ms = read_until_eof(master_fd).split() + + pid, status = os.waitpid(pid, 0) + assert key1_name == u'KEY_ESCAPE' + assert key2 == u'[' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 0.0 + assert 34 <= int(duration_ms) <= 45, duration_ms + + def test_keystroke_default_args(): "Test keyboard.Keystroke constructor with default arguments." from blessed.keyboard import Keystroke From 774863121bb96c25eaad53a7c539785d4dc1a1f4 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Wed, 3 Dec 2014 00:23:13 -0500 Subject: [PATCH 218/459] build prefix table instead of hardcoding sequence prefixes --- blessed/keyboard.py | 8 +++++++- blessed/terminal.py | 9 ++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/blessed/keyboard.py b/blessed/keyboard.py index 0a3b7416..ae4bfabc 100644 --- a/blessed/keyboard.py +++ b/blessed/keyboard.py @@ -3,7 +3,7 @@ __author__ = 'Jeff Quast ' __license__ = 'MIT' -__all__ = ['Keystroke', 'get_keyboard_codes', 'get_keyboard_sequences'] +__all__ = ['Keystroke', 'get_keyboard_codes', 'get_keyboard_sequences', 'prefixes'] import curses.has_key import collections @@ -174,6 +174,12 @@ def get_keyboard_sequences(term): (seq, sequence_map[seq]) for seq in sorted( sequence_map.keys(), key=len, reverse=True))) +def prefixes(sequences): + """prefixes(iterable of strings) -> (set) + + Returns a set of proper prefixes of an iterable of strings + """ + return set(seq[:i] for seq in sequences for i in range(1, len(seq))) def resolve_sequence(text, mapper, codes): """resolve_sequence(text, mapper, codes) -> Keystroke() diff --git a/blessed/terminal.py b/blessed/terminal.py index f9bdd9d2..288a8171 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -60,6 +60,7 @@ class IOUnsupportedOperation(Exception): from .keyboard import ( get_keyboard_sequences, get_keyboard_codes, + prefixes, resolve_sequence, ) @@ -208,6 +209,8 @@ def __init__(self, kind=None, stream=None, force_styling=False): # build database of sequence <=> KEY_NAME self._keymap = get_keyboard_sequences(self) + # build set of prefixes of sequences + self._keymap_prefixes = prefixes(self._keymap) self._keyboard_buf = collections.deque() if self.keyboard_fd is not None: @@ -736,15 +739,15 @@ def _timeleft(stime, timeout): ucs += self.getch() ks = resolve(text=ucs) - # handle escape key (KEY_ESCAPE) vs. escape sequence (which begins - # with KEY_ESCAPE, \x1b[, \x1bO, or \x1b?), up to esc_delay when + # handle escape key (KEY_ESCAPE) vs. escape sequence (like those + # that begin with \x1b[ or \x1bO) up to esc_delay when # received. This is not optimal, but causes least delay when # "meta sends escape" is used, # or when an unsupported sequence is sent. if ks.code is self.KEY_ESCAPE: esctime = time.time() while (ks.code is self.KEY_ESCAPE and - (len(ucs) == 1 or ucs[1] in (u'[', u'O', u'?')) and + ucs in self._keymap_prefixes and self.kbhit(_timeleft(esctime, esc_delay))): ucs += self.getch() ks = resolve(text=ucs) From 3a264657219cc5e67dbe81f99ccf4ac75a95763e Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Wed, 3 Dec 2014 00:39:42 -0500 Subject: [PATCH 219/459] add simple test for prefixes --- blessed/tests/test_keyboard.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index 517254fd..db9bcb7a 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -903,6 +903,14 @@ def test_resolve_sequence(): assert repr(ks) in (u"KEY_L", "KEY_L") +def test_prefixes(): + "Test prefixes" + from blessed.keyboard import prefixes + keys = {u'abc': '1', u'abdf': '2', u'e': '3'} + pfs = prefixes(keys) + assert pfs == set([u'a', u'ab', u'abd']) + + def test_keypad_mixins_and_aliases(): """ Test PC-Style function key translations when in ``keypad`` mode.""" # Key plain app modified From 9a7e1b2a34be4d2727ec7565eff3604c5edeae73 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Wed, 3 Dec 2014 00:07:11 -0800 Subject: [PATCH 220/459] Add teamcity script runners --- .gitignore | 1 + tools/teamcity-coverage-report.sh | 27 +++++++++++++++++++++ tools/teamcity-runtests.sh | 39 +++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100755 tools/teamcity-coverage-report.sh create mode 100755 tools/teamcity-runtests.sh diff --git a/.gitignore b/.gitignore index 61573956..ca6b0293 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ dist docs/_build htmlcov .coveralls.yml +._coverage* diff --git a/tools/teamcity-coverage-report.sh b/tools/teamcity-coverage-report.sh new file mode 100755 index 00000000..2e32241b --- /dev/null +++ b/tools/teamcity-coverage-report.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# This is to be executed by each individual OS test. It only +# combines coverage files and reports locally to the given +# TeamCity build configuration. +set -e +set -o pipefail +[ -z ${TEMP} ] && TEMP=/tmp + +# combine all .coverage* files, +coverage combine + +# create ascii report, +report_file=$(mktemp $TEMP/coverage.XXXXX) +coverage report --rcfile=`dirname $0`/../.coveragerc > "${report_file}" 2>/dev/null + +# Report Code Coverage for TeamCity, using 'Service Messages', +# https://confluence.jetbrains.com/display/TCD8/How+To...#HowTo...-ImportcoverageresultsinTeamCity +# https://confluence.jetbrains.com/display/TCD8/Custom+Chart#CustomChart-DefaultStatisticsValuesProvidedbyTeamCity +total_no_lines=$(awk '/TOTAL/{printf("%s",$2)}' < "${report_file}") +total_no_misses=$(awk '/TOTAL/{printf("%s",$3)}' < "${report_file}") +total_no_covered=$((${total_no_lines} - ${total_no_misses})) +echo "##teamcity[buildStatisticValue key='CodeCoverageAbsLTotal' value='""${total_no_lines}""']" +echo "##teamcity[buildStatisticValue key='CodeCoverageAbsLCovered' value='""${total_no_covered}""']" + +# Display for human consumption and remove ascii file. +cat "${report_file}" +rm "${report_file}" diff --git a/tools/teamcity-runtests.sh b/tools/teamcity-runtests.sh new file mode 100755 index 00000000..0f241a9b --- /dev/null +++ b/tools/teamcity-runtests.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# +# This script assumes that the project 'ptyprocess' is +# available in the parent of the project's folder. +set -e +set -o pipefail + +if [ -z $1 ]; then + echo "$0 (2.6|2.7|3.3|3.4)" + exit 1 +fi + +pyversion=$1 +here=$(cd `dirname $0`; pwd) +osrel=$(uname -s) + +# run tests +cd $here/.. +ret=0 +tox \ + --cov pexpect \ + --cov-config .coveragerc \ + --junit-xml=results.${osrel}.py${pyversion}.xml \ + --verbose \ + --verbose \ + || ret=$? + +if [ $ret -ne 0 ]; then + # we always exit 0, preferring instead the jUnit XML + # results to be the dominate cause of a failed build. + echo "py.test returned exit code ${ret}." >&2 + echo "the build should detect and report these failing tests." >&2 +fi + +# combine all coverage to single file, publish as build +# artifact in {pexpect_projdir}/build-output +mkdir -p build-output +coverage combine +mv .coverage build-output/.coverage.${osrel}.py{$pyversion}.$RANDOM.$$ From c3257f7cc8679f74ae4016cdd3190ba8801297cf Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Wed, 3 Dec 2014 00:12:11 -0800 Subject: [PATCH 221/459] tox and jUnitXML changes --- tools/teamcity-runtests.sh | 10 ++-------- tox.ini | 4 ++++ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/tools/teamcity-runtests.sh b/tools/teamcity-runtests.sh index 0f241a9b..b6d19d6f 100755 --- a/tools/teamcity-runtests.sh +++ b/tools/teamcity-runtests.sh @@ -17,13 +17,7 @@ osrel=$(uname -s) # run tests cd $here/.. ret=0 -tox \ - --cov pexpect \ - --cov-config .coveragerc \ - --junit-xml=results.${osrel}.py${pyversion}.xml \ - --verbose \ - --verbose \ - || ret=$? +tox || ret=$? if [ $ret -ne 0 ]; then # we always exit 0, preferring instead the jUnit XML @@ -36,4 +30,4 @@ fi # artifact in {pexpect_projdir}/build-output mkdir -p build-output coverage combine -mv .coverage build-output/.coverage.${osrel}.py{$pyversion}.$RANDOM.$$ +mv .coverage build-output/.coverage.${osrel}.$RANDOM.$$ diff --git a/tox.ini b/tox.ini index 8b9e3b1f..af6ce736 100644 --- a/tox.ini +++ b/tox.ini @@ -16,6 +16,8 @@ usedevelop=True # -- 1. w/o tty commands = /bin/bash -c '{envbindir}/py.test \ -x --strict --pep8 --flakes \ + --junit-xml=results.$(uname -s).{envname}.notty.xml \ + --verbose --verbose \ --cov blessed blessed/tests {posargs} \ < /dev/null 2>&1 | tee /dev/null' /bin/mv {toxinidir}/.coverage {toxinidir}/._coverage.{envname}.notty @@ -23,6 +25,8 @@ commands = /bin/bash -c '{envbindir}/py.test \ # -- 2. w/tty /bin/bash -c '{envbindir}/py.test \ -x --strict --pep8 --flakes \ + --junit-xml=results.$(uname -s).{envname}.withtty.xml \ + --verbose --verbose \ --cov blessed blessed/tests {posargs}' /bin/mv {toxinidir}/.coverage {toxinidir}/._coverage.{envname}.withtty From 19fabed1d2e462fcb097aa5b703211c86b990925 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Wed, 3 Dec 2014 00:19:35 -0800 Subject: [PATCH 222/459] Copy these display scripts taken from pexpect --- tools/display-sighandlers.py | 20 ++++ tools/display-terminalinfo.py | 209 ++++++++++++++++++++++++++++++++++ 2 files changed, 229 insertions(+) create mode 100755 tools/display-sighandlers.py create mode 100755 tools/display-terminalinfo.py diff --git a/tools/display-sighandlers.py b/tools/display-sighandlers.py new file mode 100755 index 00000000..98445e95 --- /dev/null +++ b/tools/display-sighandlers.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +# Displays all signals, their values, and their handlers. +from __future__ import print_function +import signal +FMT = '{name:<10} {value:<5} {description}' + +# header +print(FMT.format(name='name', value='value', description='description')) +print('-' * (33)) + +for name, value in [(signal_name, getattr(signal, signal_name)) + for signal_name in dir(signal) + if signal_name.startswith('SIG') + and not signal_name.startswith('SIG_')]: + handler = signal.getsignal(value) + description = { + signal.SIG_IGN: "ignored(SIG_IGN)", + signal.SIG_DFL: "default(SIG_DFL)" + }.get(handler, handler) + print(FMT.format(name=name, value=value, description=description)) diff --git a/tools/display-terminalinfo.py b/tools/display-terminalinfo.py new file mode 100755 index 00000000..15911d41 --- /dev/null +++ b/tools/display-terminalinfo.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python +""" Display known information about our terminal. """ +from __future__ import print_function +import termios +import locale +import sys +import os + +BITMAP_IFLAG = { + 'IGNBRK': 'ignore BREAK condition', + 'BRKINT': 'map BREAK to SIGINTR', + 'IGNPAR': 'ignore (discard) parity errors', + 'PARMRK': 'mark parity and framing errors', + 'INPCK': 'enable checking of parity errors', + 'ISTRIP': 'strip 8th bit off chars', + 'INLCR': 'map NL into CR', + 'IGNCR': 'ignore CR', + 'ICRNL': 'map CR to NL (ala CRMOD)', + 'IXON': 'enable output flow control', + 'IXOFF': 'enable input flow control', + 'IXANY': 'any char will restart after stop', + 'IMAXBEL': 'ring bell on input queue full', + 'IUCLC': 'translate upper case to lower case', +} + +BITMAP_OFLAG = { + 'OPOST': 'enable following output processing', + 'ONLCR': 'map NL to CR-NL (ala CRMOD)', + 'OXTABS': 'expand tabs to spaces', + 'ONOEOT': 'discard EOT\'s `^D\' on output)', + 'OCRNL': 'map CR to NL', + 'OLCUC': 'translate lower case to upper case', + 'ONOCR': 'No CR output at column 0', + 'ONLRET': 'NL performs CR function', +} + +BITMAP_CFLAG = { + 'CSIZE': 'character size mask', + 'CS5': '5 bits (pseudo)', + 'CS6': '6 bits', + 'CS7': '7 bits', + 'CS8': '8 bits', + 'CSTOPB': 'send 2 stop bits', + 'CREAD': 'enable receiver', + 'PARENB': 'parity enable', + 'PARODD': 'odd parity, else even', + 'HUPCL': 'hang up on last close', + 'CLOCAL': 'ignore modem status lines', + 'CCTS_OFLOW': 'CTS flow control of output', + 'CRTSCTS': 'same as CCTS_OFLOW', + 'CRTS_IFLOW': 'RTS flow control of input', + 'MDMBUF': 'flow control output via Carrier', +} + +BITMAP_LFLAG = { + 'ECHOKE': 'visual erase for line kill', + 'ECHOE': 'visually erase chars', + 'ECHO': 'enable echoing', + 'ECHONL': 'echo NL even if ECHO is off', + 'ECHOPRT': 'visual erase mode for hardcopy', + 'ECHOCTL': 'echo control chars as ^(Char)', + 'ISIG': 'enable signals INTR, QUIT, [D]SUSP', + 'ICANON': 'canonicalize input lines', + 'ALTWERASE': 'use alternate WERASE algorithm', + 'IEXTEN': 'enable DISCARD and LNEXT', + 'EXTPROC': 'external processing', + 'TOSTOP': 'stop background jobs from output', + 'FLUSHO': 'output being flushed (state)', + 'NOKERNINFO': 'no kernel output from VSTATUS', + 'PENDIN': 'XXX retype pending input (state)', + 'NOFLSH': 'don\'t flush after interrupt', +} + +CTLCHAR_INDEX = { + 'VEOF': 'EOF', + 'VEOL': 'EOL', + 'VEOL2': 'EOL2', + 'VERASE': 'ERASE', + 'VWERASE': 'WERASE', + 'VKILL': 'KILL', + 'VREPRINT': 'REPRINT', + 'VINTR': 'INTR', + 'VQUIT': 'QUIT', + 'VSUSP': 'SUSP', + 'VDSUSP': 'DSUSP', + 'VSTART': 'START', + 'VSTOP': 'STOP', + 'VLNEXT': 'LNEXT', + 'VDISCARD': 'DISCARD', + 'VMIN': '---', + 'VTIME': '---', + 'VSTATUS': 'STATUS', +} + + +def display_bitmask(kind, bitmap, value): + """ Display all matching bitmask values for ``value`` given ``bitmap``. """ + col1_width = max(map(len, list(bitmap.keys()) + [kind])) + col2_width = 7 + FMT = '{name:>{col1_width}} {value:>{col2_width}} {description}' + print(FMT.format(name=kind, + value='Value', + description='Description', + col1_width=col1_width, + col2_width=col2_width)) + print('{0} {1} {2}'.format('-' * col1_width, + '-' * col2_width, + '-' * max(map(len, bitmap.values())))) + for flag_name, description in bitmap.items(): + try: + bitmask = getattr(termios, flag_name) + bit_val = 'on' if bool(value & bitmask) else 'off' + except AttributeError: + bit_val = 'undef' + print(FMT.format(name=flag_name, + value=bit_val, + description=description, + col1_width=col1_width, + col2_width=col2_width)) + print() + + +def display_ctl_chars(index, cc): + """ Display all control character indicies, names, and values. """ + title = 'Special Character' + col1_width = len(title) + col2_width = max(map(len, index.values())) + FMT = '{idx:<{col1_width}} {name:<{col2_width}} {value}' + print('Special line Characters'.center(40).rstrip()) + print(FMT.format(idx='Index', + name='Name', + value='Value', + col1_width=col1_width, + col2_width=col2_width)) + print('{0} {1} {2}'.format('-' * col1_width, + '-' * col2_width, + '-' * 10)) + for index_name, name in index.items(): + try: + index = getattr(termios, index_name) + value = cc[index] + if value == b'\xff': + value = '_POSIX_VDISABLE' + else: + value = repr(value) + except AttributeError: + value = 'undef' + print(FMT.format(idx=index_name, + name=name, + value=value, + col1_width=col1_width, + col2_width=col2_width)) + print() + + +def display_conf(kind, names, getter): + col1_width = max(map(len, names)) + FMT = '{name:>{col1_width}} {value}' + print(FMT.format(name=kind, + value='value', + col1_width=col1_width)) + print('{0} {1}'.format('-' * col1_width, '-' * 27)) + for name in names: + try: + value = getter(name) + except OSError as err: + value = err + print(FMT.format(name=name, value=value, col1_width=col1_width)) + print() + + +def main(): + fd = sys.stdin.fileno() + locale.setlocale(locale.LC_ALL, '') + encoding = locale.getpreferredencoding() + + print('os.isatty({0}) => {1}'.format(fd, os.isatty(fd))) + print('locale.getpreferredencoding() => {0}'.format(encoding)) + + display_conf(kind='pathconf', + names=os.pathconf_names, + getter=lambda name: os.fpathconf(fd, name)) + + try: + (iflag, oflag, cflag, lflag, ispeed, ospeed, cc + ) = termios.tcgetattr(fd) + except termios.error as err: + print('stdin is not a typewriter: {0}'.format(err)) + else: + display_bitmask(kind='Input Mode', + bitmap=BITMAP_IFLAG, + value=iflag) + display_bitmask(kind='Output Mode', + bitmap=BITMAP_OFLAG, + value=oflag) + display_bitmask(kind='Control Mode', + bitmap=BITMAP_CFLAG, + value=cflag) + display_bitmask(kind='Local Mode', + bitmap=BITMAP_LFLAG, + value=lflag) + display_ctl_chars(index=CTLCHAR_INDEX, + cc=cc) + print('os.ttyname({0}) => {1}'.format(fd, os.ttyname(fd))) + print('os.ctermid() => {0}'.format(os.ttyname(fd))) + + +if __name__ == '__main__': + main() From f1f2a713b94f52531ab7869d0cd1fa13c635cf2c Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Wed, 3 Dec 2014 00:36:30 -0800 Subject: [PATCH 223/459] This script takes no usage arguments --- tools/teamcity-runtests.sh | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tools/teamcity-runtests.sh b/tools/teamcity-runtests.sh index b6d19d6f..9145cfb7 100755 --- a/tools/teamcity-runtests.sh +++ b/tools/teamcity-runtests.sh @@ -5,12 +5,6 @@ set -e set -o pipefail -if [ -z $1 ]; then - echo "$0 (2.6|2.7|3.3|3.4)" - exit 1 -fi - -pyversion=$1 here=$(cd `dirname $0`; pwd) osrel=$(uname -s) From 74ee445f6ed46319f2bd3640677657f1fb1814ec Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Wed, 3 Dec 2014 01:41:23 -0800 Subject: [PATCH 224/459] Exclude python2.6 tests on Darwin -- build hangs --- tools/teamcity-runtests.sh | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tools/teamcity-runtests.sh b/tools/teamcity-runtests.sh index 9145cfb7..50ca3ee9 100755 --- a/tools/teamcity-runtests.sh +++ b/tools/teamcity-runtests.sh @@ -10,8 +10,17 @@ osrel=$(uname -s) # run tests cd $here/.. + +_cmd=tox +if [ X"$osrel" == X"Darwin" ]; then + # python2.6 locks up during py.test on osx build slave, + # exclude the test environment py26 from osx. + _cmd='tox -epy27,py33,py34,pypy' +fi + ret=0 -tox || ret=$? +echo ${_cmd} +${_cmd} || ret=$? if [ $ret -ne 0 ]; then # we always exit 0, preferring instead the jUnit XML From 6c7304d2953e40f608e504b97ec08331f08465b7 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Wed, 3 Dec 2014 01:49:15 -0800 Subject: [PATCH 225/459] virtualenv is broken on debian for python2.6 https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=754248 --- tools/teamcity-runtests.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tools/teamcity-runtests.sh b/tools/teamcity-runtests.sh index 50ca3ee9..8757cb58 100755 --- a/tools/teamcity-runtests.sh +++ b/tools/teamcity-runtests.sh @@ -12,9 +12,14 @@ osrel=$(uname -s) cd $here/.. _cmd=tox -if [ X"$osrel" == X"Darwin" ]; then +if [ X"$osrel" == X"Darwin" ] || [ X"$osrel" == X"Linux" ]; then # python2.6 locks up during py.test on osx build slave, # exclude the test environment py26 from osx. + # + # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=754248 + # cannot create a virtualenv for python2.6 due to use of + # "{}".format in virtualenv, throws exception + # ValueError: zero length field name in format. _cmd='tox -epy27,py33,py34,pypy' fi From c37e7c2dbd79a9ec35163de78ed18c6953b6ae9a Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Wed, 3 Dec 2014 01:58:27 -0800 Subject: [PATCH 226/459] disable no-tty tests (hangs on osx?) --- tox.ini | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/tox.ini b/tox.ini index af6ce736..dd3a6252 100644 --- a/tox.ini +++ b/tox.ini @@ -12,18 +12,20 @@ whitelist_externals = /bin/bash /bin/mv setenv = PYTHONIOENCODING=UTF8 usedevelop=True -# run each test twice -# -- 1. w/o tty -commands = /bin/bash -c '{envbindir}/py.test \ - -x --strict --pep8 --flakes \ - --junit-xml=results.$(uname -s).{envname}.notty.xml \ - --verbose --verbose \ - --cov blessed blessed/tests {posargs} \ - < /dev/null 2>&1 | tee /dev/null' - /bin/mv {toxinidir}/.coverage {toxinidir}/._coverage.{envname}.notty +## run each test twice +## -- 1. w/o tty +## this intermittently hangs on OSX build slave. +## no matter, hasn't proven useful in a long while. +##commands = /bin/bash -c '{envbindir}/py.test \ +## -x --strict --pep8 --flakes \ +## --junit-xml=results.$(uname -s).{envname}.notty.xml \ +## --verbose --verbose \ +## --cov blessed blessed/tests {posargs} \ +## < /dev/null 2>&1 | tee /dev/null' +## /bin/mv {toxinidir}/.coverage {toxinidir}/._coverage.{envname}.notty -# -- 2. w/tty - /bin/bash -c '{envbindir}/py.test \ +## -- 2. w/tty +commands = /bin/bash -c '{envbindir}/py.test \ -x --strict --pep8 --flakes \ --junit-xml=results.$(uname -s).{envname}.withtty.xml \ --verbose --verbose \ From 1d1876c65397270416bd55b1cbd741b92ef00b18 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Wed, 3 Dec 2014 01:58:54 -0800 Subject: [PATCH 227/459] Re-enable python2.6 on osx (maybe notty is the lockup) --- tools/teamcity-runtests.sh | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tools/teamcity-runtests.sh b/tools/teamcity-runtests.sh index 8757cb58..07b06e9b 100755 --- a/tools/teamcity-runtests.sh +++ b/tools/teamcity-runtests.sh @@ -12,10 +12,7 @@ osrel=$(uname -s) cd $here/.. _cmd=tox -if [ X"$osrel" == X"Darwin" ] || [ X"$osrel" == X"Linux" ]; then - # python2.6 locks up during py.test on osx build slave, - # exclude the test environment py26 from osx. - # +if [ X"$osrel" == X"Linux" ]; then # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=754248 # cannot create a virtualenv for python2.6 due to use of # "{}".format in virtualenv, throws exception From b6e7275c12135082d2ae93f1931887caf493297a Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Wed, 3 Dec 2014 02:11:48 -0800 Subject: [PATCH 228/459] Still hangs, try using pytest-xdist --- dev-requirements.txt | 1 + tox.ini | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 12bda575..1d858c78 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,4 +1,5 @@ pytest-flakes +pytest-xdist pytest-pep8 pytest-cov pytest diff --git a/tox.ini b/tox.ini index dd3a6252..1935f45b 100644 --- a/tox.ini +++ b/tox.ini @@ -26,7 +26,7 @@ usedevelop=True ## -- 2. w/tty commands = /bin/bash -c '{envbindir}/py.test \ - -x --strict --pep8 --flakes \ + -x --strict --pep8 --flakes -n 4 \ --junit-xml=results.$(uname -s).{envname}.withtty.xml \ --verbose --verbose \ --cov blessed blessed/tests {posargs}' From 5bd75bb9732fb0594bf1e8484ee353e1aef4d0e3 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Wed, 3 Dec 2014 02:13:31 -0800 Subject: [PATCH 229/459] use recreate=true --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 1935f45b..140a88e7 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,8 @@ skip_missing_interpreters = true [testenv] whitelist_externals = /bin/bash /bin/mv setenv = PYTHONIOENCODING=UTF8 -usedevelop=True +usedevelop=true +recreate=true ## run each test twice ## -- 1. w/o tty From 25c8bd28c0f6f6cea88c4d27a349c36f6a3d89c5 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Wed, 3 Dec 2014 02:16:17 -0800 Subject: [PATCH 230/459] Revert "Still hangs, try using pytest-xdist" This reverts commit b6e7275c12135082d2ae93f1931887caf493297a. --- dev-requirements.txt | 1 - tox.ini | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 1d858c78..12bda575 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,5 +1,4 @@ pytest-flakes -pytest-xdist pytest-pep8 pytest-cov pytest diff --git a/tox.ini b/tox.ini index 140a88e7..c7c960aa 100644 --- a/tox.ini +++ b/tox.ini @@ -27,7 +27,7 @@ recreate=true ## -- 2. w/tty commands = /bin/bash -c '{envbindir}/py.test \ - -x --strict --pep8 --flakes -n 4 \ + -x --strict --pep8 --flakes \ --junit-xml=results.$(uname -s).{envname}.withtty.xml \ --verbose --verbose \ --cov blessed blessed/tests {posargs}' From fea58cbf193a1e090ae063f6098e6abb161c4cba Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Wed, 3 Dec 2014 09:18:32 -0800 Subject: [PATCH 231/459] Fix coverage reporting --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index c7c960aa..c0c7523c 100644 --- a/tox.ini +++ b/tox.ini @@ -16,14 +16,14 @@ recreate=true ## run each test twice ## -- 1. w/o tty ## this intermittently hangs on OSX build slave. -## no matter, hasn't proven useful in a long while. +## no matter, hasn't proved useful in a long while. ##commands = /bin/bash -c '{envbindir}/py.test \ ## -x --strict --pep8 --flakes \ ## --junit-xml=results.$(uname -s).{envname}.notty.xml \ ## --verbose --verbose \ ## --cov blessed blessed/tests {posargs} \ ## < /dev/null 2>&1 | tee /dev/null' -## /bin/mv {toxinidir}/.coverage {toxinidir}/._coverage.{envname}.notty +## /bin/mv {toxinidir}/.coverage {toxinidir}/.coverage.{envname}.notty ## -- 2. w/tty commands = /bin/bash -c '{envbindir}/py.test \ @@ -31,7 +31,7 @@ commands = /bin/bash -c '{envbindir}/py.test \ --junit-xml=results.$(uname -s).{envname}.withtty.xml \ --verbose --verbose \ --cov blessed blessed/tests {posargs}' - /bin/mv {toxinidir}/.coverage {toxinidir}/._coverage.{envname}.withtty + /bin/mv {toxinidir}/.coverage {toxinidir}/.coverage.{envname}.withtty [pytest] # py.test fixtures conflict with pyflakes From 42a106bc882a80058e6dda85d3a7efea2e3ac18f Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Thu, 1 Jan 2015 14:53:55 -0800 Subject: [PATCH 232/459] provide static analysis testing (prospector) --- .gitignore | 1 + .prospector.yaml | 66 ++++++++++++++++++++++++++++++++++++++ blessed/__init__.py | 8 +++-- blessed/keyboard.py | 2 +- blessed/sequences.py | 2 +- blessed/tests/test_core.py | 2 +- dev-requirements.txt | 6 ---- setup.py | 12 +++++-- tox.ini | 42 ++++++++++++------------ 9 files changed, 104 insertions(+), 37 deletions(-) create mode 100644 .prospector.yaml delete mode 100644 dev-requirements.txt diff --git a/.gitignore b/.gitignore index ca6b0293..e30e3478 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ *.egg-info *.egg *.pyc +results*.xml build dist docs/_build diff --git a/.prospector.yaml b/.prospector.yaml new file mode 100644 index 00000000..3ccfbdad --- /dev/null +++ b/.prospector.yaml @@ -0,0 +1,66 @@ +inherits: + - strictness_veryhigh + +ignore: + - (^|/)\..+ + - ^docs/ + +test-warnings: true + +output-format: grouped + +dodgy: + # Looks at Python code to search for things which look "dodgy" + # such as passwords or git conflict artifacts + run: true + +frosted: + # static analysis + run: true + +mccabe: + # complexity checking. + run: true + +pep257: + # docstring checking + run: true + +pep8: + # style checking + run: true + options: + max-line-length: 100 + +pyflakes: + # preferring 'frosted' instead (a fork of) + run: false + +pylint: + # static analysis and then some + run: true + options: + # pytest module has dynamically assigned functions, + # raising errors such as: E1101: Module 'pytest' has + # no 'mark' member + ignored-classes: pytest + disable: + # Too many lines in module + ##- C0302 + # Used * or ** magic + ##- W0142 + # Used builtin function 'filter'. + # (For maintainability, one should prefer list comprehension.) + ##- W0141 + # Use % formatting in logging functions but pass the % parameters + ##- W1202 + +pyroma: + # checks setup.py + run: true + +vulture: + # this tool does a good job of finding unused code. + run: true + +# vim: noai:ts=4:sw=4 diff --git a/blessed/__init__.py b/blessed/__init__.py index a1693a57..481468f0 100644 --- a/blessed/__init__.py +++ b/blessed/__init__.py @@ -1,6 +1,8 @@ -"A thin, practical wrapper around curses terminal capabilities." +""" +A thin, practical wrapper around terminal capabilities in Python -# import as _platform to avoid tab-completion with IPython (thanks @kanzure) +http://pypi.python.org/pypi/blessed +""" import platform as _platform if ('3', '0', '0') <= _platform.python_version_tuple() < ('3', '2', '2+'): # Good till 3.2.10 @@ -11,4 +13,4 @@ from .terminal import Terminal -__all__ = ['Terminal'] +__all__ = ('Terminal',) diff --git a/blessed/keyboard.py b/blessed/keyboard.py index 0a3b7416..28c26b76 100644 --- a/blessed/keyboard.py +++ b/blessed/keyboard.py @@ -3,7 +3,7 @@ __author__ = 'Jeff Quast ' __license__ = 'MIT' -__all__ = ['Keystroke', 'get_keyboard_codes', 'get_keyboard_sequences'] +__all__ = ('Keystroke', 'get_keyboard_codes', 'get_keyboard_sequences',) import curses.has_key import collections diff --git a/blessed/sequences.py b/blessed/sequences.py index bafce537..2447e53f 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -4,7 +4,7 @@ __author__ = 'Jeff Quast ' __license__ = 'MIT' -__all__ = ['init_sequence_patterns', 'Sequence', 'SequenceTextWrapper'] +__all__ = ('init_sequence_patterns', 'Sequence', 'SequenceTextWrapper',) # built-ins import functools diff --git a/blessed/tests/test_core.py b/blessed/tests/test_core.py index e21a1676..effaa135 100644 --- a/blessed/tests/test_core.py +++ b/blessed/tests/test_core.py @@ -30,7 +30,7 @@ def test_export_only_Terminal(): "Ensure only Terminal instance is exported for import * statements." import blessed - assert blessed.__all__ == ['Terminal'] + assert blessed.__all__ == ('Terminal',) def test_null_location(all_terms): diff --git a/dev-requirements.txt b/dev-requirements.txt deleted file mode 100644 index 12bda575..00000000 --- a/dev-requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -pytest-flakes -pytest-pep8 -pytest-cov -pytest -mock -tox diff --git a/setup.py b/setup.py index f1275b4b..338e2ffd 100755 --- a/setup.py +++ b/setup.py @@ -1,6 +1,10 @@ #!/usr/bin/env python +# std imports, +import subprocess import sys import os + +# 3rd-party import setuptools import setuptools.command.develop import setuptools.command.test @@ -10,9 +14,11 @@ class SetupDevelop(setuptools.command.develop.develop): def run(self): + # ensure a virtualenv is loaded, assert os.getenv('VIRTUAL_ENV'), 'You should be in a virtualenv' - self.spawn(('pip', 'install', '-U', '-r', - os.path.join(here, 'dev-requirements.txt'))) + # ensure tox is installed + subprocess.check_call(('pip', 'install', 'tox')) + # install development egg-link setuptools.command.develop.develop.run(self) @@ -60,7 +66,7 @@ def main(): 'Topic :: Software Development :: Libraries', 'Topic :: Software Development :: User Interfaces', 'Topic :: Terminals' - ], + ], keywords=['terminal', 'sequences', 'tty', 'curses', 'ncurses', 'formatting', 'style', 'color', 'console', 'keyboard', 'ansi', 'xterm'], diff --git a/tox.ini b/tox.ini index c0c7523c..35227eef 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,6 @@ [tox] -envlist = py26, +envlist = static_analysis, + py26, py27, py33, py34, @@ -10,28 +11,25 @@ skip_missing_interpreters = true [testenv] whitelist_externals = /bin/bash /bin/mv setenv = PYTHONIOENCODING=UTF8 -usedevelop=true -recreate=true +deps = pytest-flakes + pytest-pep8 + pytest-cov + pytest + mock +commands = {envbindir}/py.test \ + -x --strict --pep8 --flakes \ + --junit-xml=results.{envname}.xml \ + --verbose --verbose \ + --cov blessed blessed/tests {posargs} + /bin/mv {toxinidir}/.coverage {toxinidir}/.coverage.{envname} -## run each test twice -## -- 1. w/o tty -## this intermittently hangs on OSX build slave. -## no matter, hasn't proved useful in a long while. -##commands = /bin/bash -c '{envbindir}/py.test \ -## -x --strict --pep8 --flakes \ -## --junit-xml=results.$(uname -s).{envname}.notty.xml \ -## --verbose --verbose \ -## --cov blessed blessed/tests {posargs} \ -## < /dev/null 2>&1 | tee /dev/null' -## /bin/mv {toxinidir}/.coverage {toxinidir}/.coverage.{envname}.notty - -## -- 2. w/tty -commands = /bin/bash -c '{envbindir}/py.test \ - -x --strict --pep8 --flakes \ - --junit-xml=results.$(uname -s).{envname}.withtty.xml \ - --verbose --verbose \ - --cov blessed blessed/tests {posargs}' - /bin/mv {toxinidir}/.coverage {toxinidir}/.coverage.{envname}.withtty +[testenv:static_analysis] +deps = prospector[with_pep257,with_pyroma,with_vulture] +commands = prospector \ + --die-on-tool-error \ + --test-warnings \ + --doc-warnings \ + {toxinidir} [pytest] # py.test fixtures conflict with pyflakes From c936c27855a21fca0914351cdd0975d26e945c37 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 3 Jan 2015 16:35:20 -0800 Subject: [PATCH 233/459] Support testing all terminals reported by toe(1) --- blessed/_binterms.py | 869 ++++++++++++++++++++++++++ blessed/sequences.py | 14 +- blessed/terminal.py | 15 +- blessed/tests/accessories.py | 44 +- blessed/tests/test_core.py | 4 +- blessed/tests/test_keyboard.py | 1 - blessed/tests/test_length_sequence.py | 7 +- blessed/tests/test_sequences.py | 86 +-- blessed/tests/test_wrap.py | 148 ++--- tox.ini | 7 +- 10 files changed, 1001 insertions(+), 194 deletions(-) create mode 100644 blessed/_binterms.py diff --git a/blessed/_binterms.py b/blessed/_binterms.py new file mode 100644 index 00000000..762e762d --- /dev/null +++ b/blessed/_binterms.py @@ -0,0 +1,869 @@ +""" Exports a list of binary terminals blessed is not able to cope with. """ +#: This list of terminals is manually managed, it describes all of the terminals +#: that blessed cannot measure the sequence length for; they contain +#: binary-packed capabilities instead of numerics, so it is not possible to +#: build regular expressions in the way that sequences.py does. +#: +#: This may be generated by exporting TEST_BINTERMS, then analyzing the +#: jUnit result xml written to the project folder. +binary_terminals = """ +9term +aaa+dec +aaa+rv +aaa+rv-100 +aaa+rv-30 +aaa-rv-unk +abm80 +abm85 +abm85e +abm85h +abm85h-old +act4 +act5 +addrinfo +adds980 +adm+sgr +adm+sgr-100 +adm+sgr-30 +adm11 +adm1178 +adm12 +adm1a +adm2 +adm20 +adm21 +adm22 +adm3 +adm31 +adm31-old +adm3a +adm3a+ +adm42 +adm42-ns +adm5 +aepro +aj510 +aj830 +alto-h19 +altos4 +altos7 +altos7pc +ampex175 +ampex175-b +ampex210 +ampex232 +ampex232w +ampex80 +annarbor4080 +ansi+arrows +ansi+csr +ansi+cup +ansi+enq +ansi+erase +ansi+idc +ansi+idl +ansi+idl1 +ansi+inittabs +ansi+local +ansi+local1 +ansi+pp +ansi+rca +ansi+rep +ansi+sgr +ansi+sgrbold +ansi+sgrdim +ansi+sgrso +ansi+sgrul +ansi+tabs +ansi-color-2-emx +ansi-color-3-emx +ansi-emx +ansi-mini +ansi-mr +ansi-mtabs +ansi-nt +ansi.sys +ansi.sys-old +ansi.sysk +ansi77 +apollo +apple-80 +apple-ae +apple-soroc +apple-uterm +apple-uterm-vb +apple-videx +apple-videx2 +apple-videx3 +apple-vm80 +apple2e +apple2e-p +apple80p +appleII +appleIIgs +atari +att4415+nl +att4420 +att4424m +att5310 +att5310-100 +att5310-30 +att5620-s +avatar +avatar0 +avatar0+ +avt+s +aws +awsc +bantam +basis +beacon +beehive +blit +bq300-8 +bq300-8-pc +bq300-8-pc-rv +bq300-8-pc-w +bq300-8-pc-w-rv +bq300-8rv +bq300-8w +bq300-w-8rv +c100 +c100-rv +c108 +c108-4p +c108-rv +c108-rv-4p +c108-w +ca22851 +cbblit +cbunix +cci +cci-100 +cci-30 +cdc456 +cdc721 +cdc721-esc +cdc721-esc-100 +cdc721-esc-30 +cdc721ll +cdc752 +cdc756 +cit101e +cit101e-132 +cit101e-n +cit101e-n132 +cit80 +citoh +citoh-6lpi +citoh-8lpi +citoh-comp +citoh-elite +citoh-pica +citoh-prop +coco3 +color_xterm +commodore +contel300 +contel301 +cops10 +ct8500 +ctrm +ctrm-100 +ctrm-30 +cyb110 +cyb83 +d132 +d200 +d200-100 +d200-30 +d210-dg +d210-dg-100 +d210-dg-30 +d211-dg +d211-dg-100 +d211-dg-30 +d216-dg +d216-dg-100 +d216-dg-30 +d216-unix +d216-unix-25 +d217-unix +d217-unix-25 +d220 +d220-100 +d220-30 +d220-7b +d220-7b-100 +d220-7b-30 +d220-dg +d220-dg-100 +d220-dg-30 +d230c +d230c-100 +d230c-30 +d230c-dg +d230c-dg-100 +d230c-dg-30 +d400 +d400-100 +d400-30 +d410-dg +d410-dg-100 +d410-dg-30 +d412-dg +d412-dg-100 +d412-dg-30 +d412-unix +d412-unix-25 +d412-unix-s +d412-unix-sr +d412-unix-w +d413-unix +d413-unix-25 +d413-unix-s +d413-unix-sr +d413-unix-w +d414-unix +d414-unix-25 +d414-unix-s +d414-unix-sr +d414-unix-w +d430c-dg +d430c-dg-100 +d430c-dg-30 +d430c-dg-ccc +d430c-dg-ccc-100 +d430c-dg-ccc-30 +d430c-unix +d430c-unix-100 +d430c-unix-25 +d430c-unix-25-100 +d430c-unix-25-30 +d430c-unix-25-ccc +d430c-unix-30 +d430c-unix-ccc +d430c-unix-s +d430c-unix-s-ccc +d430c-unix-sr +d430c-unix-sr-ccc +d430c-unix-w +d430c-unix-w-ccc +d470c +d470c-7b +d470c-dg +d555-dg +d577-dg +d800 +delta +dg+ccc +dg+color +dg+color8 +dg+fixed +dg-generic +dg200 +dg210 +dg211 +dg450 +dg460-ansi +dg6053 +dg6053-old +dgkeys+11 +dgkeys+15 +dgkeys+7b +dgkeys+8b +dgmode+color +dgmode+color8 +dgunix+ccc +dgunix+fixed +diablo1620 +diablo1620-m8 +diablo1640 +diablo1640-lm +diablo1740-lm +digilog +djgpp203 +dm1520 +dm2500 +dm3025 +dm3045 +dmchat +dmterm +dp8242 +dt100 +dt100w +dt110 +dt80-sas +dtc300s +dtc382 +dumb +dw1 +dw2 +dw3 +dw4 +dwk +ecma+color +ecma+sgr +elks +elks-glasstty +elks-vt52 +emu +ep40 +ep48 +esprit +esprit-am +ex155 +f100 +f100-rv +f110 +f110-14 +f110-14w +f110-w +f200 +f200-w +f200vi +f200vi-w +falco +falco-p +fos +fox +gator-52 +gator-52t +glasstty +gnome +gnome+pcfkeys +gnome-2007 +gnome-2008 +gnome-256color +gnome-fc5 +gnome-rh72 +gnome-rh80 +gnome-rh90 +go140 +go140w +gs6300 +gsi +gt40 +gt42 +guru+rv +guru+s +h19 +h19-bs +h19-g +h19-u +h19-us +h19k +ha8675 +ha8686 +hft-old +hmod1 +hp+arrows +hp+color +hp+labels +hp+pfk+arrows +hp+pfk+cr +hp+pfk-cr +hp+printer +hp2 +hp236 +hp262x +hp2641a +hp300h +hp700-wy +hp70092 +hp9837 +hp9845 +hpterm +hpterm-color +hz1000 +hz1420 +hz1500 +hz1510 +hz1520 +hz1520-noesc +hz1552 +hz1552-rv +hz2000 +i100 +i400 +ibcs2 +ibm+16color +ibm+color +ibm-apl +ibm-system1 +ibm3101 +ibm3151 +ibm3161 +ibm3161-C +ibm3162 +ibm3164 +ibm327x +ibm5081-c +ibm8514-c +ibmaed +ibmapa8c +ibmapa8c-c +ibmega +ibmega-c +ibmmono +ibmvga +ibmvga-c +icl6404 +icl6404-w +ifmr +ims-ansi +ims950 +ims950-b +ims950-rv +intertube +intertube2 +intext +intext2 +kaypro +kermit +kermit-am +klone+acs +klone+color +klone+koi8acs +klone+sgr +klone+sgr-dumb +klone+sgr8 +konsole +konsole+pcfkeys +konsole-16color +konsole-256color +konsole-base +konsole-linux +konsole-solaris +konsole-vt100 +konsole-vt420pc +konsole-xf3x +konsole-xf4x +kt7 +kt7ix +ln03 +ln03-w +lpr +luna +megatek +mgterm +microb +mime +mime-fb +mime-hb +mime2a +mime2a-s +mime314 +mime3a +mime3ax +minitel1 +minitel1b +mlterm+pcfkeys +mm340 +modgraph2 +msk227 +msk22714 +msk227am +mt70 +ncr160vppp +ncr160vpwpp +ncr160wy50+pp +ncr160wy50+wpp +ncr160wy60pp +ncr160wy60wpp +ncr260vppp +ncr260vpwpp +ncr260wy325pp +ncr260wy325wpp +ncr260wy350pp +ncr260wy350wpp +ncr260wy50+pp +ncr260wy50+wpp +ncr260wy60pp +ncr260wy60wpp +ncr7900i +ncr7900iv +ncr7901 +ncrvt100an +ncrvt100wan +ndr9500 +ndr9500-25 +ndr9500-25-mc +ndr9500-25-mc-nl +ndr9500-25-nl +ndr9500-mc +ndr9500-mc-nl +ndr9500-nl +nec5520 +newhpkeyboard +nextshell +northstar +nsterm+c +nsterm+c41 +nsterm+s +nwp511 +oblit +oc100 +oldpc3 +origpc3 +osborne +osborne-w +osexec +otek4112 +owl +p19 +pc-coherent +pc-venix +pc6300plus +pcix +pckermit +pckermit120 +pe1251 +pe7000c +pe7000m +pilot +pmcons +prism2 +prism4 +prism5 +pro350 +psterm-fast +psterm-fast-100 +psterm-fast-30 +pt100 +pt210 +pt250 +pty +qansi +qansi-g +qansi-m +qansi-t +qansi-w +qdss +qnx +qnx-100 +qnx-30 +qnxm +qnxm-100 +qnxm-30 +qnxt +qnxt-100 +qnxt-30 +qnxt2 +qnxtmono +qnxtmono-100 +qnxtmono-30 +qnxw +qnxw-100 +qnxw-30 +qume5 +qvt101 +qvt101+ +qvt102 +qvt119+ +qvt119+-25 +qvt119+-25-w +qvt119+-w +rbcomm +rbcomm-nam +rbcomm-w +rca +regent100 +regent20 +regent25 +regent40 +regent40+ +regent60 +rt6221 +rt6221-w +rtpc +rxvt+pcfkeys +scanset +screen+fkeys +screen-16color +screen-16color-bce +screen-16color-bce-s +screen-16color-s +screen-256color +screen-256color-bce +screen-256color-bce-s +screen-256color-s +screen-bce +screen-s +screen-w +screen.linux +screen.rxvt +screen.teraterm +screen.xterm-r6 +screen2 +screen3 +screwpoint +sibo +simterm +soroc120 +soroc140 +st52 +superbee-xsb +superbeeic +superbrain +swtp +synertek +t10 +t1061 +t1061f +t3700 +t3800 +tandem6510 +tandem653 +tandem653-100 +tandem653-30 +tek +tek4013 +tek4014 +tek4014-sm +tek4015 +tek4015-sm +tek4023 +tek4105 +tek4107 +tek4113-nd +tek4205 +tek4205-100 +tek4205-30 +tek4207-s +teraterm +teraterm2.3 +teraterm4.59 +terminet1200 +ti700 +ti931 +trs16 +trs2 +tt +tty33 +tty37 +tty43 +tvi803 +tvi9065 +tvi910 +tvi910+ +tvi912 +tvi912b +tvi912b+2p +tvi912b+dim +tvi912b+dim-100 +tvi912b+dim-30 +tvi912b+mc +tvi912b+mc-100 +tvi912b+mc-30 +tvi912b+printer +tvi912b+vb +tvi912b-2p +tvi912b-2p-mc +tvi912b-2p-mc-100 +tvi912b-2p-mc-30 +tvi912b-2p-p +tvi912b-2p-unk +tvi912b-mc +tvi912b-mc-100 +tvi912b-mc-30 +tvi912b-p +tvi912b-unk +tvi912b-vb +tvi912b-vb-mc +tvi912b-vb-mc-100 +tvi912b-vb-mc-30 +tvi912b-vb-p +tvi912b-vb-unk +tvi920b +tvi920b+fn +tvi920b-2p +tvi920b-2p-mc +tvi920b-2p-mc-100 +tvi920b-2p-mc-30 +tvi920b-2p-p +tvi920b-2p-unk +tvi920b-mc +tvi920b-mc-100 +tvi920b-mc-30 +tvi920b-p +tvi920b-unk +tvi920b-vb +tvi920b-vb-mc +tvi920b-vb-mc-100 +tvi920b-vb-mc-30 +tvi920b-vb-p +tvi920b-vb-unk +tvi921 +tvi924 +tvi925 +tvi925-hi +tvi92B +tvi92D +tvi950 +tvi950-2p +tvi950-4p +tvi950-rv +tvi950-rv-2p +tvi950-rv-4p +tvipt +vanilla +vc303 +vc404 +vc404-s +vc414 +vc415 +vi200 +vi200-f +vi200-rv +vi50 +vi500 +vi50adm +vi55 +viewpoint +vp3a+ +vp60 +vp90 +vremote +vt100+enq +vt100+fnkeys +vt100+keypad +vt100+pfkeys +vt100-s +vt102+enq +vt200-js +vt220+keypad +vt50h +vt52 +vt61 +wsiris +wy100 +wy100q +wy120 +wy120-25 +wy120-vb +wy160 +wy160-25 +wy160-42 +wy160-43 +wy160-tek +wy160-tek-100 +wy160-tek-30 +wy160-vb +wy30 +wy30-mc +wy30-mc-100 +wy30-mc-30 +wy30-vb +wy325 +wy325-25 +wy325-42 +wy325-43 +wy325-vb +wy350 +wy350-100 +wy350-30 +wy350-vb +wy350-vb-100 +wy350-vb-30 +wy350-w +wy350-w-100 +wy350-w-30 +wy350-wvb +wy350-wvb-100 +wy350-wvb-30 +wy370 +wy370-100 +wy370-105k +wy370-105k-100 +wy370-105k-30 +wy370-30 +wy370-EPC +wy370-EPC-100 +wy370-EPC-30 +wy370-nk +wy370-nk-100 +wy370-nk-30 +wy370-rv +wy370-rv-100 +wy370-rv-30 +wy370-tek +wy370-tek-100 +wy370-tek-30 +wy370-vb +wy370-vb-100 +wy370-vb-30 +wy370-w +wy370-w-100 +wy370-w-30 +wy370-wvb +wy370-wvb-100 +wy370-wvb-30 +wy50 +wy50-mc +wy50-mc-100 +wy50-mc-30 +wy50-vb +wy60 +wy60-25 +wy60-42 +wy60-43 +wy60-vb +wy99-ansi +wy99a-ansi +wy99f +wy99fa +wy99gt +wy99gt-25 +wy99gt-vb +wyse-vp +xerox1720 +xerox820 +xfce +xnuppc+100x37 +xnuppc+112x37 +xnuppc+128x40 +xnuppc+128x48 +xnuppc+144x48 +xnuppc+160x64 +xnuppc+200x64 +xnuppc+200x75 +xnuppc+256x96 +xnuppc+80x25 +xnuppc+80x30 +xnuppc+90x30 +xnuppc+c +xnuppc+c-100 +xnuppc+c-30 +xtalk-100 +xtalk-30 +xterm+256color +xterm+256color-100 +xterm+256color-30 +xterm+88color +xterm+88color-100 +xterm+88color-30 +xterm+app +xterm+edit +xterm+noapp +xterm+pc+edit +xterm+pcc0 +xterm+pcc1 +xterm+pcc2 +xterm+pcc3 +xterm+pce2 +xterm+pcf0 +xterm+pcf2 +xterm+pcfkeys +xterm+r6f2 +xterm+vt+edit +xterm-vt52 +z100 +z100bw +z29 +zen30 +zen50 +ztx +""".split() + +__all__ = ('binary_terminals',) diff --git a/blessed/sequences.py b/blessed/sequences.py index 2447e53f..2862197d 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -14,12 +14,15 @@ import sys import re +# local +from ._binterms import binary_terminals as _BINTERM_UNSUPPORTED + # 3rd-party import wcwidth # https://github.com/jquast/wcwidth -_BINTERM_UNSUPPORTED = ('kermit', 'avatar') -_BINTERM_UNSUPPORTED_MSG = ('sequence-awareness for terminals emitting ' - 'binary-packed capabilities are not supported.') +_BINTERM_UNSUPPORTED_MSG = ( + u"Terminal kind {0!r} contains binary-packed capabilities, blessed " + u"is likely to fail to measure the length of its sequences.") if sys.version_info[0] == 3: text_type = str @@ -263,7 +266,7 @@ def init_sequence_patterns(term): printable length of a string. """ if term.kind in _BINTERM_UNSUPPORTED: - warnings.warn(_BINTERM_UNSUPPORTED_MSG) + warnings.warn(_BINTERM_UNSUPPORTED_MSG.format(term.kind)) # Build will_move, a list of terminal capabilities that have # indeterminate effects on the terminal cursor position. @@ -322,9 +325,6 @@ def init_sequence_patterns(term): class SequenceTextWrapper(textwrap.TextWrapper): def __init__(self, width, term, **kwargs): self.term = term - assert kwargs.get('break_long_words', False) is False, ( - 'break_long_words is not sequence-safe') - kwargs['break_long_words'] = False textwrap.TextWrapper.__init__(self, width, **kwargs) def _wrap_chunks(self, chunks): diff --git a/blessed/terminal.py b/blessed/terminal.py index 8f65321e..b3797194 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -181,8 +181,9 @@ def __init__(self, kind=None, stream=None, force_styling=False): # somewhere. try: curses.setupterm(self._kind, self._init_descriptor) - except curses.error: - warnings.warn('Failed to setupterm(kind=%s)' % (self._kind,)) + except curses.error as err: + warnings.warn('Failed to setupterm(kind={0!r}): {1}' + .format(self._kind, err)) self._kind = None self._does_styling = False else: @@ -515,16 +516,8 @@ def wrap(self, text, width=None, **kwargs): Returns a list of strings that may contain escape sequences. See ``textwrap.TextWrapper`` for all available additional kwargs to customize wrapping behavior such as ``subsequent_indent``. - - Note that the keyword argument ``break_long_words`` may not be set, - it is not sequence-safe! """ - - _blw = 'break_long_words' - assert (_blw not in kwargs or not kwargs[_blw]), ( - "keyword argument, '{}' is not sequence-safe".format(_blw)) - - width = width is None and self.width or width + width = self.width if width is None else width lines = [] for line in text.splitlines(): lines.extend( diff --git a/blessed/tests/accessories.py b/blessed/tests/accessories.py index 6785827e..ed9ab3f9 100644 --- a/blessed/tests/accessories.py +++ b/blessed/tests/accessories.py @@ -3,6 +3,7 @@ # std from __future__ import with_statement import contextlib +import subprocess import functools import traceback import termios @@ -27,16 +28,18 @@ SEND_SEMAPHORE = SEMAPHORE = b'SEMAPHORE\n' RECV_SEMAPHORE = b'SEMAPHORE\r\n' all_xterms_params = ['xterm', 'xterm-256color'] -all_terms_params = ['screen', 'vt220', 'rxvt', 'cons25', 'linux', 'ansi'] -binpacked_terminal_params = ['avatar', 'kermit'] many_lines_params = [30, 100] -many_columns_params = [10, 100] -if os.environ.get('TRAVIS', None) is None: - # TRAVIS-CI has a limited type of terminals, the others ... - all_terms_params.extend(['avatar', 'kermit', 'dtterm', 'wyse520', - 'minix', 'eterm', 'aixterm', 'putty']) -all_standard_terms_params = (set(all_terms_params) - - set(binpacked_terminal_params)) +many_columns_params = [1, 25, 50] +from blessed._binterms import binary_terminals +try: + all_terms_params = set(_term.split(None, 1)[0] for _term in + subprocess.check_output(('toe',)).splitlines() + ) - (set(binary_terminals) + if not os.environ.get('TEST_BINTERMS') + else set()) +except OSError: + all_terms_params = ['screen', 'vt220', 'rxvt', + 'cons25', 'linux', 'ansi'] class as_subprocess(object): @@ -174,7 +177,10 @@ def echo_off(fd): def unicode_cap(cap): """Return the result of ``tigetstr`` except as Unicode.""" - val = curses.tigetstr(cap) + try: + val = curses.tigetstr(cap) + except curses.error: + val = None if val: return val.decode('latin1') return u'' @@ -182,15 +188,21 @@ def unicode_cap(cap): def unicode_parm(cap, *parms): """Return the result of ``tparm(tigetstr())`` except as Unicode.""" - cap = curses.tigetstr(cap) + try: + cap = curses.tigetstr(cap) + except curses.error: + cap = None if cap: - val = curses.tparm(cap, *parms) + try: + val = curses.tparm(cap, *parms) + except curses.error: + val = None if val: return val.decode('latin1') return u'' -@pytest.fixture(params=binpacked_terminal_params) +@pytest.fixture(params=binary_terminals) def unsupported_sequence_terminals(request): """Terminals that emit warnings for unsupported sequence-awareness.""" return request.param @@ -208,12 +220,6 @@ def all_terms(request): return request.param -@pytest.fixture(params=all_standard_terms_params) -def all_standard_terms(request): - """Common kind values for all kinds of terminals (except binary-packed).""" - return request.param - - @pytest.fixture(params=many_lines_params) def many_lines(request): """Various number of lines for screen height.""" diff --git a/blessed/tests/test_core.py b/blessed/tests/test_core.py index effaa135..befcea3d 100644 --- a/blessed/tests/test_core.py +++ b/blessed/tests/test_core.py @@ -212,7 +212,9 @@ def child(): term = TestTerminal(kind='unknown', force_styling=True) except UserWarning: err = sys.exc_info()[1] - assert err.args[0] == 'Failed to setupterm(kind=unknown)' + assert err.args[0] == ( + "Failed to setupterm(kind='unknown'): " + "setupterm: could not find terminal") else: assert not term.is_a_tty and not term.does_styling, ( 'Should have thrown exception') diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index e079c886..69fef7ce 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -698,7 +698,6 @@ def child(kind): term = TestTerminal(kind=kind, force_styling=True) keymap = get_keyboard_sequences(term) if term._cuf1: - assert term._cuf1 != u' ' assert term._cuf1 in keymap assert keymap[term._cuf1] == term.KEY_RIGHT if term._cub1: diff --git a/blessed/tests/test_length_sequence.py b/blessed/tests/test_length_sequence.py index 93aa2d1f..232775be 100644 --- a/blessed/tests/test_length_sequence.py +++ b/blessed/tests/test_length_sequence.py @@ -12,12 +12,11 @@ from io import StringIO from .accessories import ( - all_standard_terms, + all_terms, as_subprocess, TestTerminal, many_columns, many_lines, - all_terms, ) import pytest @@ -310,7 +309,7 @@ def child_mnemonics_wontmove(kind): child_mnemonics_wontmove(all_terms) -def test_sequence_is_movement_true(all_standard_terms): +def test_sequence_is_movement_true(all_terms): """Test parsers about sequences that move the cursor.""" @as_subprocess def child_mnemonics_willmove(kind): @@ -340,4 +339,4 @@ def child_mnemonics_willmove(kind): assert not t.clear or (len(t.clear) == measure_length(t.clear, t)) - child_mnemonics_willmove(all_standard_terms) + child_mnemonics_willmove(all_terms) diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index 7a14ba02..6de77ec7 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -1,16 +1,19 @@ # -*- coding: utf-8 -*- """Tests for Terminal() sequences and sequence-awareness.""" +# std imports try: from StringIO import StringIO except ImportError: from io import StringIO import platform +import random import sys import os +# local from .accessories import ( unsupported_sequence_terminals, - all_standard_terms, + all_terms, as_subprocess, TestTerminal, unicode_parm, @@ -19,6 +22,7 @@ many_lines, ) +# 3rd-party import pytest import mock @@ -89,12 +93,14 @@ def child(): @pytest.mark.skipif(os.environ.get('TRAVIS', None) is not None, reason="travis-ci does not have binary-packed terminals.") -def test_emit_warnings_about_binpacked(unsupported_sequence_terminals): +def test_emit_warnings_about_binpacked(): """Test known binary-packed terminals (kermit, avatar) emit a warning.""" + from blessed.sequences import _BINTERM_UNSUPPORTED_MSG + from blessed._binterms import binary_terminals + @as_subprocess def child(kind): import warnings - from blessed.sequences import _BINTERM_UNSUPPORTED_MSG warnings.filterwarnings("error", category=RuntimeWarning) warnings.filterwarnings("error", category=UserWarning) @@ -102,30 +108,33 @@ def child(kind): TestTerminal(kind=kind, force_styling=True) except UserWarning: err = sys.exc_info()[1] - assert (err.args[0] == _BINTERM_UNSUPPORTED_MSG or - err.args[0].startswith('Unknown parameter in ') + assert (err.args[0] == _BINTERM_UNSUPPORTED_MSG.format(kind) or + err.args[0].startswith('Unknown parameter in ') or + err.args[0].startswith('Failed to setupterm(') ), err else: assert 'warnings should have been emitted.' warnings.resetwarnings() - child(unsupported_sequence_terminals) + # any binary terminal should do. + child(binary_terminals[random.randrange(len(binary_terminals))]) -def test_unit_binpacked_unittest(unsupported_sequence_terminals): +def test_unit_binpacked_unittest(): """Unit Test known binary-packed terminals emit a warning (travis-safe).""" import warnings + from blessed._binterms import binary_terminals from blessed.sequences import (_BINTERM_UNSUPPORTED_MSG, init_sequence_patterns) warnings.filterwarnings("error", category=UserWarning) term = mock.Mock() - term.kind = unsupported_sequence_terminals + term.kind = binary_terminals[random.randrange(len(binary_terminals))] try: init_sequence_patterns(term) except UserWarning: err = sys.exc_info()[1] - assert err.args[0] == _BINTERM_UNSUPPORTED_MSG + assert err.args[0] == _BINTERM_UNSUPPORTED_MSG.format(term.kind) else: assert False, 'Previous stmt should have raised exception.' warnings.resetwarnings() @@ -139,7 +148,7 @@ def test_merge_sequences(): assert (_merge_sequences(input_list) == output_expected) -def test_location_with_styling(all_standard_terms): +def test_location_with_styling(all_terms): """Make sure ``location()`` works on all terminals.""" @as_subprocess def child_with_styling(kind): @@ -152,7 +161,7 @@ def child_with_styling(kind): u'hi', unicode_cap('rc'))) assert (t.stream.getvalue() == expected_output) - child_with_styling(all_standard_terms) + child_with_styling(all_terms) def test_location_without_styling(): @@ -170,7 +179,7 @@ def child_without_styling(): child_without_styling() -def test_horizontal_location(all_standard_terms): +def test_horizontal_location(all_terms): """Make sure we can move the cursor horizontally without changing rows.""" @as_subprocess def child(kind): @@ -181,14 +190,15 @@ def child(kind): (unicode_cap('sc'), unicode_parm('hpa', 5), unicode_cap('rc'))) - assert (t.stream.getvalue() == expected_output) + assert (t.stream.getvalue() == expected_output), ( + repr(t.stream.getvalue()), repr(expected_output)) # skip 'screen', hpa is proxied (see later tests) - if all_standard_terms != 'screen': - child(all_standard_terms) + if all_terms != 'screen': + child(all_terms) -def test_vertical_location(all_standard_terms): +def test_vertical_location(all_terms): """Make sure we can move the cursor horizontally without changing rows.""" @as_subprocess def child(kind): @@ -202,8 +212,8 @@ def child(kind): assert (t.stream.getvalue() == expected_output) # skip 'screen', vpa is proxied (see later tests) - if all_standard_terms != 'screen': - child(all_standard_terms) + if all_terms != 'screen': + child(all_terms) def test_inject_move_x(): @@ -262,7 +272,7 @@ def child(kind): child('ansi') -def test_zero_location(all_standard_terms): +def test_zero_location(all_terms): """Make sure ``location()`` pays attention to 0-valued args.""" @as_subprocess def child(kind): @@ -275,10 +285,10 @@ def child(kind): unicode_cap('rc'))) assert (t.stream.getvalue() == expected_output) - child(all_standard_terms) + child(all_terms) -def test_mnemonic_colors(all_standard_terms): +def test_mnemonic_colors(all_terms): """Make sure color shortcuts work.""" @as_subprocess def child(kind): @@ -300,10 +310,10 @@ def on_color(t, num): assert (t.on_bright_black == on_color(t, 8)) assert (t.on_bright_green == on_color(t, 10)) - child(all_standard_terms) + child(all_terms) -def test_callable_numeric_colors(all_standard_terms): +def test_callable_numeric_colors(all_terms): """``color(n)`` should return a formatting wrapper.""" @as_subprocess def child(kind): @@ -333,10 +343,10 @@ def child(kind): else: assert t.on_color(6)('smoo') == 'smoo' - child(all_standard_terms) + child(all_terms) -def test_null_callable_numeric_colors(all_standard_terms): +def test_null_callable_numeric_colors(all_terms): """``color(n)`` should be a no-op on null terminals.""" @as_subprocess def child(kind): @@ -344,20 +354,20 @@ def child(kind): assert (t.color(5)('smoo') == 'smoo') assert (t.on_color(6)('smoo') == 'smoo') - child(all_standard_terms) + child(all_terms) -def test_naked_color_cap(all_standard_terms): +def test_naked_color_cap(all_terms): """``term.color`` should return a stringlike capability.""" @as_subprocess def child(kind): t = TestTerminal(kind=kind) assert (t.color + '' == t.setaf + '') - child(all_standard_terms) + child(all_terms) -def test_formatting_functions(all_standard_terms): +def test_formatting_functions(all_terms): """Test simple and compound formatting wrappers.""" @as_subprocess def child(kind): @@ -388,10 +398,10 @@ def child(kind): assert (t.subscript(u'[1]') == expected_output) - child(all_standard_terms) + child(all_terms) -def test_compound_formatting(all_standard_terms): +def test_compound_formatting(all_terms): """Test simple and compound formatting wrappers.""" @as_subprocess def child(kind): @@ -411,10 +421,10 @@ def child(kind): assert (t.on_bright_red_bold_bright_green_underline('meh') == expected_output) - child(all_standard_terms) + child(all_terms) -def test_formatting_functions_without_tty(all_standard_terms): +def test_formatting_functions_without_tty(all_terms): """Test crazy-ass formatting wrappers when there's no tty.""" @as_subprocess def child(kind): @@ -426,10 +436,10 @@ def child(kind): assert (t.bold_underline_green_on_red('loo') == u'loo') assert (t.on_bright_red_bold_bright_green_underline('meh') == u'meh') - child(all_standard_terms) + child(all_terms) -def test_nice_formatting_errors(all_standard_terms): +def test_nice_formatting_errors(all_terms): """Make sure you get nice hints if you misspell a formatting wrapper.""" @as_subprocess def child(kind): @@ -463,10 +473,10 @@ def child(kind): e = sys.exc_info()[1] assert 'probably misspelled' in e.args[0], e.args - child(all_standard_terms) + child(all_terms) -def test_null_callable_string(all_standard_terms): +def test_null_callable_string(all_terms): """Make sure NullCallableString tolerates all kinds of args.""" @as_subprocess def child(kind): @@ -480,7 +490,7 @@ def child(kind): assert (t.uhh(9876) == '') assert (t.clear('x') == 'x') - child(all_standard_terms) + child(all_terms) def test_bnc_parameter_emits_warning(): diff --git a/blessed/tests/test_wrap.py b/blessed/tests/test_wrap.py index 36e752d9..dee3ebcd 100644 --- a/blessed/tests/test_wrap.py +++ b/blessed/tests/test_wrap.py @@ -35,122 +35,50 @@ def child(): child() -def test_SequenceWrapper_drop_whitespace_subsequent_indent(): +@pytest.mark.parametrize("kwargs", [ + dict(break_long_words=False, + drop_whitespace=False, + subsequent_indent=''), + dict(break_long_words=False, + drop_whitespace=True, + subsequent_indent=''), + dict(break_long_words=False, + drop_whitespace=False, + subsequent_indent=' '), + dict(break_long_words=False, + drop_whitespace=True, + subsequent_indent=' '), + # dict(break_long_words=True, + # drop_whitespace=False, + # subsequent_indent=''), + # dict(break_long_words=True, + # drop_whitespace=True, + # subsequent_indent=''), + # dict(break_long_words=True, + # drop_whitespace=False, + # subsequent_indent=' '), + # dict(break_long_words=True, + # drop_whitespace=True, + # subsequent_indent=' '), +]) +def test_SequenceWrapper(all_terms, many_columns, kwargs): """Test that text wrapping matches internal extra options.""" - WIDTH = 10 - @as_subprocess - def child(): + def child(term, width, kwargs): # build a test paragraph, along with a very colorful version t = TestTerminal() - pgraph = u' '.join( - ('a', 'ab', 'abc', 'abcd', 'abcde', 'abcdef', 'abcdefgh', - 'abcdefghi', 'abcdefghij', 'abcdefghijk', 'abcdefghijkl', - 'abcdefghijklm', 'abcdefghijklmn', 'abcdefghijklmno ',) - * 4) - - pgraph_colored = u''.join([ - t.color(n % 7) + t.bold + ch if ch != ' ' else ' ' - for n, ch in enumerate(pgraph)]) - - internal_wrapped = textwrap.wrap(pgraph, width=WIDTH, - break_long_words=False, - drop_whitespace=True, - subsequent_indent=u' '*3) - my_wrapped = t.wrap(pgraph, width=WIDTH, - drop_whitespace=True, - subsequent_indent=u' '*3) - my_wrapped_colored = t.wrap(pgraph_colored, width=WIDTH, - drop_whitespace=True, - subsequent_indent=u' '*3) - - # ensure we textwrap ascii the same as python - assert (internal_wrapped == my_wrapped) - - # ensure our first and last line wraps at its ends - first_l = internal_wrapped[0] - last_l = internal_wrapped[-1] - my_first_l = my_wrapped_colored[0] - my_last_l = my_wrapped_colored[-1] - assert (len(first_l) == t.length(my_first_l)) - assert (len(last_l) == t.length(my_last_l)), (internal_wrapped, - my_wrapped_colored) - assert (len(internal_wrapped[-1]) == t.length(my_wrapped_colored[-1])) - - # ensure our colored textwrap is the same line length - assert (len(internal_wrapped) == len(my_wrapped_colored)) - - child() - - -@pytest.mark.skipif(platform.python_implementation() == 'PyPy', - reason='PyPy fails TIOCSWINSZ') -def test_SequenceWrapper(all_terms, many_columns): - """Test that text wrapping accounts for sequences correctly.""" - @as_subprocess - def child(kind, lines=25, cols=80): - - # set the pty's virtual window size - val = struct.pack('HHHH', lines, cols, 0, 0) - fcntl.ioctl(sys.__stdout__.fileno(), termios.TIOCSWINSZ, val) - - # build a test paragraph, along with a very colorful version - t = TestTerminal(kind=kind) - pgraph = u' '.join( - ('a', 'ab', 'abc', 'abcd', 'abcde', 'abcdef', 'abcdefgh', - 'abcdefghi', 'abcdefghij', 'abcdefghijk', 'abcdefghijkl', - 'abcdefghijklm', 'abcdefghijklmn', 'abcdefghijklmno',) * 4) - pgraph_colored = u''.join([ - t.color(n % 7) + t.bold + ch - for n, ch in enumerate(pgraph)]) - - internal_wrapped = textwrap.wrap(pgraph, t.width, - break_long_words=False) - my_wrapped = t.wrap(pgraph) - my_wrapped_colored = t.wrap(pgraph_colored) - - # ensure we textwrap ascii the same as python - assert (internal_wrapped == my_wrapped) - - # ensure our first and last line wraps at its ends - first_l = internal_wrapped[0] - last_l = internal_wrapped[-1] - my_first_l = my_wrapped_colored[0] - my_last_l = my_wrapped_colored[-1] - assert (len(first_l) == t.length(my_first_l)) - assert (len(last_l) == t.length(my_last_l)) - assert (len(internal_wrapped[-1]) == t.length(my_wrapped_colored[-1])) - - child(kind=all_terms, lines=25, cols=many_columns) - - -def test_SequenceWrapper_27(all_terms): - """Test that text wrapping accounts for sequences correctly.""" - WIDTH = 27 - - @as_subprocess - def child(kind): - # build a test paragraph, along with a very colorful version - t = TestTerminal(kind=kind) - pgraph = u' '.join( - ('a', 'ab', 'abc', 'abcd', 'abcde', 'abcdef', 'abcdefgh', - 'abcdefghi', 'abcdefghij', 'abcdefghijk', 'abcdefghijkl', - 'abcdefghijklm', 'abcdefghijklmn', 'abcdefghijklmno ',) - * 8) + pgraph = u' '.join(( + 'a', 'bc', 'def', 'ghij', 'klmno', 'pqrstu', 'vwxyz012', + '34567890A', 'BCDEFGHIJK', 'LMNOPQRSTUV', 'WXYZabcdefgh', + 'ijklmnopqrstu', 'vwxyz123456789', '0ABCDEFGHIJKLMN ')) pgraph_colored = u''.join([ - t.color(n % 7) + t.bold + ch - for n, ch in enumerate(pgraph)]) + t.color(idx % 7)(char) if char != ' ' else ' ' + for idx, char in enumerate(pgraph)]) - internal_wrapped = textwrap.wrap(pgraph, width=WIDTH, - break_long_words=False, - drop_whitespace=False) - my_wrapped = t.wrap(pgraph, width=WIDTH, - break_long_words=False, - drop_whitespace=False) - my_wrapped_colored = t.wrap(pgraph_colored, width=WIDTH, - break_long_words=False, - drop_whitespace=False) + internal_wrapped = textwrap.wrap(pgraph, width=width, **kwargs) + my_wrapped = t.wrap(pgraph, width=width, **kwargs) + my_wrapped_colored = t.wrap(pgraph_colored, width=width, **kwargs) # ensure we textwrap ascii the same as python assert (internal_wrapped == my_wrapped) @@ -167,4 +95,4 @@ def child(kind): # ensure our colored textwrap is the same line length assert (len(internal_wrapped) == len(my_wrapped_colored)) - child(kind=all_terms) + child(all_terms, many_columns, kwargs) diff --git a/tox.ini b/tox.ini index 35227eef..a2a74ec8 100644 --- a/tox.ini +++ b/tox.ini @@ -12,14 +12,15 @@ skip_missing_interpreters = true whitelist_externals = /bin/bash /bin/mv setenv = PYTHONIOENCODING=UTF8 deps = pytest-flakes + pytest-random + pytest-xdist pytest-pep8 pytest-cov pytest mock commands = {envbindir}/py.test \ - -x --strict --pep8 --flakes \ - --junit-xml=results.{envname}.xml \ - --verbose --verbose \ + --random -n 8 --strict --pep8 --flakes \ + --junit-xml=results.{envname}.xml --verbose \ --cov blessed blessed/tests {posargs} /bin/mv {toxinidir}/.coverage {toxinidir}/.coverage.{envname} From aa0673509fd170a31fe3a97556e39312680a26f2 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 3 Jan 2015 16:48:22 -0800 Subject: [PATCH 234/459] add two new binary terminals --- blessed/_binterms.py | 2 ++ blessed/tests/accessories.py | 10 +++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/blessed/_binterms.py b/blessed/_binterms.py index 762e762d..ce6e2955 100644 --- a/blessed/_binterms.py +++ b/blessed/_binterms.py @@ -707,6 +707,7 @@ tvi950-rv-2p tvi950-rv-4p tvipt +unknown vanilla vc303 vc404 @@ -816,6 +817,7 @@ wy99gt wy99gt-25 wy99gt-vb +wy99gt-tek wyse-vp xerox1720 xerox820 diff --git a/blessed/tests/accessories.py b/blessed/tests/accessories.py index ed9ab3f9..703a06e2 100644 --- a/blessed/tests/accessories.py +++ b/blessed/tests/accessories.py @@ -32,11 +32,11 @@ many_columns_params = [1, 25, 50] from blessed._binterms import binary_terminals try: - all_terms_params = set(_term.split(None, 1)[0] for _term in - subprocess.check_output(('toe',)).splitlines() - ) - (set(binary_terminals) - if not os.environ.get('TEST_BINTERMS') - else set()) + all_terms_params = (set( + _term.split(None, 1)[0] for _term in + subprocess.check_output(('toe',)).splitlines() + ) - (set(binary_terminals) if not os.environ.get('TEST_BINTERMS') + else set())) except OSError: all_terms_params = ['screen', 'vt220', 'rxvt', 'cons25', 'linux', 'ansi'] From 9baa2230f40d92fa5494335af44a963196029671 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 3 Jan 2015 19:09:38 -0800 Subject: [PATCH 235/459] py3 fix and add 'xtalk', has a strange \x1b[7m --- blessed/_binterms.py | 3 ++- blessed/tests/accessories.py | 2 +- blessed/tests/test_length_sequence.py | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/blessed/_binterms.py b/blessed/_binterms.py index ce6e2955..84f58fe8 100644 --- a/blessed/_binterms.py +++ b/blessed/_binterms.py @@ -6,7 +6,7 @@ #: #: This may be generated by exporting TEST_BINTERMS, then analyzing the #: jUnit result xml written to the project folder. -binary_terminals = """ +binary_terminals = u""" 9term aaa+dec aaa+rv @@ -837,6 +837,7 @@ xnuppc+c xnuppc+c-100 xnuppc+c-30 +xtalk xtalk-100 xtalk-30 xterm+256color diff --git a/blessed/tests/accessories.py b/blessed/tests/accessories.py index 703a06e2..a877018d 100644 --- a/blessed/tests/accessories.py +++ b/blessed/tests/accessories.py @@ -33,7 +33,7 @@ from blessed._binterms import binary_terminals try: all_terms_params = (set( - _term.split(None, 1)[0] for _term in + _term.split(None, 1)[0].decode('ascii') for _term in subprocess.check_output(('toe',)).splitlines() ) - (set(binary_terminals) if not os.environ.get('TEST_BINTERMS') else set())) diff --git a/blessed/tests/test_length_sequence.py b/blessed/tests/test_length_sequence.py index 232775be..88096dcb 100644 --- a/blessed/tests/test_length_sequence.py +++ b/blessed/tests/test_length_sequence.py @@ -243,7 +243,7 @@ def child(lines=25, cols=80): @pytest.mark.skipif(platform.python_implementation() == 'PyPy', reason='PyPy fails TIOCSWINSZ') -def test_Sequence_alignment(all_terms, many_lines): +def test_Sequence_alignment(all_terms): """Tests methods related to Sequence class, namely ljust, rjust, center.""" @as_subprocess def child(kind, lines=25, cols=80): @@ -269,7 +269,7 @@ def child(kind, lines=25, cols=80): assert (t.length(radjusted.strip()) == pony_len) assert (t.length(radjusted) == len(pony_msg.rjust(t.width))) - child(kind=all_terms, lines=many_lines) + child(kind=all_terms) def test_sequence_is_movement_false(all_terms): From 3bc984e4da49890a3e3e9bcee18dc870015e8341 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 3 Jan 2015 19:17:40 -0800 Subject: [PATCH 236/459] make toe(1) generation py26-compatible --- blessed/tests/accessories.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/blessed/tests/accessories.py b/blessed/tests/accessories.py index a877018d..8cd1e1ec 100644 --- a/blessed/tests/accessories.py +++ b/blessed/tests/accessories.py @@ -34,7 +34,8 @@ try: all_terms_params = (set( _term.split(None, 1)[0].decode('ascii') for _term in - subprocess.check_output(('toe',)).splitlines() + subprocess.Popen(["toe"], stdout=subprocess.PIPE, close_fds=True) + .communicate()[0].splitlines() ) - (set(binary_terminals) if not os.environ.get('TEST_BINTERMS') else set())) except OSError: From f0e32dac3ce4649a8b54b49cb30bc71751459704 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 3 Jan 2015 19:19:19 -0800 Subject: [PATCH 237/459] remove -n 8; travis-ci has issues --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index a2a74ec8..b56b5dbc 100644 --- a/tox.ini +++ b/tox.ini @@ -19,7 +19,7 @@ deps = pytest-flakes pytest mock commands = {envbindir}/py.test \ - --random -n 8 --strict --pep8 --flakes \ + --random --strict --pep8 --flakes \ --junit-xml=results.{envname}.xml --verbose \ --cov blessed blessed/tests {posargs} /bin/mv {toxinidir}/.coverage {toxinidir}/.coverage.{envname} From fea70a07064c95ba8f191cc0f261859f44e793da Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 3 Jan 2015 19:22:31 -0800 Subject: [PATCH 238/459] travis-ci can't run test_inkey_0s_raw_ctrl_c --- blessed/tests/test_keyboard.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index 69fef7ce..fa0ba996 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -422,6 +422,8 @@ def test_inkey_0s_cbreak_multibyte_utf8(): assert math.floor(time.time() - stime) == 0.0 +@pytest.mark.skipif(os.environ.get('TRAVIS', None) is not None, + reason="travis-ci doesn't handle ^C well.") def test_inkey_0s_raw_ctrl_c(): "0-second inkey with raw allows receiving ^C." pid, master_fd = pty.fork() @@ -449,17 +451,9 @@ def test_inkey_0s_raw_ctrl_c(): stime = time.time() output = read_until_eof(master_fd) pid, status = os.waitpid(pid, 0) - if os.environ.get('TRAVIS', None) is not None: - # For some reason, setraw has no effect travis-ci, - # is still accepts ^C, causing system exit on py26, - # but exit 0 on py27, and either way on py33 - # .. strange, huh? - assert output in (u'', u'\x03') - assert os.WEXITSTATUS(status) in (0, 2) - else: - assert (output == u'\x03' or - output == u'' and not os.isatty(0)) - assert os.WEXITSTATUS(status) == 0 + assert (output == u'\x03' or + output == u'' and not os.isatty(0)) + assert os.WEXITSTATUS(status) == 0 assert math.floor(time.time() - stime) == 0.0 From 136102b0197ed051847b087b4175a8f36f519dfc Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 3 Jan 2015 19:24:29 -0800 Subject: [PATCH 239/459] add missing pytest module --- blessed/tests/test_keyboard.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index fa0ba996..6a2a25ce 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- "Tests for keyboard support." +# std imports import functools import tempfile try: @@ -11,11 +12,12 @@ import curses import time import math -import tty +import tty # NOQA import pty import sys import os +# local from .accessories import ( read_until_eof, read_until_semaphore, @@ -29,6 +31,8 @@ xterms, ) +# 3rd-party +import pytest import mock if sys.version_info[0] == 3: From a640b004655371681964b0f04d8911ab21cca88a Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 3 Jan 2015 19:26:26 -0800 Subject: [PATCH 240/459] appears travis-ci's toe(1) output is empty ? --- blessed/tests/accessories.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/blessed/tests/accessories.py b/blessed/tests/accessories.py index 8cd1e1ec..46a39720 100644 --- a/blessed/tests/accessories.py +++ b/blessed/tests/accessories.py @@ -31,16 +31,16 @@ many_lines_params = [30, 100] many_columns_params = [1, 25, 50] from blessed._binterms import binary_terminals +default_all_terms = ['screen', 'vt220', 'rxvt', 'cons25', 'linux', 'ansi'] try: - all_terms_params = (set( + all_terms_params = list(set( _term.split(None, 1)[0].decode('ascii') for _term in subprocess.Popen(["toe"], stdout=subprocess.PIPE, close_fds=True) .communicate()[0].splitlines() ) - (set(binary_terminals) if not os.environ.get('TEST_BINTERMS') - else set())) + else set())) or default_all_terms except OSError: - all_terms_params = ['screen', 'vt220', 'rxvt', - 'cons25', 'linux', 'ansi'] + all_terms_params = default_all_terms class as_subprocess(object): From 583a305528f00636442d4949f83dd97b7f03d3eb Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 3 Jan 2015 19:33:34 -0800 Subject: [PATCH 241/459] use 'with_everything' thanks @carlio https://github.com/landscapeio/prospector/issues/73#issuecomment-68531424 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index b56b5dbc..a519439a 100644 --- a/tox.ini +++ b/tox.ini @@ -25,7 +25,7 @@ commands = {envbindir}/py.test \ /bin/mv {toxinidir}/.coverage {toxinidir}/.coverage.{envname} [testenv:static_analysis] -deps = prospector[with_pep257,with_pyroma,with_vulture] +deps = prospector[with_everything] commands = prospector \ --die-on-tool-error \ --test-warnings \ From 1ee8b786cf0711f7d288ae13b5d74371aeac33ef Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 3 Jan 2015 22:04:15 -0800 Subject: [PATCH 242/459] We honor 80-col pep8 --- .prospector.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.prospector.yaml b/.prospector.yaml index 3ccfbdad..83c0c35e 100644 --- a/.prospector.yaml +++ b/.prospector.yaml @@ -29,8 +29,6 @@ pep257: pep8: # style checking run: true - options: - max-line-length: 100 pyflakes: # preferring 'frosted' instead (a fork of) From 58a381ccf551c4d5d796b17e1271f604e98b0234 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 3 Jan 2015 22:09:12 -0800 Subject: [PATCH 243/459] chose only 3 random-available terminal types for testing --- blessed/tests/accessories.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/blessed/tests/accessories.py b/blessed/tests/accessories.py index 46a39720..9c066884 100644 --- a/blessed/tests/accessories.py +++ b/blessed/tests/accessories.py @@ -7,6 +7,7 @@ import functools import traceback import termios +import random import codecs import curses import sys @@ -33,12 +34,18 @@ from blessed._binterms import binary_terminals default_all_terms = ['screen', 'vt220', 'rxvt', 'cons25', 'linux', 'ansi'] try: - all_terms_params = list(set( + available_terms = [ _term.split(None, 1)[0].decode('ascii') for _term in subprocess.Popen(["toe"], stdout=subprocess.PIPE, close_fds=True) - .communicate()[0].splitlines() - ) - (set(binary_terminals) if not os.environ.get('TEST_BINTERMS') - else set())) or default_all_terms + .communicate()[0].splitlines()] + if not os.environ.get('TEST_ALLTERMS'): + # we just pick 3 random terminal types, they're all as good as any so + # long as they're not in the binary_terminals list. + random.shuffle(available_terms) + available_terms = available_terms[:3] + all_terms_params = list(set(available_terms) - ( + set(binary_terminals) if not os.environ.get('TEST_BINTERMS') + else set())) or default_all_terms except OSError: all_terms_params = default_all_terms From 213c3a52a8f64ae147f4cb240a85a7f789bfff5d Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 3 Jan 2015 22:16:16 -0800 Subject: [PATCH 244/459] skip ^C test on pypy --- blessed/tests/test_keyboard.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index 6a2a25ce..c81154fd 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -8,6 +8,7 @@ except ImportError: import io StringIO = io.StringIO +import platform import signal import curses import time @@ -426,8 +427,9 @@ def test_inkey_0s_cbreak_multibyte_utf8(): assert math.floor(time.time() - stime) == 0.0 -@pytest.mark.skipif(os.environ.get('TRAVIS', None) is not None, - reason="travis-ci doesn't handle ^C well.") +@pytest.mark.skipif(os.environ.get('TRAVIS', None) is not None or + platform.python_implementation() == 'PyPy', + reason="travis-ci nor pypy handle ^C very well.") def test_inkey_0s_raw_ctrl_c(): "0-second inkey with raw allows receiving ^C." pid, master_fd = pty.fork() From 039dfa06461d3e71a6c7f07dbe0fc71788c38f05 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 3 Jan 2015 22:21:13 -0800 Subject: [PATCH 245/459] add pypy kind unicode workaround in __init__ we could fix the tests, but this might help somebody else out along the line. Why is pypy always so difficult ;/ --- blessed/terminal.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/blessed/terminal.py b/blessed/terminal.py index b3797194..1e17d736 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -4,6 +4,7 @@ import contextlib import functools import warnings +import platform import codecs import curses import locale @@ -180,7 +181,13 @@ def __init__(self, kind=None, stream=None, force_styling=False): # send them to stdout as a fallback, since they have to go # somewhere. try: - curses.setupterm(self._kind, self._init_descriptor) + if (platform.python_implementation() == 'PyPy' and + isinstance(self._kind, unicode)): + # pypy/2.4.0_2/libexec/lib_pypy/_curses.py, line 1131 + # TypeError: initializer for ctype 'char *' must be a str + curses.setupterm(self._kind.encode('ascii'), self._init_descriptor) + else: + curses.setupterm(self._kind, self._init_descriptor) except curses.error as err: warnings.warn('Failed to setupterm(kind={0!r}): {1}' .format(self._kind, err)) From e1c1aabf8e4f1f21e880c9108ab2441fd07f91d2 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 3 Jan 2015 22:22:29 -0800 Subject: [PATCH 246/459] no need for --random --- tox.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index a519439a..2c9ee50a 100644 --- a/tox.ini +++ b/tox.ini @@ -12,14 +12,13 @@ skip_missing_interpreters = true whitelist_externals = /bin/bash /bin/mv setenv = PYTHONIOENCODING=UTF8 deps = pytest-flakes - pytest-random pytest-xdist pytest-pep8 pytest-cov pytest mock commands = {envbindir}/py.test \ - --random --strict --pep8 --flakes \ + --strict --pep8 --flakes \ --junit-xml=results.{envname}.xml --verbose \ --cov blessed blessed/tests {posargs} /bin/mv {toxinidir}/.coverage {toxinidir}/.coverage.{envname} From ac7a413bdab796edce87a185c5b02a8d9f71dc59 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 3 Jan 2015 22:24:38 -0800 Subject: [PATCH 247/459] point coveralls.io to master branch only --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 7949ded9..e3c9a425 100644 --- a/README.rst +++ b/README.rst @@ -2,9 +2,9 @@ :alt: Travis Continous Integration :target: https://travis-ci.org/jquast/blessed -.. image:: https://img.shields.io/coveralls/jquast/blessed.svg +.. image:: https://coveralls.io/repos/jquast/blessed/badge.png?branch=master :alt: Coveralls Code Coverage - :target: https://coveralls.io/r/jquast/blessed + :target: https://coveralls.io/r/jquast/blessed?branch=master .. image:: https://img.shields.io/pypi/v/blessed.svg :alt: Latest Version From 77466f05d730ace9741632a9616f219fa7d3aa03 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 3 Jan 2015 22:30:35 -0800 Subject: [PATCH 248/459] try fixing pypy tox/virtualenv fail on linux with pip --upgrade --- tools/teamcity-runtests.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tools/teamcity-runtests.sh b/tools/teamcity-runtests.sh index 07b06e9b..a89c266a 100755 --- a/tools/teamcity-runtests.sh +++ b/tools/teamcity-runtests.sh @@ -8,6 +8,9 @@ set -o pipefail here=$(cd `dirname $0`; pwd) osrel=$(uname -s) +# ensure pip, virtualenv, and tox are up-to-date +pip install --upgrade pip virtualenv tox + # run tests cd $here/.. From e0111c8657ffbab53c65fe83ef5189a5926fa909 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 3 Jan 2015 22:33:18 -0800 Subject: [PATCH 249/459] Revert "try fixing pypy tox/virtualenv fail on linux with pip --upgrade" This reverts commit 77466f05d730ace9741632a9616f219fa7d3aa03. (We can't: OSError: [Errno 13] Permission denied: '/usr/bin/pip') --- tools/teamcity-runtests.sh | 3 --- 1 file changed, 3 deletions(-) diff --git a/tools/teamcity-runtests.sh b/tools/teamcity-runtests.sh index a89c266a..07b06e9b 100755 --- a/tools/teamcity-runtests.sh +++ b/tools/teamcity-runtests.sh @@ -8,9 +8,6 @@ set -o pipefail here=$(cd `dirname $0`; pwd) osrel=$(uname -s) -# ensure pip, virtualenv, and tox are up-to-date -pip install --upgrade pip virtualenv tox - # run tests cd $here/.. From a96479959d9dcc97651fef5a09149860f0e11877 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 3 Jan 2015 23:53:39 -0800 Subject: [PATCH 250/459] break_long_words=True now supported by term.wrap --- README.rst | 1 + blessed/sequences.py | 54 ++++++++++++++++++++++++++++++++++++++ blessed/tests/test_wrap.py | 24 ++++++++--------- docs/conf.py | 2 +- setup.py | 4 +-- tox.ini | 3 ++- 6 files changed, 72 insertions(+), 16 deletions(-) diff --git a/README.rst b/README.rst index e3c9a425..d805e7da 100644 --- a/README.rst +++ b/README.rst @@ -691,6 +691,7 @@ shares the same. See the LICENSE file. Version History =============== 1.9 + * enhancement: ``break_long_words=True`` now supported by ``term.wrap`` * workaround: ignore curses.error 'tparm() returned NULL', this occurs on win32 platforms using PDCurses_ where ``tparm()`` is not implemented. diff --git a/blessed/sequences.py b/blessed/sequences.py index 2862197d..b0de64ed 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -369,6 +369,60 @@ def _wrap_chunks(self, chunks): lines.append(indent + u''.join(cur_line)) return lines + def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width): + """_handle_long_word(chunks : [string], + cur_line : [string], + cur_len : int, width : int) + + Handle a chunk of text (most likely a word, not whitespace) that + is too long to fit in any line. + """ + # Figure out when indent is larger than the specified width, and make + # sure at least one character is stripped off on every pass + if width < 1: + space_left = 1 + else: + space_left = width - cur_len + + # If we're allowed to break long words, then do so: put as much + # of the next chunk onto the current line as will fit. + if self.break_long_words: + term = self.term + chunk = reversed_chunks[-1] + if (space_left == 0 or + space_left == 1 and chunk == u' '): + idx = space_left + else: + nxt = 0 + for idx in range(0, len(chunk)): + if idx == nxt: + # at sequence, point beyond it, + nxt = idx + measure_length(chunk[idx:], term) + if nxt <= idx: + # point beyond next sequence, if any, + # otherwise point to next character + nxt = idx + measure_length(chunk[idx:], term) + 1 + if Sequence(chunk[:nxt], term).length() > space_left: + break + else: + idx = space_left + + cur_line.append(reversed_chunks[-1][:idx]) + reversed_chunks[-1] = reversed_chunks[-1][idx:] + + # Otherwise, we have to preserve the long word intact. Only add + # it to the current line if there's nothing already there -- + # that minimizes how much we violate the width constraint. + elif not cur_line: + cur_line.append(reversed_chunks.pop()) + + # If we're not allowed to break long words, and there's already + # text on the current line, do nothing. Next time through the + # main loop of _wrap_chunks(), we'll wind up here again, but + # cur_len will be zero, so the next line will be entirely + # devoted to the long word that we can't handle right now. + + SequenceTextWrapper.__doc__ = textwrap.TextWrapper.__doc__ diff --git a/blessed/tests/test_wrap.py b/blessed/tests/test_wrap.py index dee3ebcd..1e12060a 100644 --- a/blessed/tests/test_wrap.py +++ b/blessed/tests/test_wrap.py @@ -48,18 +48,18 @@ def child(): dict(break_long_words=False, drop_whitespace=True, subsequent_indent=' '), - # dict(break_long_words=True, - # drop_whitespace=False, - # subsequent_indent=''), - # dict(break_long_words=True, - # drop_whitespace=True, - # subsequent_indent=''), - # dict(break_long_words=True, - # drop_whitespace=False, - # subsequent_indent=' '), - # dict(break_long_words=True, - # drop_whitespace=True, - # subsequent_indent=' '), + dict(break_long_words=True, + drop_whitespace=False, + subsequent_indent=''), + dict(break_long_words=True, + drop_whitespace=True, + subsequent_indent=''), + dict(break_long_words=True, + drop_whitespace=False, + subsequent_indent=' '), + dict(break_long_words=True, + drop_whitespace=True, + subsequent_indent=' '), ]) def test_SequenceWrapper(all_terms, many_columns, kwargs): """Test that text wrapping matches internal extra options.""" diff --git a/docs/conf.py b/docs/conf.py index 4ab6f413..6975d7f1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -53,7 +53,7 @@ # built documents. # # The short X.Y version. -version = '1.9.4' +version = '1.9.5' # The full version, including alpha/beta/rc tags. release = version diff --git a/setup.py b/setup.py index 338e2ffd..489ebed5 100755 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ def run(self): # ensure a virtualenv is loaded, assert os.getenv('VIRTUAL_ENV'), 'You should be in a virtualenv' # ensure tox is installed - subprocess.check_call(('pip', 'install', 'tox')) + subprocess.check_call(('pip', 'install', 'tox', 'ipython')) # install development egg-link setuptools.command.develop.develop.run(self) @@ -38,7 +38,7 @@ def main(): setuptools.setup( name='blessed', - version='1.9.4', + version='1.9.5', description="A feature-filled fork of Erik Rose's blessings project", long_description=open(os.path.join(here, 'README.rst')).read(), author='Jeff Quast', diff --git a/tox.ini b/tox.ini index 2c9ee50a..5c93fca1 100644 --- a/tox.ini +++ b/tox.ini @@ -20,7 +20,8 @@ deps = pytest-flakes commands = {envbindir}/py.test \ --strict --pep8 --flakes \ --junit-xml=results.{envname}.xml --verbose \ - --cov blessed blessed/tests {posargs} + --cov blessed blessed/tests --cov-report=term-missing \ + {posargs} /bin/mv {toxinidir}/.coverage {toxinidir}/.coverage.{envname} [testenv:static_analysis] From 78cbca896974fd7b6d412476a02421736d138422 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 4 Jan 2015 00:06:02 -0800 Subject: [PATCH 251/459] make termwrap test less computationally expensive --- blessed/tests/accessories.py | 2 +- blessed/tests/test_wrap.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/blessed/tests/accessories.py b/blessed/tests/accessories.py index 9c066884..f6970ae4 100644 --- a/blessed/tests/accessories.py +++ b/blessed/tests/accessories.py @@ -30,7 +30,7 @@ RECV_SEMAPHORE = b'SEMAPHORE\r\n' all_xterms_params = ['xterm', 'xterm-256color'] many_lines_params = [30, 100] -many_columns_params = [1, 25, 50] +many_columns_params = [1, 25] from blessed._binterms import binary_terminals default_all_terms = ['screen', 'vt220', 'rxvt', 'cons25', 'linux', 'ansi'] try: diff --git a/blessed/tests/test_wrap.py b/blessed/tests/test_wrap.py index 1e12060a..c253aa3f 100644 --- a/blessed/tests/test_wrap.py +++ b/blessed/tests/test_wrap.py @@ -68,9 +68,7 @@ def child(term, width, kwargs): # build a test paragraph, along with a very colorful version t = TestTerminal() pgraph = u' '.join(( - 'a', 'bc', 'def', 'ghij', 'klmno', 'pqrstu', 'vwxyz012', - '34567890A', 'BCDEFGHIJK', 'LMNOPQRSTUV', 'WXYZabcdefgh', - 'ijklmnopqrstu', 'vwxyz123456789', '0ABCDEFGHIJKLMN ')) + 'a', 'bc', 'vwxyz123456789ABCDEFGHIJKLMN ')) * 2 pgraph_colored = u''.join([ t.color(idx % 7)(char) if char != ' ' else ' ' From f568bfa98ace8216a18a19a284e8c8327170cb4e Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 4 Jan 2015 00:07:17 -0800 Subject: [PATCH 252/459] use linux-friendly form of ``toe -a`` --- blessed/tests/accessories.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blessed/tests/accessories.py b/blessed/tests/accessories.py index f6970ae4..a62645d3 100644 --- a/blessed/tests/accessories.py +++ b/blessed/tests/accessories.py @@ -36,7 +36,7 @@ try: available_terms = [ _term.split(None, 1)[0].decode('ascii') for _term in - subprocess.Popen(["toe"], stdout=subprocess.PIPE, close_fds=True) + subprocess.Popen(('toe', '-a'), stdout=subprocess.PIPE, close_fds=True) .communicate()[0].splitlines()] if not os.environ.get('TEST_ALLTERMS'): # we just pick 3 random terminal types, they're all as good as any so From 6d9e276115e0b5ef193a9da2b3867dd74258df14 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 4 Jan 2015 00:26:31 -0800 Subject: [PATCH 253/459] fix enter/exit fullscreen comment --- blessed/sequences.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blessed/sequences.py b/blessed/sequences.py index b0de64ed..8f9ed95e 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -103,7 +103,7 @@ def get_movement_sequence_patterns(term): re.escape(term.rc), # clear_screen: clear screen and home cursor re.escape(term.clear), - # cursor_up: Up one line + # enter/exit_fullscreen: switch to alternate screen buffer re.escape(term.enter_fullscreen), re.escape(term.exit_fullscreen), # forward cursor From 8f264cee6c24f3405752652719f73f3d4369bd12 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 4 Jan 2015 00:34:35 -0800 Subject: [PATCH 254/459] revert test paragraph, we have a bug to squash yet --- blessed/tests/accessories.py | 32 ++++++++++++++++---------------- blessed/tests/test_wrap.py | 4 +++- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/blessed/tests/accessories.py b/blessed/tests/accessories.py index a62645d3..408d2e7b 100644 --- a/blessed/tests/accessories.py +++ b/blessed/tests/accessories.py @@ -30,24 +30,24 @@ RECV_SEMAPHORE = b'SEMAPHORE\r\n' all_xterms_params = ['xterm', 'xterm-256color'] many_lines_params = [30, 100] -many_columns_params = [1, 25] +many_columns_params = [1, 10] from blessed._binterms import binary_terminals default_all_terms = ['screen', 'vt220', 'rxvt', 'cons25', 'linux', 'ansi'] -try: - available_terms = [ - _term.split(None, 1)[0].decode('ascii') for _term in - subprocess.Popen(('toe', '-a'), stdout=subprocess.PIPE, close_fds=True) - .communicate()[0].splitlines()] - if not os.environ.get('TEST_ALLTERMS'): - # we just pick 3 random terminal types, they're all as good as any so - # long as they're not in the binary_terminals list. - random.shuffle(available_terms) - available_terms = available_terms[:3] - all_terms_params = list(set(available_terms) - ( - set(binary_terminals) if not os.environ.get('TEST_BINTERMS') - else set())) or default_all_terms -except OSError: - all_terms_params = default_all_terms +if os.environ.get('TEST_ALLTERMS'): + try: + available_terms = [ + _term.split(None, 1)[0].decode('ascii') for _term in + subprocess.Popen(('toe', '-a'), + stdout=subprocess.PIPE, + close_fds=True) + .communicate()[0].splitlines()] + except OSError: + all_terms_params = default_all_terms +else: + available_terms = default_all_terms +all_terms_params = list(set(available_terms) - ( + set(binary_terminals) if not os.environ.get('TEST_BINTERMS') + else set())) or default_all_terms class as_subprocess(object): diff --git a/blessed/tests/test_wrap.py b/blessed/tests/test_wrap.py index c253aa3f..1e12060a 100644 --- a/blessed/tests/test_wrap.py +++ b/blessed/tests/test_wrap.py @@ -68,7 +68,9 @@ def child(term, width, kwargs): # build a test paragraph, along with a very colorful version t = TestTerminal() pgraph = u' '.join(( - 'a', 'bc', 'vwxyz123456789ABCDEFGHIJKLMN ')) * 2 + 'a', 'bc', 'def', 'ghij', 'klmno', 'pqrstu', 'vwxyz012', + '34567890A', 'BCDEFGHIJK', 'LMNOPQRSTUV', 'WXYZabcdefgh', + 'ijklmnopqrstu', 'vwxyz123456789', '0ABCDEFGHIJKLMN ')) pgraph_colored = u''.join([ t.color(idx % 7)(char) if char != ' ' else ' ' From a3c5b7d0b23ecfb55b06e2e600c86d16f6bad4d4 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 4 Jan 2015 13:32:58 -0800 Subject: [PATCH 255/459] bugfix textwrap when a 'word' ends with a sequence --- .gitignore | 2 +- .prospector.yaml | 3 +++ blessed/sequences.py | 34 +++++++++++------------- blessed/tests/accessories.py | 2 +- blessed/tests/test_sequences.py | 1 - blessed/tests/test_wrap.py | 47 +++++++++++++++++++-------------- 6 files changed, 48 insertions(+), 41 deletions(-) diff --git a/.gitignore b/.gitignore index e30e3478..06cf26ba 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .coverage +.coverage.* .cache .tox *.egg-info @@ -10,4 +11,3 @@ dist docs/_build htmlcov .coveralls.yml -._coverage* diff --git a/.prospector.yaml b/.prospector.yaml index 83c0c35e..d6714ed2 100644 --- a/.prospector.yaml +++ b/.prospector.yaml @@ -4,6 +4,9 @@ inherits: ignore: - (^|/)\..+ - ^docs/ + # ignore tests and bin/ for the moment, their quality does not so much matter. + - ^bin/ + - ^blessed/tests/ test-warnings: true diff --git a/blessed/sequences.py b/blessed/sequences.py index 8f9ed95e..9353da83 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -389,26 +389,24 @@ def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width): if self.break_long_words: term = self.term chunk = reversed_chunks[-1] - if (space_left == 0 or - space_left == 1 and chunk == u' '): - idx = space_left + nxt = 0 + for idx in range(0, len(chunk)): + if idx == nxt: + # at sequence, point beyond it, + nxt = idx + measure_length(chunk[idx:], term) + if nxt <= idx: + # point beyond next sequence, if any, + # otherwise point to next character + nxt = idx + measure_length(chunk[idx:], term) + 1 + if Sequence(chunk[:nxt], term).length() > space_left: + break else: - nxt = 0 - for idx in range(0, len(chunk)): - if idx == nxt: - # at sequence, point beyond it, - nxt = idx + measure_length(chunk[idx:], term) - if nxt <= idx: - # point beyond next sequence, if any, - # otherwise point to next character - nxt = idx + measure_length(chunk[idx:], term) + 1 - if Sequence(chunk[:nxt], term).length() > space_left: - break - else: - idx = space_left + # our text ends with a sequence, such as in text + # u'!\x1b(B\x1b[m', set index at at end (nxt) + idx = nxt - cur_line.append(reversed_chunks[-1][:idx]) - reversed_chunks[-1] = reversed_chunks[-1][idx:] + cur_line.append(chunk[:idx]) + reversed_chunks[-1] = chunk[idx:] # Otherwise, we have to preserve the long word intact. Only add # it to the current line if there's nothing already there -- diff --git a/blessed/tests/accessories.py b/blessed/tests/accessories.py index 408d2e7b..f7e2a428 100644 --- a/blessed/tests/accessories.py +++ b/blessed/tests/accessories.py @@ -36,7 +36,7 @@ if os.environ.get('TEST_ALLTERMS'): try: available_terms = [ - _term.split(None, 1)[0].decode('ascii') for _term in + _term.split(None, 1)[0] for _term in subprocess.Popen(('toe', '-a'), stdout=subprocess.PIPE, close_fds=True) diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index 6de77ec7..b15426cb 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -19,7 +19,6 @@ unicode_parm, many_columns, unicode_cap, - many_lines, ) # 3rd-party diff --git a/blessed/tests/test_wrap.py b/blessed/tests/test_wrap.py index 1e12060a..e5f7a15b 100644 --- a/blessed/tests/test_wrap.py +++ b/blessed/tests/test_wrap.py @@ -21,9 +21,9 @@ def test_SequenceWrapper_invalid_width(): @as_subprocess def child(): - t = TestTerminal() + term = TestTerminal() try: - my_wrapped = t.wrap(u'------- -------------', WIDTH) + my_wrapped = term.wrap(u'------- -------------', WIDTH) except ValueError as err: assert err.args[0] == ( "invalid width %r(%s) (must be integer > 0)" % ( @@ -66,33 +66,40 @@ def test_SequenceWrapper(all_terms, many_columns, kwargs): @as_subprocess def child(term, width, kwargs): # build a test paragraph, along with a very colorful version - t = TestTerminal() - pgraph = u' '.join(( - 'a', 'bc', 'def', 'ghij', 'klmno', 'pqrstu', 'vwxyz012', - '34567890A', 'BCDEFGHIJK', 'LMNOPQRSTUV', 'WXYZabcdefgh', - 'ijklmnopqrstu', 'vwxyz123456789', '0ABCDEFGHIJKLMN ')) + term = TestTerminal() + pgraph = u' Z! a bc defghij klmnopqrstuvw<<>>xyz012345678900 ' + attributes = ('bright_red', 'on_bright_blue', 'underline', 'reverse', + 'red_reverse', 'red_on_white', 'superscript', + 'subscript', 'on_bright_white') + term.bright_red('x') + term.on_bright_blue('x') + term.underline('x') + term.reverse('x') + term.red_reverse('x') + term.red_on_white('x') + term.superscript('x') + term.subscript('x') + term.on_bright_white('x') pgraph_colored = u''.join([ - t.color(idx % 7)(char) if char != ' ' else ' ' + getattr(term, (attributes[idx % len(attributes)]))(char) + if char != u' ' else u' ' for idx, char in enumerate(pgraph)]) internal_wrapped = textwrap.wrap(pgraph, width=width, **kwargs) - my_wrapped = t.wrap(pgraph, width=width, **kwargs) - my_wrapped_colored = t.wrap(pgraph_colored, width=width, **kwargs) + my_wrapped = term.wrap(pgraph, width=width, **kwargs) + my_wrapped_colored = term.wrap(pgraph_colored, width=width, **kwargs) # ensure we textwrap ascii the same as python - assert (internal_wrapped == my_wrapped) + assert internal_wrapped == my_wrapped - # ensure our first and last line wraps at its ends - first_l = internal_wrapped[0] - last_l = internal_wrapped[-1] - my_first_l = my_wrapped_colored[0] - my_last_l = my_wrapped_colored[-1] - assert (len(first_l) == t.length(my_first_l)) - assert (len(last_l) == t.length(my_last_l)) - assert (len(internal_wrapped[-1]) == t.length(my_wrapped_colored[-1])) + # ensure content matches for each line, when the sequences are + # stripped back off of each line + for line_no, (left, right) in enumerate( + zip(internal_wrapped, my_wrapped_colored)): + assert left == term.strip_seqs(right) - # ensure our colored textwrap is the same line length + # ensure our colored textwrap is the same paragraph length assert (len(internal_wrapped) == len(my_wrapped_colored)) child(all_terms, many_columns, kwargs) From a843591bdeceaf48475fdb49910953ed21de8f1c Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 4 Jan 2015 13:52:01 -0800 Subject: [PATCH 256/459] add sudo: false for faster travis-ci builds --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index a716b6be..80ce9cbf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,6 @@ language: python +# http://blog.travis-ci.com/2014-12-17-faster-builds-with-container-based-infrastructure/ +sudo: false env: - TOXENV=py26 From 1965724fb28cd08522e2443bf5960ec82dda5865 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 4 Jan 2015 14:02:48 -0800 Subject: [PATCH 257/459] fix the rename thing --- blessed/terminal.py | 931 +++++++++++++++++++++++++--------------- blessed/terminal.py.bak | 785 --------------------------------- 2 files changed, 578 insertions(+), 1138 deletions(-) delete mode 100644 blessed/terminal.py.bak diff --git a/blessed/terminal.py b/blessed/terminal.py index a2254b1b..1e17d736 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -1,42 +1,72 @@ -"""A thin, practical wrapper around terminal coloring, styling, and -positioning""" - -from contextlib import contextmanager +"This primary module provides the Terminal class." +# standard modules +import collections +import contextlib +import functools +import warnings +import platform +import codecs import curses -from curses import setupterm, tigetnum, tigetstr, tparm -from fcntl import ioctl +import locale +import select +import struct +import time +import sys +import os + +try: + import termios + import fcntl + import tty +except ImportError: + tty_methods = ('setraw', 'cbreak', 'kbhit', 'height', 'width') + msg_nosupport = ( + "One or more of the modules: 'termios', 'fcntl', and 'tty' " + "are not found on your platform '{0}'. The following methods " + "of Terminal are dummy/no-op unless a deriving class overrides " + "them: {1}".format(sys.platform.lower(), ', '.join(tty_methods))) + warnings.warn(msg_nosupport) + HAS_TTY = False +else: + HAS_TTY = True try: from io import UnsupportedOperation as IOUnsupportedOperation except ImportError: class IOUnsupportedOperation(Exception): """A dummy exception to take the place of Python 3's - ``io.UnsupportedOperation`` in Python 2""" - -from os import isatty, environ -from platform import python_version_tuple -import struct -import sys -from termios import TIOCGWINSZ - + ``io.UnsupportedOperation`` in Python 2.5""" -__all__ = ['Terminal'] - - -if ('3', '0', '0') <= python_version_tuple() < ('3', '2', '2+'): # Good till - # 3.2.10 - # Python 3.x < 3.2.3 has a bug in which tparm() erroneously takes a string. - raise ImportError('Blessings needs Python 3.2.3 or greater for Python 3 ' - 'support due to http://bugs.python.org/issue10570.') +try: + _ = InterruptedError + del _ +except NameError: + # alias py2 exception to py3 + InterruptedError = select.error + +# local imports +from .formatters import ( + ParameterizingString, + NullCallableString, + resolve_capability, + resolve_attribute, +) + +from .sequences import ( + init_sequence_patterns, + SequenceTextWrapper, + Sequence, +) + +from .keyboard import ( + get_keyboard_sequences, + get_keyboard_codes, + resolve_sequence, +) class Terminal(object): - """An abstraction around terminal capabilities - - Unlike curses, this doesn't require clearing the screen before doing - anything, and it's friendlier to use. It keeps the endless calls to - ``tigetstr()`` and ``tparm()`` out of your code, and it acts intelligently - when somebody pipes your output to a non-terminal. + """A wrapper for curses and related terminfo(5) terminal capabilities. Instance attributes: @@ -45,8 +75,44 @@ class Terminal(object): around with the terminal; it's almost always needed when the terminal is and saves sticking lots of extra args on client functions in practice. - """ + + #: Sugary names for commonly-used capabilities + _sugar = dict( + save='sc', + restore='rc', + # 'clear' clears the whole screen. + clear_eol='el', + clear_bol='el1', + clear_eos='ed', + position='cup', # deprecated + enter_fullscreen='smcup', + exit_fullscreen='rmcup', + move='cup', + move_x='hpa', + move_y='vpa', + move_left='cub1', + move_right='cuf1', + move_up='cuu1', + move_down='cud1', + hide_cursor='civis', + normal_cursor='cnorm', + reset_colors='op', # oc doesn't work on my OS X terminal. + normal='sgr0', + reverse='rev', + italic='sitm', + no_italic='ritm', + shadow='sshm', + no_shadow='rshm', + standout='smso', + no_standout='rmso', + subscript='ssubm', + no_subscript='rsubm', + superscript='ssupm', + no_superscript='rsupm', + underline='smul', + no_underline='rmul') + def __init__(self, kind=None, stream=None, force_styling=False): """Initialize the terminal. @@ -75,172 +141,186 @@ def __init__(self, kind=None, stream=None, force_styling=False): ``force_styling=None``. """ - if stream is None: + global _CUR_TERM + self.keyboard_fd = None + + # default stream is stdout, keyboard only valid as stdin when + # output stream is stdout and output stream is a tty + if stream is None or stream == sys.__stdout__: stream = sys.__stdout__ + self.keyboard_fd = sys.__stdin__.fileno() + try: - stream_descriptor = (stream.fileno() if hasattr(stream, 'fileno') - and callable(stream.fileno) - else None) + stream_fd = (stream.fileno() if hasattr(stream, 'fileno') + and callable(stream.fileno) else None) except IOUnsupportedOperation: - stream_descriptor = None + stream_fd = None - self._is_a_tty = (stream_descriptor is not None and - isatty(stream_descriptor)) + self._is_a_tty = stream_fd is not None and os.isatty(stream_fd) self._does_styling = ((self.is_a_tty or force_styling) and force_styling is not None) + # keyboard_fd only non-None if both stdin and stdout is a tty. + self.keyboard_fd = (self.keyboard_fd + if self.keyboard_fd is not None and + self.is_a_tty and os.isatty(self.keyboard_fd) + else None) + self._normal = None # cache normal attr, preventing recursive lookups + # The descriptor to direct terminal initialization sequences to. # sys.__stdout__ seems to always have a descriptor of 1, even if output # is redirected. - self._init_descriptor = (sys.__stdout__.fileno() - if stream_descriptor is None - else stream_descriptor) + self._init_descriptor = (stream_fd is None and sys.__stdout__.fileno() + or stream_fd) + self._kind = kind or os.environ.get('TERM', 'unknown') + if self.does_styling: # Make things like tigetstr() work. Explicit args make setupterm() # work even when -s is passed to nosetests. Lean toward sending # init sequences to the stream if it has a file descriptor, and # send them to stdout as a fallback, since they have to go # somewhere. - setupterm(kind or environ.get('TERM', 'unknown'), - self._init_descriptor) + try: + if (platform.python_implementation() == 'PyPy' and + isinstance(self._kind, unicode)): + # pypy/2.4.0_2/libexec/lib_pypy/_curses.py, line 1131 + # TypeError: initializer for ctype 'char *' must be a str + curses.setupterm(self._kind.encode('ascii'), self._init_descriptor) + else: + curses.setupterm(self._kind, self._init_descriptor) + except curses.error as err: + warnings.warn('Failed to setupterm(kind={0!r}): {1}' + .format(self._kind, err)) + self._kind = None + self._does_styling = False + else: + if _CUR_TERM is None or self._kind == _CUR_TERM: + _CUR_TERM = self._kind + else: + warnings.warn( + 'A terminal of kind "%s" has been requested; due to an' + ' internal python curses bug, terminal capabilities' + ' for a terminal of kind "%s" will continue to be' + ' returned for the remainder of this process.' % ( + self._kind, _CUR_TERM,)) + + for re_name, re_val in init_sequence_patterns(self).items(): + setattr(self, re_name, re_val) + + # build database of int code <=> KEY_NAME + self._keycodes = get_keyboard_codes() + + # store attributes as: self.KEY_NAME = code + for key_code, key_name in self._keycodes.items(): + setattr(self, key_name, key_code) + + # build database of sequence <=> KEY_NAME + self._keymap = get_keyboard_sequences(self) + + self._keyboard_buf = collections.deque() + if self.keyboard_fd is not None: + locale.setlocale(locale.LC_ALL, '') + self._encoding = locale.getpreferredencoding() or 'ascii' + try: + self._keyboard_decoder = codecs.getincrementaldecoder( + self._encoding)() + except LookupError as err: + warnings.warn('%s, fallback to ASCII for keyboard.' % (err,)) + self._encoding = 'ascii' + self._keyboard_decoder = codecs.getincrementaldecoder( + self._encoding)() self.stream = stream - # Sugary names for commonly-used capabilities, intended to help avoid trips - # to the terminfo man page and comments in your code: - _sugar = dict( - # Don't use "on" or "bright" as an underscore-separated chunk in any of - # these (e.g. on_cology or rock_on) so we don't interfere with - # __getattr__. - save='sc', - restore='rc', - - clear_eol='el', - clear_bol='el1', - clear_eos='ed', - # 'clear' clears the whole screen. - position='cup', # deprecated - enter_fullscreen='smcup', - exit_fullscreen='rmcup', - move='cup', - move_x='hpa', - move_y='vpa', - move_left='cub1', - move_right='cuf1', - move_up='cuu1', - move_down='cud1', - - hide_cursor='civis', - normal_cursor='cnorm', - - reset_colors='op', # oc doesn't work on my OS X terminal. - - normal='sgr0', - reverse='rev', - # 'bold' is just 'bold'. Similarly... - # blink - # dim - # flash - italic='sitm', - no_italic='ritm', - shadow='sshm', - no_shadow='rshm', - standout='smso', - no_standout='rmso', - subscript='ssubm', - no_subscript='rsubm', - superscript='ssupm', - no_superscript='rsupm', - underline='smul', - no_underline='rmul') - def __getattr__(self, attr): - """Return a terminal capability, like bold. - - For example, you can say ``term.bold`` to get the string that turns on - bold formatting and ``term.normal`` to get the string that turns it off - again. Or you can take a shortcut: ``term.bold('hi')`` bolds its - argument and sets everything to normal afterward. You can even combine - things: ``term.bold_underline_red_on_bright_green('yowzers!')``. + """Return a terminal capability as Unicode string. - For a parametrized capability like ``cup``, pass the parameters too: - ``some_term.cup(line, column)``. + For example, ``term.bold`` is a unicode string that may be prepended + to text to set the video attribute for bold, which should also be + terminated with the pairing ``term.normal``. - ``man terminfo`` for a complete list of capabilities. + This capability is also callable, so you can use ``term.bold("hi")`` + which results in the joining of (term.bold, "hi", term.normal). - Return values are always Unicode. + Compound formatters may also be used, for example: + ``term.bold_blink_red_on_green("merry x-mas!")``. + For a parametrized capability such as ``cup`` (cursor_address), pass + the parameters as arguments ``some_term.cup(line, column)``. See + manual page terminfo(5) for a complete list of capabilities. """ - resolution = (self._resolve_formatter(attr) if self.does_styling - else NullCallableString()) - setattr(self, attr, resolution) # Cache capability codes. - return resolution + if not self.does_styling: + return NullCallableString() + val = resolve_attribute(self, attr) + # Cache capability codes. + setattr(self, attr, val) + return val @property - def does_styling(self): - """Whether attempt to emit capabilities - - This is influenced by the ``is_a_tty`` property and by the - ``force_styling`` argument to the constructor. You can examine - this value to decide whether to draw progress bars or other frippery. + def kind(self): + """Name of this terminal type as string.""" + return self._kind - """ + @property + def does_styling(self): + """Whether this instance will emit terminal sequences (bool).""" return self._does_styling @property def is_a_tty(self): - """Whether my ``stream`` appears to be associated with a terminal""" + """Whether the ``stream`` associated with this instance is a terminal + (bool).""" return self._is_a_tty @property def height(self): - """The height of the terminal in characters - - If no stream or a stream not representing a terminal was passed in at - construction, return the dimension of the controlling terminal so - piping to things that eventually display on the terminal (like ``less - -R``) work. If a stream representing a terminal was passed in, return - the dimensions of that terminal. If there somehow is no controlling - terminal, return ``None``. (Thus, you should check that the property - ``is_a_tty`` is true before doing any math on the result.) + """T.height -> int + The height of the terminal in characters. """ - return self._height_and_width()[0] + return self._height_and_width().ws_row @property def width(self): - """The width of the terminal in characters + """T.width -> int + + The width of the terminal in characters. + """ + return self._height_and_width().ws_col + + @staticmethod + def _winsize(fd): + """T._winsize -> WINSZ(ws_row, ws_col, ws_xpixel, ws_ypixel) - See ``height()`` for some corner cases. + The tty connected by file desriptor fd is queried for its window size, + and returned as a collections.namedtuple instance WINSZ. + May raise exception IOError. """ - return self._height_and_width()[1] + if HAS_TTY: + data = fcntl.ioctl(fd, termios.TIOCGWINSZ, WINSZ._BUF) + return WINSZ(*struct.unpack(WINSZ._FMT, data)) + return WINSZ(ws_row=24, ws_col=80, ws_xpixel=0, ws_ypixel=0) def _height_and_width(self): """Return a tuple of (terminal height, terminal width). - - Start by trying TIOCGWINSZ (Terminal I/O-Control: Get Window Size), - falling back to environment variables (LINES, COLUMNS), and returning - (None, None) if those are unavailable or invalid. - """ - # tigetnum('lines') and tigetnum('cols') update only if we call - # setupterm() again. - for descriptor in self._init_descriptor, sys.__stdout__: + # TODO(jquast): hey kids, even if stdout is redirected to a file, + # we can still query sys.__stdin__.fileno() for our terminal size. + # -- of course, if both are redirected, we have no use for this fd. + for fd in (self._init_descriptor, sys.__stdout__): try: - return struct.unpack( - 'hhhh', ioctl(descriptor, TIOCGWINSZ, '\000' * 8))[0:2] + if fd is not None: + return self._winsize(fd) except IOError: - # when the output stream or init descriptor is not a tty, such - # as when when stdout is piped to another program, fe. tee(1), - # these ioctls will raise IOError pass - try: - return int(environ.get('LINES')), int(environ.get('COLUMNS')) - except TypeError: - return None, None - @contextmanager + return WINSZ(ws_row=int(os.getenv('LINES', '25')), + ws_col=int(os.getenv('COLUMNS', '80')), + ws_xpixel=None, + ws_ypixel=None) + + @contextlib.contextmanager def location(self, x=None, y=None): """Return a context manager for temporarily moving the cursor. @@ -274,20 +354,29 @@ def location(self, x=None, y=None): # Restore original cursor position: self.stream.write(self.restore) - @contextmanager + @contextlib.contextmanager def fullscreen(self): """Return a context manager that enters fullscreen mode while inside it - and restores normal mode on leaving.""" + and restores normal mode on leaving. + + Fullscreen mode is characterized by instructing the terminal emulator + to store and save the current screen state (all screen output), switch + to "alternate screen". Upon exiting, the previous screen state is + returned. + + This call may not be tested; only one screen state may be saved at a + time. + """ self.stream.write(self.enter_fullscreen) try: yield finally: self.stream.write(self.exit_fullscreen) - @contextmanager + @contextlib.contextmanager def hidden_cursor(self): - """Return a context manager that hides the cursor while inside it and - makes it visible on leaving.""" + """Return a context manager that hides the cursor upon entering, + and makes it visible again upon exiting.""" self.stream.write(self.hide_cursor) try: yield @@ -296,9 +385,9 @@ def hidden_cursor(self): @property def color(self): - """Return a capability that sets the foreground color. + """Returns capability that sets the foreground color. - The capability is unparametrized until called and passed a number + The capability is unparameterized until called and passed a number (0-15), at which point it returns another string which represents a specific color change. This second string can further be called to color a piece of text and set everything back to normal afterward. @@ -306,95 +395,39 @@ def color(self): :arg num: The number, 0-15, of the color """ - return ParametrizingString(self._foreground_color, self.normal) + if not self.does_styling: + return NullCallableString() + return ParameterizingString(self._foreground_color, + self.normal, 'color') @property def on_color(self): - """Return a capability that sets the background color. - - See ``color()``. + "Returns capability that sets the background color." + if not self.does_styling: + return NullCallableString() + return ParameterizingString(self._background_color, + self.normal, 'on_color') - """ - return ParametrizingString(self._background_color, self.normal) + @property + def normal(self): + "Returns sequence that resets video attribute." + if self._normal: + return self._normal + self._normal = resolve_capability(self, 'normal') + return self._normal @property def number_of_colors(self): """Return the number of colors the terminal supports. - Common values are 0, 8, 16, 88, and 256. - - Though the underlying capability returns -1 when there is no color - support, we return 0. This lets you test more Pythonically:: + Common values are 0, 8, 16, 88, and 256. Most commonly + this may be used to test color capabilities at all:: if term.number_of_colors: - ... - - We also return 0 if the terminal won't tell us how many colors it - supports, which I think is rare. - - """ - # This is actually the only remotely useful numeric capability. We - # don't name it after the underlying capability, because we deviate - # slightly from its behavior, and we might someday wish to give direct - # access to it. - colors = tigetnum('colors') # Returns -1 if no color support, -2 if no - # such cap. - # self.__dict__['colors'] = ret # Cache it. It's not changing. - # (Doesn't work.) - return colors if colors >= 0 else 0 - - def _resolve_formatter(self, attr): - """Resolve a sugary or plain capability name, color, or compound - formatting function name into a callable capability. - - Return a ``ParametrizingString`` or a ``FormattingString``. - - """ - if attr in COLORS: - return self._resolve_color(attr) - elif attr in COMPOUNDABLES: - # Bold, underline, or something that takes no parameters - return self._formatting_string(self._resolve_capability(attr)) - else: - formatters = split_into_formatters(attr) - if all(f in COMPOUNDABLES for f in formatters): - # It's a compound formatter, like "bold_green_on_red". Future - # optimization: combine all formatting into a single escape - # sequence. - return self._formatting_string( - u''.join(self._resolve_formatter(s) for s in formatters)) - else: - return ParametrizingString(self._resolve_capability(attr)) - - def _resolve_capability(self, atom): - """Return a terminal code for a capname or a sugary name, or an empty - Unicode. - - The return value is always Unicode, because otherwise it is clumsy - (especially in Python 3) to concatenate with real (Unicode) strings. - - """ - code = tigetstr(self._sugar.get(atom, atom)) - if code: - # See the comment in ParametrizingString for why this is latin1. - return code.decode('latin1') - return u'' - - def _resolve_color(self, color): - """Resolve a color like red or on_bright_green into a callable - capability.""" - # TODO: Does curses automatically exchange red and blue and cyan and - # yellow when a terminal supports setf/setb rather than setaf/setab? - # I'll be blasted if I can find any documentation. The following - # assumes it does. - color_cap = (self._background_color if 'on_' in color else - self._foreground_color) - # curses constants go up to only 7, so add an offset to get at the - # bright colors at 8-15: - offset = 8 if 'bright_' in color else 0 - base_color = color.rsplit('_', 1)[-1] - return self._formatting_string( - color_cap(getattr(curses, 'COLOR_' + base_color.upper()) + offset)) + ...""" + # trim value to 0, as tigetnum('colors') returns -1 if no support, + # -2 if no such capability. + return max(0, self.does_styling and curses.tigetnum('colors') or -1) @property def _foreground_color(self): @@ -404,157 +437,349 @@ def _foreground_color(self): def _background_color(self): return self.setab or self.setb - def _formatting_string(self, formatting): - """Return a new ``FormattingString`` which implicitly receives my - notion of "normal".""" - return FormattingString(formatting, self.normal) + def ljust(self, text, width=None, fillchar=u' '): + """T.ljust(text, [width], [fillchar]) -> unicode + + Return string ``text``, left-justified by printable length ``width``. + Padding is done using the specified fill character (default is a + space). Default ``width`` is the attached terminal's width. ``text`` + may contain terminal sequences.""" + if width is None: + width = self.width + return Sequence(text, self).ljust(width, fillchar) + + def rjust(self, text, width=None, fillchar=u' '): + """T.rjust(text, [width], [fillchar]) -> unicode + + Return string ``text``, right-justified by printable length ``width``. + Padding is done using the specified fill character (default is a + space). Default ``width`` is the attached terminal's width. ``text`` + may contain terminal sequences.""" + if width is None: + width = self.width + return Sequence(text, self).rjust(width, fillchar) + + def center(self, text, width=None, fillchar=u' '): + """T.center(text, [width], [fillchar]) -> unicode + + Return string ``text``, centered by printable length ``width``. + Padding is done using the specified fill character (default is a + space). Default ``width`` is the attached terminal's width. ``text`` + may contain terminal sequences.""" + if width is None: + width = self.width + return Sequence(text, self).center(width, fillchar) + + def length(self, text): + """T.length(text) -> int + + Return the printable length of string ``text``, which may contain + terminal sequences. Strings containing sequences such as 'clear', + which repositions the cursor, does not give accurate results, and + their printable length is evaluated *0*.. + """ + return Sequence(text, self).length() + + def strip(self, text, chars=None): + """T.strip(text) -> unicode + Return string ``text`` with terminal sequences removed, and leading + and trailing whitespace removed. + """ + return Sequence(text, self).strip(chars) -def derivative_colors(colors): - """Return the names of valid color variants, given the base colors.""" - return set([('on_' + c) for c in colors] + - [('bright_' + c) for c in colors] + - [('on_bright_' + c) for c in colors]) + def rstrip(self, text, chars=None): + """T.rstrip(text) -> unicode + Return string ``text`` with terminal sequences and trailing whitespace + removed. + """ + return Sequence(text, self).rstrip(chars) -COLORS = set(['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', - 'white']) -COLORS.update(derivative_colors(COLORS)) -COMPOUNDABLES = (COLORS | - set(['bold', 'underline', 'reverse', 'blink', 'dim', 'italic', - 'shadow', 'standout', 'subscript', 'superscript'])) + def lstrip(self, text, chars=None): + """T.lstrip(text) -> unicode + + Return string ``text`` with terminal sequences and leading whitespace + removed. + """ + return Sequence(text, self).lstrip(chars) + def strip_seqs(self, text): + """T.strip_seqs(text) -> unicode -class ParametrizingString(unicode): - """A Unicode string which can be called to parametrize it as a terminal - capability""" + Return string ``text`` stripped only of its sequences. + """ + return Sequence(text, self).strip_seqs() - def __new__(cls, formatting, normal=None): - """Instantiate. + def wrap(self, text, width=None, **kwargs): + """T.wrap(text, [width=None, **kwargs ..]) -> list[unicode] - :arg normal: If non-None, indicates that, once parametrized, this can - be used as a ``FormattingString``. The value is used as the - "normal" capability. + Wrap paragraphs containing escape sequences ``text`` to the full + ``width`` of Terminal instance *T*, unless ``width`` is specified. + Wrapped by the virtual printable length, irregardless of the video + attribute sequences it may contain, allowing text containing colors, + bold, underline, etc. to be wrapped. + Returns a list of strings that may contain escape sequences. See + ``textwrap.TextWrapper`` for all available additional kwargs to + customize wrapping behavior such as ``subsequent_indent``. """ - new = unicode.__new__(cls, formatting) - new._normal = normal - return new + width = self.width if width is None else width + lines = [] + for line in text.splitlines(): + lines.extend( + (_linewrap for _linewrap in SequenceTextWrapper( + width=width, term=self, **kwargs).wrap(text)) + if line.strip() else (u'',)) - def __call__(self, *args): - try: - # Re-encode the cap, because tparm() takes a bytestring in Python - # 3. However, appear to be a plain Unicode string otherwise so - # concats work. - # - # We use *latin1* encoding so that bytes emitted by tparm are - # encoded to their native value: some terminal kinds, such as - # 'avatar' or 'kermit', emit 8-bit bytes in range 0x7f to 0xff. - # latin1 leaves these values unmodified in their conversion to - # unicode byte values. The terminal emulator will "catch" and - # handle these values, even if emitting utf8-encoded text, where - # these bytes would otherwise be illegal utf8 start bytes. - parametrized = tparm(self.encode('latin1'), *args).decode('latin1') - return (parametrized if self._normal is None else - FormattingString(parametrized, self._normal)) - except curses.error: - # Catch "must call (at least) setupterm() first" errors, as when - # running simply `nosetests` (without progressive) on nose- - # progressive. Perhaps the terminal has gone away between calling - # tigetstr and calling tparm. - return u'' - except TypeError: - # If the first non-int (i.e. incorrect) arg was a string, suggest - # something intelligent: - if len(args) == 1 and isinstance(args[0], basestring): - raise TypeError( - 'A native or nonexistent capability template received ' - '%r when it was expecting ints. You probably misspelled a ' - 'formatting call like bright_red_on_white(...).' % args) - else: - # Somebody passed a non-string; I don't feel confident - # guessing what they were trying to do. - raise + return lines + def getch(self): + """T.getch() -> unicode -class FormattingString(unicode): - """A Unicode string which can be called upon a piece of text to wrap it in - formatting""" + Read and decode next byte from keyboard stream. May return u'' + if decoding is not yet complete, or completed unicode character. + Should always return bytes when self.kbhit() returns True. + + Implementors of input streams other than os.read() on the stdin fd + should derive and override this method. + """ + assert self.keyboard_fd is not None + byte = os.read(self.keyboard_fd, 1) + return self._keyboard_decoder.decode(byte, final=False) - def __new__(cls, formatting, normal): - new = unicode.__new__(cls, formatting) - new._normal = normal - return new + def kbhit(self, timeout=None, _intr_continue=True): + """T.kbhit([timeout=None]) -> bool - def __call__(self, text): - """Return a new string that is ``text`` formatted with my contents. + Returns True if a keypress has been detected on keyboard. - At the beginning of the string, I prepend the formatting that is my - contents. At the end, I append the "normal" sequence to set everything - back to defaults. The return value is always a Unicode. + When ``timeout`` is 0, this call is non-blocking, Otherwise blocking + until keypress is detected (default). When ``timeout`` is a positive + number, returns after ``timeout`` seconds have elapsed. + If input is not a terminal, False is always returned. """ - return self + text + self._normal + # Special care is taken to handle a custom SIGWINCH handler, which + # causes select() to be interrupted with errno 4 (EAGAIN) -- + # it is ignored, and a new timeout value is derived from the previous, + # unless timeout becomes negative, because signal handler has blocked + # beyond timeout, then False is returned. Otherwise, when timeout is 0, + # we continue to block indefinitely (default). + stime = time.time() + check_w, check_x, ready_r = [], [], [None, ] + check_r = [self.keyboard_fd] if self.keyboard_fd is not None else [] + + while HAS_TTY and True: + try: + ready_r, ready_w, ready_x = select.select( + check_r, check_w, check_x, timeout) + except InterruptedError: + if not _intr_continue: + return u'' + if timeout is not None: + # subtract time already elapsed, + timeout -= time.time() - stime + if timeout > 0: + continue + # no time remains after handling exception (rare) + ready_r = [] + break + else: + break + return False if self.keyboard_fd is None else check_r == ready_r -class NullCallableString(unicode): - """A dummy callable Unicode to stand in for ``FormattingString`` and - ``ParametrizingString`` + @contextlib.contextmanager + def cbreak(self): + """Return a context manager that enters 'cbreak' mode: disabling line + buffering of keyboard input, making characters typed by the user + immediately available to the program. Also referred to as 'rare' + mode, this is the opposite of 'cooked' mode, the default for most + shells. - We use this when there is no tty and thus all capabilities should be blank. + In 'cbreak' mode, echo of input is also disabled: the application must + explicitly print any input received, if they so wish. - """ - def __new__(cls): - new = unicode.__new__(cls, u'') - return new - - def __call__(self, *args): - """Return a Unicode or whatever you passed in as the first arg - (hopefully a string of some kind). - - When called with an int as the first arg, return an empty Unicode. An - int is a good hint that I am a ``ParametrizingString``, as there are - only about half a dozen string-returning capabilities on OS X's - terminfo man page which take any param that's not an int, and those are - seldom if ever used on modern terminal emulators. (Most have to do with - programming function keys. Blessings' story for supporting - non-string-returning caps is undeveloped.) And any parametrized - capability in a situation where all capabilities themselves are taken - to be blank are, of course, themselves blank. - - When called with a non-int as the first arg (no no args at all), return - the first arg. I am acting as a ``FormattingString``. + More information can be found in the manual page for curses.h, + http://www.openbsd.org/cgi-bin/man.cgi?query=cbreak + The python manual for curses, + http://docs.python.org/2/library/curses.html + + Note also that setcbreak sets VMIN = 1 and VTIME = 0, + http://www.unixwiz.net/techtips/termios-vmin-vtime.html """ - if len(args) != 1 or isinstance(args[0], int): - # I am acting as a ParametrizingString. + if HAS_TTY and self.keyboard_fd is not None: + # save current terminal mode, + save_mode = termios.tcgetattr(self.keyboard_fd) + tty.setcbreak(self.keyboard_fd, termios.TCSANOW) + try: + yield + finally: + # restore prior mode, + termios.tcsetattr(self.keyboard_fd, + termios.TCSAFLUSH, + save_mode) + else: + yield - # tparm can take not only ints but also (at least) strings as its - # second...nth args. But we don't support callably parametrizing - # caps that take non-ints yet, so we can cheap out here. TODO: Go - # through enough of the motions in the capability resolvers to - # determine which of 2 special-purpose classes, - # NullParametrizableString or NullFormattingString, to return, and - # retire this one. - return u'' - return args[0] # Should we force even strs in Python 2.x to be - # unicodes? No. How would I know what encoding to use - # to convert it? + @contextlib.contextmanager + def raw(self): + """Return a context manager that enters *raw* mode. Raw mode is + similar to *cbreak* mode, in that characters typed are immediately + available to ``inkey()`` with one exception: the interrupt, quit, + suspend, and flow control characters are all passed through as their + raw character values instead of generating a signal. + """ + if HAS_TTY and self.keyboard_fd is not None: + # save current terminal mode, + save_mode = termios.tcgetattr(self.keyboard_fd) + tty.setraw(self.keyboard_fd, termios.TCSANOW) + try: + yield + finally: + # restore prior mode, + termios.tcsetattr(self.keyboard_fd, + termios.TCSAFLUSH, + save_mode) + else: + yield + @contextlib.contextmanager + def keypad(self): + """ + Context manager that enables keypad input (*keyboard_transmit* mode). -def split_into_formatters(compound): - """Split a possibly compound format string into segments. + This enables the effect of calling the curses function keypad(3x): + display terminfo(5) capability `keypad_xmit` (smkx) upon entering, + and terminfo(5) capability `keypad_local` (rmkx) upon exiting. - >>> split_into_formatters('bold_underline_bright_blue_on_red') - ['bold', 'underline', 'bright_blue', 'on_red'] + On an IBM-PC keypad of ttype *xterm*, with numlock off, the + lower-left diagonal key transmits sequence ``\\x1b[F``, ``KEY_END``. - """ - merged_segs = [] - # These occur only as prefixes, so they can always be merged: - mergeable_prefixes = ['on', 'bright', 'on_bright'] - for s in compound.split('_'): - if merged_segs and merged_segs[-1] in mergeable_prefixes: - merged_segs[-1] += '_' + s - else: - merged_segs.append(s) - return merged_segs + However, upon entering keypad mode, ``\\x1b[OF`` is transmitted, + translating to ``KEY_LL`` (lower-left key), allowing diagonal + direction keys to be determined. + """ + try: + self.stream.write(self.smkx) + yield + finally: + self.stream.write(self.rmkx) + + def inkey(self, timeout=None, esc_delay=0.35, _intr_continue=True): + """T.inkey(timeout=None, [esc_delay, [_intr_continue]]) -> Keypress() + + Receive next keystroke from keyboard (stdin), blocking until a + keypress is received or ``timeout`` elapsed, if specified. + + When used without the context manager ``cbreak``, stdin remains + line-buffered, and this function will block until return is pressed, + even though only one unicode character is returned at a time.. + + The value returned is an instance of ``Keystroke``, with properties + ``is_sequence``, and, when True, non-None values for attributes + ``code`` and ``name``. The value of ``code`` may be compared against + attributes of this terminal beginning with *KEY*, such as + ``KEY_ESCAPE``. + + To distinguish between ``KEY_ESCAPE``, and sequences beginning with + escape, the ``esc_delay`` specifies the amount of time after receiving + the escape character (chr(27)) to seek for the completion + of other application keys before returning ``KEY_ESCAPE``. + + Normally, when this function is interrupted by a signal, such as the + installment of SIGWINCH, this function will ignore this interruption + and continue to poll for input up to the ``timeout`` specified. If + you'd rather this function return ``u''`` early, specify a value + of ``False`` for ``_intr_continue``. + """ + # TODO(jquast): "meta sends escape", where alt+1 would send '\x1b1', + # what do we do with that? Surely, something useful. + # comparator to term.KEY_meta('x') ? + # TODO(jquast): Ctrl characters, KEY_CTRL_[A-Z], and the rest; + # KEY_CTRL_\, KEY_CTRL_{, etc. are not legitimate + # attributes. comparator to term.KEY_ctrl('z') ? + def _timeleft(stime, timeout): + """_timeleft(stime, timeout) -> float + + Returns time-relative time remaining before ``timeout`` + after time elapsed since ``stime``. + """ + if timeout is not None: + if timeout is 0: + return 0 + return max(0, timeout - (time.time() - stime)) + + resolve = functools.partial(resolve_sequence, + mapper=self._keymap, + codes=self._keycodes) + + stime = time.time() + + # re-buffer previously received keystrokes, + ucs = u'' + while self._keyboard_buf: + ucs += self._keyboard_buf.pop() + + # receive all immediately available bytes + while self.kbhit(0): + ucs += self.getch() + + # decode keystroke, if any + ks = resolve(text=ucs) + + # so long as the most immediately received or buffered keystroke is + # incomplete, (which may be a multibyte encoding), block until until + # one is received. + while not ks and self.kbhit(_timeleft(stime, timeout), _intr_continue): + ucs += self.getch() + ks = resolve(text=ucs) + + # handle escape key (KEY_ESCAPE) vs. escape sequence (which begins + # with KEY_ESCAPE, \x1b[, \x1bO, or \x1b?), up to esc_delay when + # received. This is not optimal, but causes least delay when + # (currently unhandled, and rare) "meta sends escape" is used, + # or when an unsupported sequence is sent. + if ks.code is self.KEY_ESCAPE: + esctime = time.time() + while (ks.code is self.KEY_ESCAPE and + self.kbhit(_timeleft(esctime, esc_delay))): + ucs += self.getch() + ks = resolve(text=ucs) + + # buffer any remaining text received + self._keyboard_buf.extendleft(ucs[len(ks):]) + return ks + +# From libcurses/doc/ncurses-intro.html (ESR, Thomas Dickey, et. al): +# +# "After the call to setupterm(), the global variable cur_term is set to +# point to the current structure of terminal capabilities. By calling +# setupterm() for each terminal, and saving and restoring cur_term, it +# is possible for a program to use two or more terminals at once." +# +# However, if you study Python's ./Modules/_cursesmodule.c, you'll find: +# +# if (!initialised_setupterm && setupterm(termstr,fd,&err) == ERR) { +# +# Python - perhaps wrongly - will not allow for re-initialisation of new +# terminals through setupterm(), so the value of cur_term cannot be changed +# once set: subsequent calls to setupterm() have no effect. +# +# Therefore, the ``kind`` of each Terminal() is, in essence, a singleton. +# This global variable reflects that, and a warning is emitted if somebody +# expects otherwise. + +_CUR_TERM = None + +WINSZ = collections.namedtuple('WINSZ', ( + 'ws_row', # /* rows, in characters */ + 'ws_col', # /* columns, in characters */ + 'ws_xpixel', # /* horizontal size, pixels */ + 'ws_ypixel', # /* vertical size, pixels */ +)) +#: format of termios structure +WINSZ._FMT = 'hhhh' +#: buffer of termios structure appropriate for ioctl argument +WINSZ._BUF = '\x00' * struct.calcsize(WINSZ._FMT) diff --git a/blessed/terminal.py.bak b/blessed/terminal.py.bak deleted file mode 100644 index 1e17d736..00000000 --- a/blessed/terminal.py.bak +++ /dev/null @@ -1,785 +0,0 @@ -"This primary module provides the Terminal class." -# standard modules -import collections -import contextlib -import functools -import warnings -import platform -import codecs -import curses -import locale -import select -import struct -import time -import sys -import os - -try: - import termios - import fcntl - import tty -except ImportError: - tty_methods = ('setraw', 'cbreak', 'kbhit', 'height', 'width') - msg_nosupport = ( - "One or more of the modules: 'termios', 'fcntl', and 'tty' " - "are not found on your platform '{0}'. The following methods " - "of Terminal are dummy/no-op unless a deriving class overrides " - "them: {1}".format(sys.platform.lower(), ', '.join(tty_methods))) - warnings.warn(msg_nosupport) - HAS_TTY = False -else: - HAS_TTY = True - -try: - from io import UnsupportedOperation as IOUnsupportedOperation -except ImportError: - class IOUnsupportedOperation(Exception): - """A dummy exception to take the place of Python 3's - ``io.UnsupportedOperation`` in Python 2.5""" - -try: - _ = InterruptedError - del _ -except NameError: - # alias py2 exception to py3 - InterruptedError = select.error - -# local imports -from .formatters import ( - ParameterizingString, - NullCallableString, - resolve_capability, - resolve_attribute, -) - -from .sequences import ( - init_sequence_patterns, - SequenceTextWrapper, - Sequence, -) - -from .keyboard import ( - get_keyboard_sequences, - get_keyboard_codes, - resolve_sequence, -) - - -class Terminal(object): - """A wrapper for curses and related terminfo(5) terminal capabilities. - - Instance attributes: - - ``stream`` - The stream the terminal outputs to. It's convenient to pass the stream - around with the terminal; it's almost always needed when the terminal - is and saves sticking lots of extra args on client functions in - practice. - """ - - #: Sugary names for commonly-used capabilities - _sugar = dict( - save='sc', - restore='rc', - # 'clear' clears the whole screen. - clear_eol='el', - clear_bol='el1', - clear_eos='ed', - position='cup', # deprecated - enter_fullscreen='smcup', - exit_fullscreen='rmcup', - move='cup', - move_x='hpa', - move_y='vpa', - move_left='cub1', - move_right='cuf1', - move_up='cuu1', - move_down='cud1', - hide_cursor='civis', - normal_cursor='cnorm', - reset_colors='op', # oc doesn't work on my OS X terminal. - normal='sgr0', - reverse='rev', - italic='sitm', - no_italic='ritm', - shadow='sshm', - no_shadow='rshm', - standout='smso', - no_standout='rmso', - subscript='ssubm', - no_subscript='rsubm', - superscript='ssupm', - no_superscript='rsupm', - underline='smul', - no_underline='rmul') - - def __init__(self, kind=None, stream=None, force_styling=False): - """Initialize the terminal. - - If ``stream`` is not a tty, I will default to returning an empty - Unicode string for all capability values, so things like piping your - output to a file won't strew escape sequences all over the place. The - ``ls`` command sets a precedent for this: it defaults to columnar - output when being sent to a tty and one-item-per-line when not. - - :arg kind: A terminal string as taken by ``setupterm()``. Defaults to - the value of the ``TERM`` environment variable. - :arg stream: A file-like object representing the terminal. Defaults to - the original value of stdout, like ``curses.initscr()`` does. - :arg force_styling: Whether to force the emission of capabilities, even - if we don't seem to be in a terminal. This comes in handy if users - are trying to pipe your output through something like ``less -r``, - which supports terminal codes just fine but doesn't appear itself - to be a terminal. Just expose a command-line option, and set - ``force_styling`` based on it. Terminal initialization sequences - will be sent to ``stream`` if it has a file descriptor and to - ``sys.__stdout__`` otherwise. (``setupterm()`` demands to send them - somewhere, and stdout is probably where the output is ultimately - headed. If not, stderr is probably bound to the same terminal.) - - If you want to force styling to not happen, pass - ``force_styling=None``. - - """ - global _CUR_TERM - self.keyboard_fd = None - - # default stream is stdout, keyboard only valid as stdin when - # output stream is stdout and output stream is a tty - if stream is None or stream == sys.__stdout__: - stream = sys.__stdout__ - self.keyboard_fd = sys.__stdin__.fileno() - - try: - stream_fd = (stream.fileno() if hasattr(stream, 'fileno') - and callable(stream.fileno) else None) - except IOUnsupportedOperation: - stream_fd = None - - self._is_a_tty = stream_fd is not None and os.isatty(stream_fd) - self._does_styling = ((self.is_a_tty or force_styling) and - force_styling is not None) - - # keyboard_fd only non-None if both stdin and stdout is a tty. - self.keyboard_fd = (self.keyboard_fd - if self.keyboard_fd is not None and - self.is_a_tty and os.isatty(self.keyboard_fd) - else None) - self._normal = None # cache normal attr, preventing recursive lookups - - # The descriptor to direct terminal initialization sequences to. - # sys.__stdout__ seems to always have a descriptor of 1, even if output - # is redirected. - self._init_descriptor = (stream_fd is None and sys.__stdout__.fileno() - or stream_fd) - self._kind = kind or os.environ.get('TERM', 'unknown') - - if self.does_styling: - # Make things like tigetstr() work. Explicit args make setupterm() - # work even when -s is passed to nosetests. Lean toward sending - # init sequences to the stream if it has a file descriptor, and - # send them to stdout as a fallback, since they have to go - # somewhere. - try: - if (platform.python_implementation() == 'PyPy' and - isinstance(self._kind, unicode)): - # pypy/2.4.0_2/libexec/lib_pypy/_curses.py, line 1131 - # TypeError: initializer for ctype 'char *' must be a str - curses.setupterm(self._kind.encode('ascii'), self._init_descriptor) - else: - curses.setupterm(self._kind, self._init_descriptor) - except curses.error as err: - warnings.warn('Failed to setupterm(kind={0!r}): {1}' - .format(self._kind, err)) - self._kind = None - self._does_styling = False - else: - if _CUR_TERM is None or self._kind == _CUR_TERM: - _CUR_TERM = self._kind - else: - warnings.warn( - 'A terminal of kind "%s" has been requested; due to an' - ' internal python curses bug, terminal capabilities' - ' for a terminal of kind "%s" will continue to be' - ' returned for the remainder of this process.' % ( - self._kind, _CUR_TERM,)) - - for re_name, re_val in init_sequence_patterns(self).items(): - setattr(self, re_name, re_val) - - # build database of int code <=> KEY_NAME - self._keycodes = get_keyboard_codes() - - # store attributes as: self.KEY_NAME = code - for key_code, key_name in self._keycodes.items(): - setattr(self, key_name, key_code) - - # build database of sequence <=> KEY_NAME - self._keymap = get_keyboard_sequences(self) - - self._keyboard_buf = collections.deque() - if self.keyboard_fd is not None: - locale.setlocale(locale.LC_ALL, '') - self._encoding = locale.getpreferredencoding() or 'ascii' - try: - self._keyboard_decoder = codecs.getincrementaldecoder( - self._encoding)() - except LookupError as err: - warnings.warn('%s, fallback to ASCII for keyboard.' % (err,)) - self._encoding = 'ascii' - self._keyboard_decoder = codecs.getincrementaldecoder( - self._encoding)() - - self.stream = stream - - def __getattr__(self, attr): - """Return a terminal capability as Unicode string. - - For example, ``term.bold`` is a unicode string that may be prepended - to text to set the video attribute for bold, which should also be - terminated with the pairing ``term.normal``. - - This capability is also callable, so you can use ``term.bold("hi")`` - which results in the joining of (term.bold, "hi", term.normal). - - Compound formatters may also be used, for example: - ``term.bold_blink_red_on_green("merry x-mas!")``. - - For a parametrized capability such as ``cup`` (cursor_address), pass - the parameters as arguments ``some_term.cup(line, column)``. See - manual page terminfo(5) for a complete list of capabilities. - """ - if not self.does_styling: - return NullCallableString() - val = resolve_attribute(self, attr) - # Cache capability codes. - setattr(self, attr, val) - return val - - @property - def kind(self): - """Name of this terminal type as string.""" - return self._kind - - @property - def does_styling(self): - """Whether this instance will emit terminal sequences (bool).""" - return self._does_styling - - @property - def is_a_tty(self): - """Whether the ``stream`` associated with this instance is a terminal - (bool).""" - return self._is_a_tty - - @property - def height(self): - """T.height -> int - - The height of the terminal in characters. - """ - return self._height_and_width().ws_row - - @property - def width(self): - """T.width -> int - - The width of the terminal in characters. - """ - return self._height_and_width().ws_col - - @staticmethod - def _winsize(fd): - """T._winsize -> WINSZ(ws_row, ws_col, ws_xpixel, ws_ypixel) - - The tty connected by file desriptor fd is queried for its window size, - and returned as a collections.namedtuple instance WINSZ. - - May raise exception IOError. - """ - if HAS_TTY: - data = fcntl.ioctl(fd, termios.TIOCGWINSZ, WINSZ._BUF) - return WINSZ(*struct.unpack(WINSZ._FMT, data)) - return WINSZ(ws_row=24, ws_col=80, ws_xpixel=0, ws_ypixel=0) - - def _height_and_width(self): - """Return a tuple of (terminal height, terminal width). - """ - # TODO(jquast): hey kids, even if stdout is redirected to a file, - # we can still query sys.__stdin__.fileno() for our terminal size. - # -- of course, if both are redirected, we have no use for this fd. - for fd in (self._init_descriptor, sys.__stdout__): - try: - if fd is not None: - return self._winsize(fd) - except IOError: - pass - - return WINSZ(ws_row=int(os.getenv('LINES', '25')), - ws_col=int(os.getenv('COLUMNS', '80')), - ws_xpixel=None, - ws_ypixel=None) - - @contextlib.contextmanager - def location(self, x=None, y=None): - """Return a context manager for temporarily moving the cursor. - - Move the cursor to a certain position on entry, let you print stuff - there, then return the cursor to its original position:: - - term = Terminal() - with term.location(2, 5): - print 'Hello, world!' - for x in xrange(10): - print 'I can do it %i times!' % x - - Specify ``x`` to move to a certain column, ``y`` to move to a certain - row, both, or neither. If you specify neither, only the saving and - restoration of cursor position will happen. This can be useful if you - simply want to restore your place after doing some manual cursor - movement. - - """ - # Save position and move to the requested column, row, or both: - self.stream.write(self.save) - if x is not None and y is not None: - self.stream.write(self.move(y, x)) - elif x is not None: - self.stream.write(self.move_x(x)) - elif y is not None: - self.stream.write(self.move_y(y)) - try: - yield - finally: - # Restore original cursor position: - self.stream.write(self.restore) - - @contextlib.contextmanager - def fullscreen(self): - """Return a context manager that enters fullscreen mode while inside it - and restores normal mode on leaving. - - Fullscreen mode is characterized by instructing the terminal emulator - to store and save the current screen state (all screen output), switch - to "alternate screen". Upon exiting, the previous screen state is - returned. - - This call may not be tested; only one screen state may be saved at a - time. - """ - self.stream.write(self.enter_fullscreen) - try: - yield - finally: - self.stream.write(self.exit_fullscreen) - - @contextlib.contextmanager - def hidden_cursor(self): - """Return a context manager that hides the cursor upon entering, - and makes it visible again upon exiting.""" - self.stream.write(self.hide_cursor) - try: - yield - finally: - self.stream.write(self.normal_cursor) - - @property - def color(self): - """Returns capability that sets the foreground color. - - The capability is unparameterized until called and passed a number - (0-15), at which point it returns another string which represents a - specific color change. This second string can further be called to - color a piece of text and set everything back to normal afterward. - - :arg num: The number, 0-15, of the color - - """ - if not self.does_styling: - return NullCallableString() - return ParameterizingString(self._foreground_color, - self.normal, 'color') - - @property - def on_color(self): - "Returns capability that sets the background color." - if not self.does_styling: - return NullCallableString() - return ParameterizingString(self._background_color, - self.normal, 'on_color') - - @property - def normal(self): - "Returns sequence that resets video attribute." - if self._normal: - return self._normal - self._normal = resolve_capability(self, 'normal') - return self._normal - - @property - def number_of_colors(self): - """Return the number of colors the terminal supports. - - Common values are 0, 8, 16, 88, and 256. Most commonly - this may be used to test color capabilities at all:: - - if term.number_of_colors: - ...""" - # trim value to 0, as tigetnum('colors') returns -1 if no support, - # -2 if no such capability. - return max(0, self.does_styling and curses.tigetnum('colors') or -1) - - @property - def _foreground_color(self): - return self.setaf or self.setf - - @property - def _background_color(self): - return self.setab or self.setb - - def ljust(self, text, width=None, fillchar=u' '): - """T.ljust(text, [width], [fillchar]) -> unicode - - Return string ``text``, left-justified by printable length ``width``. - Padding is done using the specified fill character (default is a - space). Default ``width`` is the attached terminal's width. ``text`` - may contain terminal sequences.""" - if width is None: - width = self.width - return Sequence(text, self).ljust(width, fillchar) - - def rjust(self, text, width=None, fillchar=u' '): - """T.rjust(text, [width], [fillchar]) -> unicode - - Return string ``text``, right-justified by printable length ``width``. - Padding is done using the specified fill character (default is a - space). Default ``width`` is the attached terminal's width. ``text`` - may contain terminal sequences.""" - if width is None: - width = self.width - return Sequence(text, self).rjust(width, fillchar) - - def center(self, text, width=None, fillchar=u' '): - """T.center(text, [width], [fillchar]) -> unicode - - Return string ``text``, centered by printable length ``width``. - Padding is done using the specified fill character (default is a - space). Default ``width`` is the attached terminal's width. ``text`` - may contain terminal sequences.""" - if width is None: - width = self.width - return Sequence(text, self).center(width, fillchar) - - def length(self, text): - """T.length(text) -> int - - Return the printable length of string ``text``, which may contain - terminal sequences. Strings containing sequences such as 'clear', - which repositions the cursor, does not give accurate results, and - their printable length is evaluated *0*.. - """ - return Sequence(text, self).length() - - def strip(self, text, chars=None): - """T.strip(text) -> unicode - - Return string ``text`` with terminal sequences removed, and leading - and trailing whitespace removed. - """ - return Sequence(text, self).strip(chars) - - def rstrip(self, text, chars=None): - """T.rstrip(text) -> unicode - - Return string ``text`` with terminal sequences and trailing whitespace - removed. - """ - return Sequence(text, self).rstrip(chars) - - def lstrip(self, text, chars=None): - """T.lstrip(text) -> unicode - - Return string ``text`` with terminal sequences and leading whitespace - removed. - """ - return Sequence(text, self).lstrip(chars) - - def strip_seqs(self, text): - """T.strip_seqs(text) -> unicode - - Return string ``text`` stripped only of its sequences. - """ - return Sequence(text, self).strip_seqs() - - def wrap(self, text, width=None, **kwargs): - """T.wrap(text, [width=None, **kwargs ..]) -> list[unicode] - - Wrap paragraphs containing escape sequences ``text`` to the full - ``width`` of Terminal instance *T*, unless ``width`` is specified. - Wrapped by the virtual printable length, irregardless of the video - attribute sequences it may contain, allowing text containing colors, - bold, underline, etc. to be wrapped. - - Returns a list of strings that may contain escape sequences. See - ``textwrap.TextWrapper`` for all available additional kwargs to - customize wrapping behavior such as ``subsequent_indent``. - """ - width = self.width if width is None else width - lines = [] - for line in text.splitlines(): - lines.extend( - (_linewrap for _linewrap in SequenceTextWrapper( - width=width, term=self, **kwargs).wrap(text)) - if line.strip() else (u'',)) - - return lines - - def getch(self): - """T.getch() -> unicode - - Read and decode next byte from keyboard stream. May return u'' - if decoding is not yet complete, or completed unicode character. - Should always return bytes when self.kbhit() returns True. - - Implementors of input streams other than os.read() on the stdin fd - should derive and override this method. - """ - assert self.keyboard_fd is not None - byte = os.read(self.keyboard_fd, 1) - return self._keyboard_decoder.decode(byte, final=False) - - def kbhit(self, timeout=None, _intr_continue=True): - """T.kbhit([timeout=None]) -> bool - - Returns True if a keypress has been detected on keyboard. - - When ``timeout`` is 0, this call is non-blocking, Otherwise blocking - until keypress is detected (default). When ``timeout`` is a positive - number, returns after ``timeout`` seconds have elapsed. - - If input is not a terminal, False is always returned. - """ - # Special care is taken to handle a custom SIGWINCH handler, which - # causes select() to be interrupted with errno 4 (EAGAIN) -- - # it is ignored, and a new timeout value is derived from the previous, - # unless timeout becomes negative, because signal handler has blocked - # beyond timeout, then False is returned. Otherwise, when timeout is 0, - # we continue to block indefinitely (default). - stime = time.time() - check_w, check_x, ready_r = [], [], [None, ] - check_r = [self.keyboard_fd] if self.keyboard_fd is not None else [] - - while HAS_TTY and True: - try: - ready_r, ready_w, ready_x = select.select( - check_r, check_w, check_x, timeout) - except InterruptedError: - if not _intr_continue: - return u'' - if timeout is not None: - # subtract time already elapsed, - timeout -= time.time() - stime - if timeout > 0: - continue - # no time remains after handling exception (rare) - ready_r = [] - break - else: - break - - return False if self.keyboard_fd is None else check_r == ready_r - - @contextlib.contextmanager - def cbreak(self): - """Return a context manager that enters 'cbreak' mode: disabling line - buffering of keyboard input, making characters typed by the user - immediately available to the program. Also referred to as 'rare' - mode, this is the opposite of 'cooked' mode, the default for most - shells. - - In 'cbreak' mode, echo of input is also disabled: the application must - explicitly print any input received, if they so wish. - - More information can be found in the manual page for curses.h, - http://www.openbsd.org/cgi-bin/man.cgi?query=cbreak - - The python manual for curses, - http://docs.python.org/2/library/curses.html - - Note also that setcbreak sets VMIN = 1 and VTIME = 0, - http://www.unixwiz.net/techtips/termios-vmin-vtime.html - """ - if HAS_TTY and self.keyboard_fd is not None: - # save current terminal mode, - save_mode = termios.tcgetattr(self.keyboard_fd) - tty.setcbreak(self.keyboard_fd, termios.TCSANOW) - try: - yield - finally: - # restore prior mode, - termios.tcsetattr(self.keyboard_fd, - termios.TCSAFLUSH, - save_mode) - else: - yield - - @contextlib.contextmanager - def raw(self): - """Return a context manager that enters *raw* mode. Raw mode is - similar to *cbreak* mode, in that characters typed are immediately - available to ``inkey()`` with one exception: the interrupt, quit, - suspend, and flow control characters are all passed through as their - raw character values instead of generating a signal. - """ - if HAS_TTY and self.keyboard_fd is not None: - # save current terminal mode, - save_mode = termios.tcgetattr(self.keyboard_fd) - tty.setraw(self.keyboard_fd, termios.TCSANOW) - try: - yield - finally: - # restore prior mode, - termios.tcsetattr(self.keyboard_fd, - termios.TCSAFLUSH, - save_mode) - else: - yield - - @contextlib.contextmanager - def keypad(self): - """ - Context manager that enables keypad input (*keyboard_transmit* mode). - - This enables the effect of calling the curses function keypad(3x): - display terminfo(5) capability `keypad_xmit` (smkx) upon entering, - and terminfo(5) capability `keypad_local` (rmkx) upon exiting. - - On an IBM-PC keypad of ttype *xterm*, with numlock off, the - lower-left diagonal key transmits sequence ``\\x1b[F``, ``KEY_END``. - - However, upon entering keypad mode, ``\\x1b[OF`` is transmitted, - translating to ``KEY_LL`` (lower-left key), allowing diagonal - direction keys to be determined. - """ - try: - self.stream.write(self.smkx) - yield - finally: - self.stream.write(self.rmkx) - - def inkey(self, timeout=None, esc_delay=0.35, _intr_continue=True): - """T.inkey(timeout=None, [esc_delay, [_intr_continue]]) -> Keypress() - - Receive next keystroke from keyboard (stdin), blocking until a - keypress is received or ``timeout`` elapsed, if specified. - - When used without the context manager ``cbreak``, stdin remains - line-buffered, and this function will block until return is pressed, - even though only one unicode character is returned at a time.. - - The value returned is an instance of ``Keystroke``, with properties - ``is_sequence``, and, when True, non-None values for attributes - ``code`` and ``name``. The value of ``code`` may be compared against - attributes of this terminal beginning with *KEY*, such as - ``KEY_ESCAPE``. - - To distinguish between ``KEY_ESCAPE``, and sequences beginning with - escape, the ``esc_delay`` specifies the amount of time after receiving - the escape character (chr(27)) to seek for the completion - of other application keys before returning ``KEY_ESCAPE``. - - Normally, when this function is interrupted by a signal, such as the - installment of SIGWINCH, this function will ignore this interruption - and continue to poll for input up to the ``timeout`` specified. If - you'd rather this function return ``u''`` early, specify a value - of ``False`` for ``_intr_continue``. - """ - # TODO(jquast): "meta sends escape", where alt+1 would send '\x1b1', - # what do we do with that? Surely, something useful. - # comparator to term.KEY_meta('x') ? - # TODO(jquast): Ctrl characters, KEY_CTRL_[A-Z], and the rest; - # KEY_CTRL_\, KEY_CTRL_{, etc. are not legitimate - # attributes. comparator to term.KEY_ctrl('z') ? - def _timeleft(stime, timeout): - """_timeleft(stime, timeout) -> float - - Returns time-relative time remaining before ``timeout`` - after time elapsed since ``stime``. - """ - if timeout is not None: - if timeout is 0: - return 0 - return max(0, timeout - (time.time() - stime)) - - resolve = functools.partial(resolve_sequence, - mapper=self._keymap, - codes=self._keycodes) - - stime = time.time() - - # re-buffer previously received keystrokes, - ucs = u'' - while self._keyboard_buf: - ucs += self._keyboard_buf.pop() - - # receive all immediately available bytes - while self.kbhit(0): - ucs += self.getch() - - # decode keystroke, if any - ks = resolve(text=ucs) - - # so long as the most immediately received or buffered keystroke is - # incomplete, (which may be a multibyte encoding), block until until - # one is received. - while not ks and self.kbhit(_timeleft(stime, timeout), _intr_continue): - ucs += self.getch() - ks = resolve(text=ucs) - - # handle escape key (KEY_ESCAPE) vs. escape sequence (which begins - # with KEY_ESCAPE, \x1b[, \x1bO, or \x1b?), up to esc_delay when - # received. This is not optimal, but causes least delay when - # (currently unhandled, and rare) "meta sends escape" is used, - # or when an unsupported sequence is sent. - if ks.code is self.KEY_ESCAPE: - esctime = time.time() - while (ks.code is self.KEY_ESCAPE and - self.kbhit(_timeleft(esctime, esc_delay))): - ucs += self.getch() - ks = resolve(text=ucs) - - # buffer any remaining text received - self._keyboard_buf.extendleft(ucs[len(ks):]) - return ks - -# From libcurses/doc/ncurses-intro.html (ESR, Thomas Dickey, et. al): -# -# "After the call to setupterm(), the global variable cur_term is set to -# point to the current structure of terminal capabilities. By calling -# setupterm() for each terminal, and saving and restoring cur_term, it -# is possible for a program to use two or more terminals at once." -# -# However, if you study Python's ./Modules/_cursesmodule.c, you'll find: -# -# if (!initialised_setupterm && setupterm(termstr,fd,&err) == ERR) { -# -# Python - perhaps wrongly - will not allow for re-initialisation of new -# terminals through setupterm(), so the value of cur_term cannot be changed -# once set: subsequent calls to setupterm() have no effect. -# -# Therefore, the ``kind`` of each Terminal() is, in essence, a singleton. -# This global variable reflects that, and a warning is emitted if somebody -# expects otherwise. - -_CUR_TERM = None - -WINSZ = collections.namedtuple('WINSZ', ( - 'ws_row', # /* rows, in characters */ - 'ws_col', # /* columns, in characters */ - 'ws_xpixel', # /* horizontal size, pixels */ - 'ws_ypixel', # /* vertical size, pixels */ -)) -#: format of termios structure -WINSZ._FMT = 'hhhh' -#: buffer of termios structure appropriate for ioctl argument -WINSZ._BUF = '\x00' * struct.calcsize(WINSZ._FMT) From 300552b5b636b23040d2ec5a325957033060ba0b Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 4 Jan 2015 14:10:15 -0800 Subject: [PATCH 258/459] post-merge README flub --- README.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.rst b/README.rst index 796e67c3..d805e7da 100644 --- a/README.rst +++ b/README.rst @@ -676,6 +676,10 @@ Bugs or suggestions? Visit the `issue tracker`_. For patches, please construct a test case if possible. +To test, execute ``./setup.py develop`` followed by command ``tox``. + +Pull requests are tested by Travis-CI. + License ======= From 9ea3e82a0f976dd86510f8c299576a575fcdafe4 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 4 Jan 2015 14:12:07 -0800 Subject: [PATCH 259/459] wheel support --- setup.cfg | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 setup.cfg diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..2a9acf13 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 From 3ad2f12424482d86bedecff134dd79785d29ee9f Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 4 Jan 2015 14:12:29 -0800 Subject: [PATCH 260/459] blessed should be zip_safe --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 489ebed5..32ec3f91 100755 --- a/setup.py +++ b/setup.py @@ -72,6 +72,7 @@ def main(): 'ansi', 'xterm'], cmdclass={'develop': SetupDevelop, 'test': SetupTest}, + zip_safe=True, **extra ) From 58a5959ea53b379d88418881d6467339b1e759a8 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 4 Jan 2015 14:15:58 -0800 Subject: [PATCH 261/459] use all svg-based badges --- README.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index d805e7da..d03e8b00 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ :alt: Travis Continous Integration :target: https://travis-ci.org/jquast/blessed -.. image:: https://coveralls.io/repos/jquast/blessed/badge.png?branch=master +.. image:: https://img.shields.io/coveralls/jquast/blessed.svg :alt: Coveralls Code Coverage :target: https://coveralls.io/r/jquast/blessed?branch=master @@ -16,6 +16,7 @@ .. image:: https://img.shields.io/pypi/dm/blessed.svg :alt: Downloads + :target: https://pypi.python.org/pypi/blessed ======= Blessed From 0659f10b12164186d45b967154d589627527c81f Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 4 Jan 2015 14:27:34 -0800 Subject: [PATCH 262/459] Add TeamCity build status, use png instead of svg. Unfortunately, svg cannot link to web pages, so we use png --- README.rst | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index d03e8b00..79263271 100644 --- a/README.rst +++ b/README.rst @@ -1,20 +1,23 @@ -.. image:: https://img.shields.io/travis/jquast/blessed.svg +.. image:: https://img.shields.io/travis/jquast/blessed.png :alt: Travis Continous Integration :target: https://travis-ci.org/jquast/blessed -.. image:: https://img.shields.io/coveralls/jquast/blessed.svg +.. image:: https://img.shields.io/teamcity/http/teamcity-master.pexpect.org/s/Blessed_BuildHead.png + :alt: TeamCity Build status + :target: https://teamcity-master.pexpect.org/viewType.html?buildTypeId=Blessed_BuildHead&branch_Blessed=%3Cdefault%3E&tab=buildTypeStatusDiv + +.. image:: https://img.shields.io/coveralls/jquast/blessed.png :alt: Coveralls Code Coverage :target: https://coveralls.io/r/jquast/blessed?branch=master -.. image:: https://img.shields.io/pypi/v/blessed.svg +.. image:: https://img.shields.io/pypi/v/blessed.png :alt: Latest Version :target: https://pypi.python.org/pypi/blessed .. image:: https://pypip.in/license/blessed/badge.svg :alt: License - :target: http://opensource.org/licenses/MIT -.. image:: https://img.shields.io/pypi/dm/blessed.svg +.. image:: https://img.shields.io/pypi/dm/blessed.png :alt: Downloads :target: https://pypi.python.org/pypi/blessed From 600474fd5731929c180990aeb7cc917c0b28d620 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Mon, 12 Jan 2015 00:38:44 -0800 Subject: [PATCH 263/459] remove 'screenshots' section temporarily just realized by new site bonkered up the screenshot references, we'll bring them back, maybe even a ttyrec2gif recording instead. --- README.rst | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/README.rst b/README.rst index 79263271..93f5d9b0 100644 --- a/README.rst +++ b/README.rst @@ -97,30 +97,6 @@ The same program with *Blessed* is simply:: print('This is' + term.underline('pretty!')) -Screenshots -=========== - -.. image:: http://jeffquast.com/blessed-weather.png - :target: http://jeffquast.com/blessed-weather.png - :scale: 50 % - :alt: Weather forecast demo (by @jquast) - -.. image:: http://jeffquast.com/blessed-tetris.png - :target: http://jeffquast.com/blessed-tetris.png - :scale: 50 % - :alt: Tetris game demo (by @johannesl) - -.. image:: http://jeffquast.com/blessed-wall.png - :target: http://jeffquast.com/blessed-wall.png - :scale: 50 % - :alt: bbs-scene.org api oneliners demo (art by xzip!impure) - -.. image:: http://jeffquast.com/blessed-quick-logon.png - :target: http://jeffquast.com/blessed-quick-logon.png - :scale: 50 % - :alt: x/84 bbs quick logon screen (art by xzip!impure) - - What It Provides ================ From 5ded6a81255ca31d8c5f141602cc17f981971f77 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 21 Feb 2015 21:55:15 -0800 Subject: [PATCH 264/459] FreeBSD support, updates for prospector (static analysis) - prospector: dropped --test-warnings argument - prospector: disable vulture and use 'ignore-patterns' - pytest: leave pep8/flake8 to prospector - do not test 'ansi' hpa/vpa: On FreeBSD, ansi has no such capability. This is the reason such "proxies" exist, anyway. - workaround for freebsd, which actually has an 'unknown' termcap entry - 'ansi' on freebsd tests '0' for number of colors, use cons25 on such system. - docfix: anomalous backslash in string: '\d' - pep8: E402 module level import not at top of file - freebsd: handle ValueError in signal.getsignal in the accessory script tools/display-signalhandlers.py --- .prospector.yaml | 4 ++-- blessed/_binterms.py | 1 - blessed/sequences.py | 10 ++++++---- blessed/tests/accessories.py | 3 +-- blessed/tests/test_core.py | 26 +++++++++++++++++++------- blessed/tests/test_sequences.py | 6 +++--- tools/display-sighandlers.py | 6 +++++- tox.ini | 8 ++------ 8 files changed, 38 insertions(+), 26 deletions(-) diff --git a/.prospector.yaml b/.prospector.yaml index d6714ed2..57ba3032 100644 --- a/.prospector.yaml +++ b/.prospector.yaml @@ -1,7 +1,7 @@ inherits: - strictness_veryhigh -ignore: +ignore-patterns: - (^|/)\..+ - ^docs/ # ignore tests and bin/ for the moment, their quality does not so much matter. @@ -62,6 +62,6 @@ pyroma: vulture: # this tool does a good job of finding unused code. - run: true + run: false # vim: noai:ts=4:sw=4 diff --git a/blessed/_binterms.py b/blessed/_binterms.py index 84f58fe8..2b6ec663 100644 --- a/blessed/_binterms.py +++ b/blessed/_binterms.py @@ -707,7 +707,6 @@ tvi950-rv-2p tvi950-rv-4p tvipt -unknown vanilla vc303 vc404 diff --git a/blessed/sequences.py b/blessed/sequences.py index 9353da83..c5625861 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -40,8 +40,9 @@ def _merge_sequences(inp): def _build_numeric_capability(term, cap, optional=False, base_num=99, nparams=1): - """ Build regexp from capabilities having matching numeric - parameter contained within termcap value: n->(\d+). + r""" + Build regexp from capabilities having matching numeric + parameter contained within termcap value: n->(\d+). """ _cap = getattr(term, cap) opt = '?' if optional else '' @@ -59,8 +60,9 @@ def _build_numeric_capability(term, cap, optional=False, def _build_any_numeric_capability(term, cap, num=99, nparams=1): - """ Build regexp from capabilities having *any* digit parameters - (substitute matching \d with pattern \d and return). + r""" + Build regexp from capabilities having *any* digit parameters + (substitute matching \d with pattern \d and return). """ _cap = getattr(term, cap) if _cap: diff --git a/blessed/tests/accessories.py b/blessed/tests/accessories.py index f7e2a428..d908f5cd 100644 --- a/blessed/tests/accessories.py +++ b/blessed/tests/accessories.py @@ -7,7 +7,6 @@ import functools import traceback import termios -import random import codecs import curses import sys @@ -16,6 +15,7 @@ # local from blessed import Terminal +from blessed._binterms import binary_terminals # 3rd import pytest @@ -31,7 +31,6 @@ all_xterms_params = ['xterm', 'xterm-256color'] many_lines_params = [30, 100] many_columns_params = [1, 10] -from blessed._binterms import binary_terminals default_all_terms = ['screen', 'vt220', 'rxvt', 'cons25', 'linux', 'ansi'] if os.environ.get('TEST_ALLTERMS'): try: diff --git a/blessed/tests/test_core.py b/blessed/tests/test_core.py index befcea3d..9106b9f0 100644 --- a/blessed/tests/test_core.py +++ b/blessed/tests/test_core.py @@ -107,7 +107,12 @@ def child_256_forcestyle(): @as_subprocess def child_8_forcestyle(): - t = TestTerminal(kind='ansi', stream=StringIO(), + kind = 'ansi' + if platform.system().lower() == 'freebsd': + # 'ansi' on freebsd returns 0 colors, we use the 'cons25' driver, + # compatible with its kernel tty.c + kind = 'cons25' + t = TestTerminal(kind=kind, stream=StringIO(), force_styling=True) assert (t.number_of_colors == 8) @@ -132,7 +137,12 @@ def child_256(): @as_subprocess def child_8(): - t = TestTerminal(kind='ansi') + kind = 'ansi' + if platform.system().lower() == 'freebsd': + # 'ansi' on freebsd returns 0 colors, we use the 'cons25' driver, + # compatible with its kernel tty.c + kind = 'cons25' + t = TestTerminal(kind=kind) assert (t.number_of_colors == 8) @as_subprocess @@ -201,9 +211,10 @@ def child(): def test_setupterm_invalid_issue39(): "A warning is emitted if TERM is invalid." # https://bugzilla.mozilla.org/show_bug.cgi?id=878089 - + # # if TERM is unset, defaults to 'unknown', which should - # fail to lookup and emit a warning, only. + # fail to lookup and emit a warning on *some* systems. + # freebsd actually has a termcap entry for 'unknown' @as_subprocess def child(): warnings.filterwarnings("error", category=UserWarning) @@ -216,8 +227,9 @@ def child(): "Failed to setupterm(kind='unknown'): " "setupterm: could not find terminal") else: - assert not term.is_a_tty and not term.does_styling, ( - 'Should have thrown exception') + if platform.system().lower() != 'freebsd': + assert not term.is_a_tty and not term.does_styling, ( + 'Should have thrown exception') warnings.resetwarnings() child() @@ -233,7 +245,7 @@ def test_setupterm_invalid_has_no_styling(): def child(): warnings.filterwarnings("ignore", category=UserWarning) - term = TestTerminal(kind='unknown', force_styling=True) + term = TestTerminal(kind='xxXunknownXxx', force_styling=True) assert term.kind is None assert term.does_styling is False assert term.number_of_colors == 0 diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index b15426cb..160be47a 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -192,8 +192,8 @@ def child(kind): assert (t.stream.getvalue() == expected_output), ( repr(t.stream.getvalue()), repr(expected_output)) - # skip 'screen', hpa is proxied (see later tests) - if all_terms != 'screen': + # skip 'screen', 'ansi': hpa is proxied (see later tests) + if all_terms not in ('screen', 'ansi'): child(all_terms) @@ -211,7 +211,7 @@ def child(kind): assert (t.stream.getvalue() == expected_output) # skip 'screen', vpa is proxied (see later tests) - if all_terms != 'screen': + if all_terms not in ('screen', 'ansi'): child(all_terms) diff --git a/tools/display-sighandlers.py b/tools/display-sighandlers.py index 98445e95..f3559f72 100755 --- a/tools/display-sighandlers.py +++ b/tools/display-sighandlers.py @@ -12,7 +12,11 @@ for signal_name in dir(signal) if signal_name.startswith('SIG') and not signal_name.startswith('SIG_')]: - handler = signal.getsignal(value) + try: + handler = signal.getsignal(value) + except ValueError: + # FreeBSD: signal number out of range + handler = 'out of range' description = { signal.SIG_IGN: "ignored(SIG_IGN)", signal.SIG_DFL: "default(SIG_DFL)" diff --git a/tox.ini b/tox.ini index 5c93fca1..adc52445 100644 --- a/tox.ini +++ b/tox.ini @@ -11,14 +11,11 @@ skip_missing_interpreters = true [testenv] whitelist_externals = /bin/bash /bin/mv setenv = PYTHONIOENCODING=UTF8 -deps = pytest-flakes - pytest-xdist - pytest-pep8 - pytest-cov +deps = pytest-cov pytest mock commands = {envbindir}/py.test \ - --strict --pep8 --flakes \ + --strict \ --junit-xml=results.{envname}.xml --verbose \ --cov blessed blessed/tests --cov-report=term-missing \ {posargs} @@ -28,7 +25,6 @@ commands = {envbindir}/py.test \ deps = prospector[with_everything] commands = prospector \ --die-on-tool-error \ - --test-warnings \ --doc-warnings \ {toxinidir} From 3d926d1f54efabf0d1e02ed87df56603a589e782 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Mon, 21 Sep 2015 16:18:57 -0700 Subject: [PATCH 265/459] make bin/editor.py python2 compatible --- bin/editor.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/bin/editor.py b/bin/editor.py index 4dff2b70..2f0feaea 100755 --- a/bin/editor.py +++ b/bin/editor.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python # Dumb full-screen editor. It doesn't save anything but to the screen. # # "Why wont python let me read memory @@ -11,8 +11,18 @@ import functools from blessed import Terminal -echo = lambda text: ( - functools.partial(print, end='', flush=True)(text)) +try: + print(end='', flush=True) + echo = lambda text: ( + functools.partial(print, end='', flush=True)(text)) +except TypeError: + # TypeError: 'flush' is an invalid keyword argument for this function + # python 2 + import sys + + def echo(text): + sys.stdout.write(text) + sys.stdout.flush() echo_yx = lambda cursor, text: ( echo(cursor.term.move(cursor.y, cursor.x) + text)) From 1af188b50248c794b671e55df5659e5935fdc799 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 2 Oct 2015 14:08:41 -0700 Subject: [PATCH 266/459] Backmerge rejected upstream enhancements Version 1.9.6 readyness: A branch in the upstream repository to merge all of our work has stalled. https://github.com/erikrose/blessings/pull/104 This squashed commit returns all such work into the mainline branch of 'blessed', so that work may progress which has otherwise stagnated since January of this year. Once these commits are squashed and boiled out, most of which are renames that are later reversed, very little remains that warrant a release. We are left with the following changes: - Bugfix? Remove 'unknown' terminal type. - Add dependency 'six' for 'string_type'. - Improvements in documentation. - Improvements in test and build chain. --- .gitignore | 1 + .landscape.yml | 83 +++ .prospector.yaml | 67 -- .travis.yml | 58 +- CONTRIBUTING.rst | 42 ++ MANIFEST.in | 4 +- README.rst | 845 +----------------------- bin/editor.py | 270 ++++---- bin/keymatrix.py | 166 +++-- bin/on_resize.py | 60 +- bin/progress_bar.py | 13 +- bin/tprint.py | 27 +- bin/worms.py | 220 ++++--- blessed/__init__.py | 10 +- blessed/_binterms.py | 21 +- blessed/formatters.py | 287 +++++--- blessed/keyboard.py | 292 +++++--- blessed/sequences.py | 468 ++++++++----- blessed/terminal.py | 915 ++++++++++++++++---------- blessed/tests/accessories.py | 30 +- blessed/tests/test_core.py | 64 +- blessed/tests/test_formatters.py | 22 +- blessed/tests/test_keyboard.py | 242 +++---- blessed/tests/test_length_sequence.py | 12 +- blessed/tests/test_sequences.py | 64 +- docs/api.rst | 46 ++ docs/conf.py | 108 ++- docs/contributing.rst | 1 + docs/examples.rst | 68 ++ docs/further.rst | 104 +++ docs/history.rst | 203 ++++++ docs/index.rst | 75 +-- docs/intro.rst | 162 +++++ docs/overview.rst | 578 ++++++++++++++++ docs/pains.rst | 376 +++++++++++ docs/sphinxext/github.py | 155 +++++ fabfile.py | 39 -- requirements.txt | 3 +- setup.py | 127 ++-- tools/display-sighandlers.py | 46 +- tools/display-terminalinfo.py | 39 +- tools/teamcity-runtests.sh | 5 +- tox.ini | 84 ++- version.json | 1 + 44 files changed, 3999 insertions(+), 2504 deletions(-) create mode 100644 .landscape.yml delete mode 100644 .prospector.yaml create mode 100644 CONTRIBUTING.rst mode change 100644 => 120000 README.rst create mode 100644 docs/api.rst create mode 120000 docs/contributing.rst create mode 100644 docs/examples.rst create mode 100644 docs/further.rst create mode 100644 docs/history.rst create mode 100644 docs/intro.rst create mode 100644 docs/overview.rst create mode 100644 docs/pains.rst create mode 100644 docs/sphinxext/github.py delete mode 100644 fabfile.py create mode 100644 version.json diff --git a/.gitignore b/.gitignore index 06cf26ba..f71f24b1 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ dist docs/_build htmlcov .coveralls.yml +.DS_Store diff --git a/.landscape.yml b/.landscape.yml new file mode 100644 index 00000000..8d52777d --- /dev/null +++ b/.landscape.yml @@ -0,0 +1,83 @@ +inherits: + - strictness_veryhigh + +ignore-patterns: + - (^|/)\..+ + - ^docs/ + - ^build/ + # ignore these, their quality does not so much matter. + - ^blessed/tests/ + +test-warnings: true + +output-format: grouped + +dodgy: + # Looks at Python code to search for things which look "dodgy" + # such as passwords or git conflict artifacts + run: true + +pyflakes: + # preferring 'frosted' instead (a fork of) + run: false + +frosted: + # static analysis, pyflakes improved? + # + run: true + disable: + # Terminal imported but unused (false) + - 'E101' + +mccabe: + # complexity checking. We know of only one offense of the + # default of 10, which is Terminal.inkey() at exactly 10. + # It is not easily avoided, likely removing deprecation + # warnings in a future release will help reduce it. + run: false + options: + max-complexity: 11 + +pep257: + # docstring checking + run: true + disable: + # 1 blank line required before class docstring + - 'D203' + # 1 blank line required after class docstring + - 'D204' + + +pep8: + # style checking + run: true + +pylint: + # static analysis and then some + run: true + options: + # pytest module has dynamically assigned functions, + # raising errors such as: E1101: Module 'pytest' has + # no 'mark' member + ignored-classes: pytest + # List of builtins function names that should not be used, separated by a comma + # bad-functions=map,filter,input + bad-functions: input + persistent: no + # 'ks' is often-cited variable representing Keystroke instance. + # 'fd' is a common shorthand term for file descriptor (as int). + good-names: _,ks,fd + + disable: + # Access to a protected member _sugar of a client class + - protected-access + +pyroma: + # checks setup.py + run: true + +vulture: + # this tool does a good job of finding unused code. + run: false + +# vim: noai:ts=4:sw=4 diff --git a/.prospector.yaml b/.prospector.yaml deleted file mode 100644 index 57ba3032..00000000 --- a/.prospector.yaml +++ /dev/null @@ -1,67 +0,0 @@ -inherits: - - strictness_veryhigh - -ignore-patterns: - - (^|/)\..+ - - ^docs/ - # ignore tests and bin/ for the moment, their quality does not so much matter. - - ^bin/ - - ^blessed/tests/ - -test-warnings: true - -output-format: grouped - -dodgy: - # Looks at Python code to search for things which look "dodgy" - # such as passwords or git conflict artifacts - run: true - -frosted: - # static analysis - run: true - -mccabe: - # complexity checking. - run: true - -pep257: - # docstring checking - run: true - -pep8: - # style checking - run: true - -pyflakes: - # preferring 'frosted' instead (a fork of) - run: false - -pylint: - # static analysis and then some - run: true - options: - # pytest module has dynamically assigned functions, - # raising errors such as: E1101: Module 'pytest' has - # no 'mark' member - ignored-classes: pytest - disable: - # Too many lines in module - ##- C0302 - # Used * or ** magic - ##- W0142 - # Used builtin function 'filter'. - # (For maintainability, one should prefer list comprehension.) - ##- W0141 - # Use % formatting in logging functions but pass the % parameters - ##- W1202 - -pyroma: - # checks setup.py - run: true - -vulture: - # this tool does a good job of finding unused code. - run: false - -# vim: noai:ts=4:sw=4 diff --git a/.travis.yml b/.travis.yml index e3c76c25..64302e35 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,36 +1,38 @@ language: python -# http://blog.travis-ci.com/2014-12-17-faster-builds-with-container-based-infrastructure/ sudo: false -env: - - TOXENV=py26 - - TOXENV=py27 - - TOXENV=py33 - - TOXENV=pypy - - TOXENV=py34 - -install: - - pip install -q tox - - # for python versions <27, we must install ordereddict - # mimicking the same dynamically generated 'requires=' - # in setup.py - - if [[ "${TOXENV}" == "py25" ]] || [[ "${TOXENV}" == "py26" ]]; then - pip install -q ordereddict; - fi - - # for python version =27, install coverage, coveralls. - # (coverage only measured and published for one version) - - if [[ "${TOXENV}" == "py34" ]]; then - pip install -q coverage coveralls; - fi - +before_script: + - pip install -q tox script: - - tox -e $TOXENV + - tox -after_success: - - if [[ "${TOXENV}" == "py34" ]]; then for f in ._coverage*; do mv $f `echo $f | tr -d '_'`; done; coverage combine; coveralls; fi +matrix: + fast_finish: true + include: + - env: TOXENV=sa + - env: TOXENV=docs + - python: 2.6 + env: TOXENV=py26 + - python: 2.7 + env: TOXENV=py27 + - python: 3.3 + env: TOXENV=py33 + - python: 3.4 + env: TOXENV=py34 + - python: 3.5 + env: TOXENV=py35 notifications: email: - - contact@jeffquast.com + recipients: + - contact@jeffquast.com + on_success: change + on_failure: change + irc: + channels: + - "irc.Prison.NET#1984" + template: + - "%{repository}(%{branch}): %{message} (%{duration}) %{build_url}" + skip_join: true + on_success: change + on_failure: change diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 00000000..2d3cfc1f --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,42 @@ +Contributing +============ + +We welcome contributions via GitHub pull requests: + +- `Fork a Repo `_ +- `Creating a pull request + `_ + +Developing +---------- + +Install git, Python 2 and 3, and pip. + +Then, from the blessed code folder:: + + pip install --editable . + +Running Tests +~~~~~~~~~~~~~ + +Install and run tox + +:: + + pip install --upgrade tox + tox + +Test Coverage +~~~~~~~~~~~~~ + +Blessed has 99% code coverage, and we'd like to keep it that way, as +terminals are fiddly beasts. Thus, when you contribute a new feature, make +sure it is covered by tests. Likewise, a bug fix should include a test +demonstrating the bug. + +Style and Static Analysis +~~~~~~~~~~~~~~~~~~~~~~~~~ + +The test runner (``tox``) ensures all code and documentation complies +with standard python style guides, pep8 and pep257, as well as various +static analysis tools. diff --git a/MANIFEST.in b/MANIFEST.in index 8891ea07..c2a28ff6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,6 @@ -include README.rst +include docs/*.rst include LICENSE +include version.json +include requirements.txt include tox.ini include blessed/tests/wall.ans diff --git a/README.rst b/README.rst deleted file mode 100644 index 93f5d9b0..00000000 --- a/README.rst +++ /dev/null @@ -1,844 +0,0 @@ -.. image:: https://img.shields.io/travis/jquast/blessed.png - :alt: Travis Continous Integration - :target: https://travis-ci.org/jquast/blessed - -.. image:: https://img.shields.io/teamcity/http/teamcity-master.pexpect.org/s/Blessed_BuildHead.png - :alt: TeamCity Build status - :target: https://teamcity-master.pexpect.org/viewType.html?buildTypeId=Blessed_BuildHead&branch_Blessed=%3Cdefault%3E&tab=buildTypeStatusDiv - -.. image:: https://img.shields.io/coveralls/jquast/blessed.png - :alt: Coveralls Code Coverage - :target: https://coveralls.io/r/jquast/blessed?branch=master - -.. image:: https://img.shields.io/pypi/v/blessed.png - :alt: Latest Version - :target: https://pypi.python.org/pypi/blessed - -.. image:: https://pypip.in/license/blessed/badge.svg - :alt: License - -.. image:: https://img.shields.io/pypi/dm/blessed.png - :alt: Downloads - :target: https://pypi.python.org/pypi/blessed - -======= -Blessed -======= - -Coding with *Blessed* looks like this... :: - - from blessed import Terminal - - t = Terminal() - - print(t.bold('Hi there!')) - print(t.bold_red_on_bright_green('It hurts my eyes!')) - - with t.location(0, t.height - 1): - print(t.center(t.blink('press any key to continue.'))) - - with t.cbreak(): - t.inkey() - - -The Pitch -========= - -*Blessed* is a more simplified wrapper around curses_, providing : - -* Styles, color, and maybe a little positioning without necessarily - clearing the whole screen first. -* Leave more than one screenful of scrollback in the buffer after your program - exits, like a well-behaved command-line application should. -* No more C-like calls to tigetstr_ and `tparm`_. -* Act intelligently when somebody redirects your output to a file, omitting - all of the terminal sequences such as styling, colors, or positioning. -* Dead-simple keyboard handling, modeled after the Basic language's *INKEY$* - -Before And After ----------------- - -With the built-in curses_ module, this is how you would typically -print some underlined text at the bottom of the screen:: - - from curses import tigetstr, setupterm, tparm - from fcntl import ioctl - from os import isatty - import struct - import sys - from termios import TIOCGWINSZ - - # If we want to tolerate having our output piped to other commands or - # files without crashing, we need to do all this branching: - if hasattr(sys.stdout, 'fileno') and isatty(sys.stdout.fileno()): - setupterm() - sc = tigetstr('sc') - cup = tigetstr('cup') - rc = tigetstr('rc') - underline = tigetstr('smul') - normal = tigetstr('sgr0') - else: - sc = cup = rc = underline = normal = '' - print(sc) # Save cursor position. - if cup: - # tigetnum('lines') doesn't always update promptly, hence this: - height = struct.unpack('hhhh', ioctl(0, TIOCGWINSZ, '\000' * 8))[0] - print(tparm(cup, height - 1, 0)) # Move cursor to bottom. - print('This is {under}underlined{normal}!'.format(under=underline, - normal=normal)) - print(rc) # Restore cursor position. - -The same program with *Blessed* is simply:: - - from blessed import Terminal - - term = Terminal() - with term.location(0, term.height - 1): - print('This is' + term.underline('pretty!')) - - -What It Provides -================ - -Blessed provides just **one** top-level object: *Terminal*. Instantiating a -*Terminal* figures out whether you're on a terminal at all and, if so, does -any necessary setup. After that, you can proceed to ask it all sorts of things -about the terminal, such as its size and color support, and use its styling -to construct strings containing color and styling. Also, the special sequences -inserted with application keys (arrow and function keys) are understood and -decoded, as well as your locale-specific encoded multibyte input. - - -Simple Formatting ------------------ - -Lots of handy formatting codes are available as attributes on a *Terminal* class -instance. For example:: - - from blessed import Terminal - - term = Terminal() - print('I am ' + term.bold + 'bold' + term.normal + '!') - -These capabilities (*bold*, *normal*) are translated to their sequences, which -when displayed simply change the video attributes. And, when used as a callable, -automatically wraps the given string with this sequence, and terminates it with -*normal*. - -The same can be written as:: - - print('I am' + term.bold('bold') + '!') - -You may also use the *Terminal* instance as an argument for ``.format`` string -method, so that capabilities can be displayed in-line for more complex strings:: - - print('{t.red_on_yellow}Candy corn{t.normal} for everyone!'.format(t=term)) - - -Capabilities ------------- - -The basic capabilities supported by most terminals are: - -``bold`` - Turn on 'extra bright' mode. -``reverse`` - Switch fore and background attributes. -``blink`` - Turn on blinking. -``normal`` - Reset attributes to default. - -The less commonly supported capabilities: - -``dim`` - Enable half-bright mode. -``underline`` - Enable underline mode. -``no_underline`` - Exit underline mode. -``italic`` - Enable italicized text. -``no_italic`` - Exit italics. -``shadow`` - Enable shadow text mode (rare). -``no_shadow`` - Exit shadow text mode. -``standout`` - Enable standout mode (often, an alias for ``reverse``.). -``no_standout`` - Exit standout mode. -``subscript`` - Enable subscript mode. -``no_subscript`` - Exit subscript mode. -``superscript`` - Enable superscript mode. -``no_superscript`` - Exit superscript mode. -``flash`` - Visual bell, flashes the screen. - -Note that, while the inverse of *underline* is *no_underline*, the only way -to turn off *bold* or *reverse* is *normal*, which also cancels any custom -colors. - -Many of these are aliases, their true capability names (such as 'smul' for -'begin underline mode') may still be used. Any capability in the `terminfo(5)`_ -manual, under column **Cap-name**, may be used as an attribute to a *Terminal* -instance. If it is not a supported capability, or a non-tty is used as an -output stream, an empty string is returned. - - -Colors ------- - -Color terminals are capable of at least 8 basic colors. - -* ``black`` -* ``red`` -* ``green`` -* ``yellow`` -* ``blue`` -* ``magenta`` -* ``cyan`` -* ``white`` - -The same colors, prefixed with *bright_* (synonymous with *bold_*), -such as *bright_blue*, provides 16 colors in total. - -The same colors, prefixed with *on_* sets the background color, some -terminals also provide an additional 8 high-intensity versions using -*on_bright*, some example compound formats:: - - from blessed import Terminal - - term = Terminal() - - print(term.on_bright_blue('Blue skies!')) - print(term.bright_red_on_bright_yellow('Pepperoni Pizza!')) - -There is also a numerical interface to colors, which takes an integer from -0-15.:: - - from blessed import Terminal - - term = Terminal() - - for n in range(16): - print(term.color(n)('Color {}'.format(n))) - -If the terminal defined by the **TERM** environment variable does not support -colors, these simply return empty strings, or the string passed as an argument -when used as a callable, without any video attributes. If the **TERM** defines -a terminal that does support colors, but actually does not, they are usually -harmless. - -Colorless terminals, such as the amber or green monochrome *vt220*, do not -support colors but do support reverse video. For this reason, it may be -desirable in some applications, such as a selection bar, to simply select -a foreground color, followed by reverse video to achieve the desired -background color effect:: - - from blessed import Terminal - - term = Terminal() - - print('some terminals {standout} more than others'.format( - standout=term.green_reverse('standout'))) - -Which appears as *bright white on green* on color terminals, or *black text -on amber or green* on monochrome terminals. - -You can check whether the terminal definition used supports colors, and how -many, using the ``number_of_colors`` property, which returns any of *0*, -*8* or *256* for terminal types such as *vt220*, *ansi*, and -*xterm-256color*, respectively. - -**NOTE**: On most color terminals, unlink *black*, *bright_black* is not -invisible -- it is actually a very dark shade of gray! - -Compound Formatting -------------------- - -If you want to do lots of crazy formatting all at once, you can just mash it -all together:: - - from blessed import Terminal - - term = Terminal() - - print(term.bold_underline_green_on_yellow('Woo')) - -I'd be remiss if I didn't credit couleur_, where I probably got the idea for -all this mashing. This compound notation comes in handy if you want to allow -users to customize formatting, just allow compound formatters, like *bold_green*, -as a command line argument or configuration item:: - - #!/usr/bin/env python - import argparse - - parser = argparse.ArgumentParser( - description='displays argument as specified style') - parser.add_argument('style', type=str, help='style formatter') - parser.add_argument('text', type=str, nargs='+') - - from blessed import Terminal - - term = Terminal() - args = parser.parse_args() - - style = getattr(term, args.style) - - print(style(' '.join(args.text))) - -Saved as **tprint.py**, this could be called simply:: - - $ ./tprint.py bright_blue_reverse Blue Skies - - -Moving The Cursor ------------------ - -When you want to move the cursor, you have a few choices, the -``location(x=None, y=None)`` context manager, ``move(y, x)``, ``move_y(row)``, -and ``move_x(col)`` attributes. - -**NOTE**: The ``location()`` method receives arguments in form of *(x, y)*, -whereas the ``move()`` argument receives arguments in form of *(y, x)*. This -is a flaw in the original `erikrose/blessings`_ implementation, but remains -for compatibility. - -Moving Temporarily -~~~~~~~~~~~~~~~~~~ - -A context manager, ``location()`` is provided to move the cursor to an -*(x, y)* screen position and restore the previous position upon exit:: - - from blessed import Terminal - - term = Terminal() - with term.location(0, term.height - 1): - print('Here is the bottom.') - print('This is back where I came from.') - -Parameters to ``location()`` are **optional** *x* and/or *y*:: - - with term.location(y=10): - print('We changed just the row.') - -When omitted, it saves the cursor position and restore it upon exit:: - - with term.location(): - print(term.move(1, 1) + 'Hi') - print(term.move(9, 9) + 'Mom') - -**NOTE**: calls to ``location()`` may not be nested, as only one location -may be saved. - - -Moving Permanently -~~~~~~~~~~~~~~~~~~ - -If you just want to move and aren't worried about returning, do something like -this:: - - from blessed import Terminal - - term = Terminal() - print(term.move(10, 1) + 'Hi, mom!') - -``move`` - Position the cursor, parameter in form of *(y, x)* -``move_x`` - Position the cursor at given horizontal column. -``move_y`` - Position the cursor at given vertical column. - -One-Notch Movement -~~~~~~~~~~~~~~~~~~ - -Finally, there are some parameterless movement capabilities that move the -cursor one character in various directions: - -* ``move_left`` -* ``move_right`` -* ``move_up`` -* ``move_down`` - -**NOTE**: *move_down* is often valued as *\\n*, which additionally returns -the carriage to column 0, depending on your terminal emulator. - - -Height And Width ----------------- - -Use the *height* and *width* properties of the *Terminal* class instance:: - - from blessed import Terminal - - term = Terminal() - height, width = term.height, term.width - with term.location(x=term.width / 3, y=term.height / 3): - print('1/3 ways in!') - -These are always current, so they may be used with a callback from SIGWINCH_ signals.:: - - import signal - from blessed import Terminal - - term = Terminal() - - def on_resize(sig, action): - print('height={t.height}, width={t.width}'.format(t=term)) - - signal.signal(signal.SIGWINCH, on_resize) - - term.inkey() - - -Clearing The Screen -------------------- - -Blessed provides syntactic sugar over some screen-clearing capabilities: - -``clear`` - Clear the whole screen. -``clear_eol`` - Clear to the end of the line. -``clear_bol`` - Clear backward to the beginning of the line. -``clear_eos`` - Clear to the end of screen. - - -Full-Screen Mode ----------------- - -If you've ever noticed a program, such as an editor, restores the previous -screen (such as your shell prompt) after exiting, you're seeing the -*enter_fullscreen* and *exit_fullscreen* attributes in effect. - -``enter_fullscreen`` - Switch to alternate screen, previous screen is stored by terminal driver. -``exit_fullscreen`` - Switch back to standard screen, restoring the same terminal state. - -There's also a context manager you can use as a shortcut:: - - from __future__ import division - from blessed import Terminal - - term = Terminal() - with term.fullscreen(): - print(term.move_y(term.height // 2) + - term.center('press any key').rstrip()) - term.inkey() - -Pipe Savvy ----------- - -If your program isn't attached to a terminal, such as piped to a program -like *less(1)* or redirected to a file, all the capability attributes on -*Terminal* will return empty strings. You'll get a nice-looking file without -any formatting codes gumming up the works. - -If you want to override this, such as when piping output to ``less -r``, pass -argument ``force_styling=True`` to the *Terminal* constructor. - -In any case, there is a *does_styling* attribute on *Terminal* that lets -you see whether the terminal attached to the output stream is capable of -formatting. If it is *False*, you may refrain from drawing progress -bars and other frippery and just stick to content:: - - from blessed import Terminal - - term = Terminal() - if term.does_styling: - with term.location(x=0, y=term.height - 1): - print('Progress: [=======> ]') - print(term.bold('Important stuff')) - -Sequence Awareness ------------------- - -Blessed may measure the printable width of strings containing sequences, -providing ``.center``, ``.ljust``, and ``.rjust`` methods, using the -terminal screen's width as the default *width* value:: - - from blessed import Terminal - - term = Terminal() - with term.location(y=term.height / 2): - print (term.center(term.bold('X')) - -Any string containing sequences may have its printable length measured using the -``.length`` method. Additionally, ``textwrap.wrap()`` is supplied on the Terminal -class as method ``.wrap`` method that is also sequence-aware, so now you may -word-wrap strings containing sequences. The following example displays a poem -from Tao Te Ching, word-wrapped to 25 columns:: - - from blessed import Terminal - - term = Terminal() - - poem = (term.bold_blue('Plan difficult tasks'), - term.blue('through the simplest tasks'), - term.bold_cyan('Achieve large tasks'), - term.cyan('through the smallest tasks')) - - for line in poem: - print('\n'.join(term.wrap(line, width=25, subsequent_indent=' ' * 4))) - -Keyboard Input --------------- - -The built-in python function ``raw_input`` function does not return a value until -the return key is pressed, and is not suitable for detecting each individual -keypress, much less arrow or function keys that emit multibyte sequences. - -Special `termios(4)`_ routines are required to enter Non-canonical mode, known -in curses as `cbreak(3)`_. When calling read on input stream, only bytes are -received, which must be decoded to unicode. - -Blessed handles all of these special cases!! - -cbreak -~~~~~~ - -The context manager ``cbreak`` can be used to enter *key-at-a-time* mode: Any -keypress by the user is immediately consumed by read calls:: - - from blessed import Terminal - import sys - - t = Terminal() - - with t.cbreak(): - # blocks until any key is pressed. - sys.stdin.read(1) - -raw -~~~ - -The context manager ``raw`` is the same as ``cbreak``, except interrupt (^C), -quit (^\\), suspend (^Z), and flow control (^S, ^Q) characters are not trapped, -but instead sent directly as their natural character. This is necessary if you -actually want to handle the receipt of Ctrl+C - -inkey -~~~~~ - -The method ``inkey`` resolves many issues with terminal input by returning -a unicode-derived *Keypress* instance. Although its return value may be -printed, joined with, or compared to other unicode strings, it also provides -the special attributes ``is_sequence`` (bool), ``code`` (int), -and ``name`` (str):: - - from blessed import Terminal - - t = Terminal() - - print("press 'q' to quit.") - with t.cbreak(): - val = None - while val not in (u'q', u'Q',): - val = t.inkey(timeout=5) - if not val: - # timeout - print("It sure is quiet in here ...") - elif val.is_sequence: - print("got sequence: {}.".format((str(val), val.name, val.code))) - elif val: - print("got {}.".format(val)) - print('bye!') - -Its output might appear as:: - - got sequence: ('\x1b[A', 'KEY_UP', 259). - got sequence: ('\x1b[1;2A', 'KEY_SUP', 337). - got sequence: ('\x1b[17~', 'KEY_F6', 270). - got sequence: ('\x1b', 'KEY_ESCAPE', 361). - got sequence: ('\n', 'KEY_ENTER', 343). - got /. - It sure is quiet in here ... - got sequence: ('\x1bOP', 'KEY_F1', 265). - It sure is quiet in here ... - got q. - bye! - -A ``timeout`` value of *None* (default) will block forever. Any other value -specifies the length of time to poll for input, if no input is received after -such time has elapsed, an empty string is returned. A ``timeout`` value of *0* -is non-blocking. - -keyboard codes -~~~~~~~~~~~~~~ - -The return value of the *Terminal* method ``inkey`` is an instance of the -class ``Keystroke``, and may be inspected for its property ``is_sequence`` -(bool). When *True*, the value is a **multibyte sequence**, representing -a special non-alphanumeric key of your keyboard. - -The ``code`` property (int) may then be compared with attributes of -*Terminal*, which are duplicated from those seen in the manpage -`curs_getch(3)`_ or the curses_ module, with the following helpful -aliases: - -* use ``KEY_DELETE`` for ``KEY_DC`` (chr(127)). -* use ``KEY_TAB`` for chr(9). -* use ``KEY_INSERT`` for ``KEY_IC``. -* use ``KEY_PGUP`` for ``KEY_PPAGE``. -* use ``KEY_PGDOWN`` for ``KEY_NPAGE``. -* use ``KEY_ESCAPE`` for ``KEY_EXIT``. -* use ``KEY_SUP`` for ``KEY_SR`` (shift + up). -* use ``KEY_SDOWN`` for ``KEY_SF`` (shift + down). -* use ``KEY_DOWN_LEFT`` for ``KEY_C1`` (keypad lower-left). -* use ``KEY_UP_RIGHT`` for ``KEY_A1`` (keypad upper-left). -* use ``KEY_DOWN_RIGHT`` for ``KEY_C3`` (keypad lower-left). -* use ``KEY_UP_RIGHT`` for ``KEY_A3`` (keypad lower-right). -* use ``KEY_CENTER`` for ``KEY_B2`` (keypad center). -* use ``KEY_BEGIN`` for ``KEY_BEG``. - -The *name* property of the return value of ``inkey()`` will prefer -these aliases over the built-in curses_ names. - -The following are **not** available in the curses_ module, but -provided for keypad support, especially where the ``keypad()`` -context manager is used: - -* ``KEY_KP_MULTIPLY`` -* ``KEY_KP_ADD`` -* ``KEY_KP_SEPARATOR`` -* ``KEY_KP_SUBTRACT`` -* ``KEY_KP_DECIMAL`` -* ``KEY_KP_DIVIDE`` -* ``KEY_KP_0`` through ``KEY_KP_9`` - -Shopping List -============= - -There are decades of legacy tied up in terminal interaction, so attention to -detail and behavior in edge cases make a difference. Here are some ways -*Blessed* has your back: - -* Uses the `terminfo(5)`_ database so it works with any terminal type -* Provides up-to-the-moment terminal height and width, so you can respond to - terminal size changes (*SIGWINCH* signals). (Most other libraries query the - ``COLUMNS`` and ``LINES`` environment variables or the ``cols`` or ``lines`` - terminal capabilities, which don't update promptly, if at all.) -* Avoids making a mess if the output gets piped to a non-terminal. -* Works great with standard Python string formatting. -* Provides convenient access to **all** terminal capabilities. -* Outputs to any file-like object (*StringIO*, file), not just *stdout*. -* Keeps a minimum of internal state, so you can feel free to mix and match with - calls to curses or whatever other terminal libraries you like -* Safely decodes internationalization keyboard input to their unicode equivalents. -* Safely decodes multibyte sequences for application/arrow keys. -* Allows the printable length of strings containing sequences to be determined. -* Provides plenty of context managers to safely express various terminal modes, - restoring to a safe state upon exit. - -Blessed does not provide... - -* Native color support on the Windows command prompt. A PDCurses_ build - of python for windows provides only partial support at this time -- there - are plans to merge with the ansi_ module in concert with colorama_ to - resolve this. Patches welcome! - - -Devlopers, Bugs -=============== - -Bugs or suggestions? Visit the `issue tracker`_. -`API Documentation`_ is available. - -For patches, please construct a test case if possible. - -To test, execute ``./setup.py develop`` followed by command ``tox``. - -Pull requests are tested by Travis-CI. - - -License -======= - -Blessed is derived from Blessings, which is under the MIT License, and -shares the same. See the LICENSE file. - - -Version History -=============== -1.9 - * enhancement: ``break_long_words=True`` now supported by ``term.wrap`` - * workaround: ignore curses.error 'tparm() returned NULL', this occurs - on win32 platforms using PDCurses_ where ``tparm()`` is not - implemented. - * enhancement: new context manager ``keypad()``, which enables - keypad application keys such as the diagonal keys on the numpad. - * bugfix: translate keypad application keys correctly. - * enhancement: no longer depend on the '2to3' tool for python 3 support. - * enhancement: allow ``civis`` and ``cnorm`` (*hide_cursor*, *normal_hide*) - to work with terminal-type *ansi* by emulating support by proxy. - * enhancement: new public attribute: ``kind``. - -1.8 - * enhancement: export keyboard-read function as public method ``getch()``, - so that it may be overridden by custom terminal implementers. - * enhancement: allow ``inkey()`` and ``kbhit()`` to return early when - interrupted by signal by passing argument ``_intr_continue=False``. - * enhancement: allow ``hpa`` and ``vpa`` (*move_x*, *move_y*) to work on - tmux(1) or screen(1) by emulating support by proxy. - * enhancement: ``setup.py develop`` ensures virtualenv and installs tox, - and ``setup.py test`` calls tox. Requires pythons defined by tox.ini. - * enhancement: add ``rstrip()`` and ``lstrip()``, strips both sequences - and trailing or leading whitespace, respectively. - * enhancement: include wcwidth_ library support for ``length()``, the - printable width of many kinds of CJK (Chinese, Japanese, Korean) ideographs - are more correctly determined. - * enhancement: better support for detecting the length or sequences of - externally-generated *ecma-48* codes when using ``xterm`` or ``aixterm``. - * bugfix: if ``locale.getpreferredencoding()`` returns empty string or an - encoding that is not a valid encoding for ``codecs.getincrementaldecoder``, - fallback to ascii and emit a warning. - * bugfix: ensure ``FormattingString`` and ``ParameterizingString`` may be - pickled. - * bugfix: allow ``term.inkey()`` and related to be called without a keyboard. - * **change**: ``term.keyboard_fd`` is set ``None`` if ``stream`` or - ``sys.stdout`` is not a tty, making ``term.inkey()``, ``term.cbreak()``, - ``term.raw()``, no-op. - * bugfix: ``\x1bOH`` (KEY_HOME) was incorrectly mapped as KEY_LEFT. - -1.7 - * Forked github project `erikrose/blessings`_ to `jquast/blessed`_, this - project was previously known as **blessings** version 1.6 and prior. - * introduced: context manager ``cbreak()`` and ``raw()``, which is equivalent - to ``tty.setcbreak()`` and ``tty.setraw()``, allowing input from stdin to - be read as each key is pressed. - * introduced: ``inkey()`` and ``kbhit()``, which will return 1 or more - characters as a unicode sequence, with attributes ``.code`` and ``.name``, - with value non-``None`` when a multibyte sequence is received, allowing - application keys (such as UP/DOWN) to be detected. Optional value - ``timeout`` allows timed asynchronous polling or blocking. - * introduced: ``center()``, ``rjust()``, ``ljust()``, ``strip()``, and - ``strip_seqs()`` methods. Allows text containing sequences to be aligned - to screen, or ``width`` specified. - * introduced: ``wrap()`` method. Allows text containing sequences to be - word-wrapped without breaking mid-sequence, honoring their printable width. - * bugfix: cannot call ``setupterm()`` more than once per process -- issue a - warning about what terminal kind subsequent calls will use. - * bugfix: resolved issue where ``number_of_colors`` fails when - ``does_styling`` is ``False``. Resolves issue where piping tests - output would fail. - * bugfix: warn and set ``does_styling`` to ``False`` when TERM is unknown. - * bugfix: allow unsupported terminal capabilities to be callable just as - supported capabilities, so that the return value of ``term.color(n)`` may - be called on terminals without color capabilities. - * bugfix: for terminals without underline, such as vt220, - ``term.underline('text')``. Would be ``u'text' + term.normal``, now is - only ``u'text'``. - * enhancement: some attributes are now properties, raise exceptions when - assigned. - * enhancement: pypy is now a supported python platform implementation. - * enhancement: removed pokemon ``curses.error`` exceptions. - * enhancement: converted nose tests to pytest, merged travis and tox. - * enhancement: pytest fixtures, paired with a new ``@as_subprocess`` - decorator - are used to test a multitude of terminal types. - * enhancement: test accessories ``@as_subprocess`` resolves various issues - with different terminal types that previously went untested. - * deprecation: python2.5 is no longer supported (as tox does not supported). - -1.6 - * Add ``does_styling`` property. This takes ``force_styling`` into account - and should replace most uses of ``is_a_tty``. - * Make ``is_a_tty`` a read-only property, like ``does_styling``. Writing to - it never would have done anything constructive. - * Add ``fullscreen()`` and ``hidden_cursor()`` to the auto-generated docs. - -1.5.1 - * Clean up fabfile, removing the redundant ``test`` command. - * Add Travis support. - * Make ``python setup.py test`` work without spurious errors on 2.6. - * Work around a tox parsing bug in its config file. - * Make context managers clean up after themselves even if there's an - exception. (Vitja Makarov) - * Parameterizing a capability no longer crashes when there is no tty. (Vitja - Makarov) - -1.5 - * Add syntactic sugar and documentation for ``enter_fullscreen`` and - ``exit_fullscreen``. - * Add context managers ``fullscreen()`` and ``hidden_cursor()``. - * Now you can force a *Terminal* never to emit styles by passing - ``force_styling=None``. - -1.4 - * Add syntactic sugar for cursor visibility control and single-space-movement - capabilities. - * Endorse the ``location()`` idiom for restoring cursor position after a - series of manual movements. - * Fix a bug in which ``location()`` wouldn't do anything when passed zeros. - * Allow tests to be run with ``python setup.py test``. - -1.3 - * Added ``number_of_colors``, which tells you how many colors the terminal - supports. - * Made ``color(n)`` and ``on_color(n)`` callable to wrap a string, like the - named colors can. Also, make them both fall back to the ``setf`` and - ``setb`` capabilities (like the named colors do) if the ANSI ``setaf`` and - ``setab`` aren't available. - * Allowed ``color`` attr to act as an unparametrized string, not just a - callable. - * Made ``height`` and ``width`` examine any passed-in stream before falling - back to stdout. (This rarely if ever affects actual behavior; it's mostly - philosophical.) - * Made caching simpler and slightly more efficient. - * Got rid of a reference cycle between Terminals and FormattingStrings. - * Updated docs to reflect that terminal addressing (as in ``location()``) is - 0-based. - -1.2 - * Added support for Python 3! We need 3.2.3 or greater, because the curses - library couldn't decide whether to accept strs or bytes before that - (http://bugs.python.org/issue10570). - * Everything that comes out of the library is now unicode. This lets us - support Python 3 without making a mess of the code, and Python 2 should - continue to work unless you were testing types (and badly). Please file a - bug if this causes trouble for you. - * Changed to the MIT License for better world domination. - * Added Sphinx docs. - -1.1 - * Added nicely named attributes for colors. - * Introduced compound formatting. - * Added wrapper behavior for styling and colors. - * Let you force capabilities to be non-empty, even if the output stream is - not a terminal. - * Added the ``is_a_tty`` attribute for telling whether the output stream is a - terminal. - * Sugared the remaining interesting string capabilities. - * Let ``location()`` operate on just an x *or* y coordinate. - -1.0 - * Extracted Blessings from `nose-progressive`_. - -.. _`nose-progressive`: http://pypi.python.org/pypi/nose-progressive/ -.. _`erikrose/blessings`: https://github.com/erikrose/blessings -.. _`jquast/blessed`: https://github.com/jquast/blessed -.. _`issue tracker`: https://github.com/jquast/blessed/issues/ -.. _curses: https://docs.python.org/library/curses.html -.. _couleur: https://pypi.python.org/pypi/couleur -.. _colorama: https://pypi.python.org/pypi/colorama -.. _wcwidth: https://pypi.python.org/pypi/wcwidth -.. _`cbreak(3)`: http://www.openbsd.org/cgi-bin/man.cgi?query=cbreak&apropos=0&sektion=3 -.. _`curs_getch(3)`: http://www.openbsd.org/cgi-bin/man.cgi?query=curs_getch&apropos=0&sektion=3 -.. _`termios(4)`: http://www.openbsd.org/cgi-bin/man.cgi?query=termios&apropos=0&sektion=4 -.. _`terminfo(5)`: http://www.openbsd.org/cgi-bin/man.cgi?query=terminfo&apropos=0&sektion=5 -.. _tigetstr: http://www.openbsd.org/cgi-bin/man.cgi?query=tigetstr&sektion=3 -.. _tparm: http://www.openbsd.org/cgi-bin/man.cgi?query=tparm&sektion=3 -.. _SIGWINCH: https://en.wikipedia.org/wiki/SIGWINCH -.. _`API Documentation`: http://blessed.rtfd.org -.. _`PDCurses`: http://www.lfd.uci.edu/~gohlke/pythonlibs/#curses -.. _`ansi`: https://github.com/tehmaze/ansi diff --git a/README.rst b/README.rst new file mode 120000 index 00000000..9a716adc --- /dev/null +++ b/README.rst @@ -0,0 +1 @@ +docs/intro.rst \ No newline at end of file diff --git a/bin/editor.py b/bin/editor.py index 2f0feaea..b3060f0b 100755 --- a/bin/editor.py +++ b/bin/editor.py @@ -1,160 +1,110 @@ #!/usr/bin/env python -# Dumb full-screen editor. It doesn't save anything but to the screen. -# -# "Why wont python let me read memory -# from screen like assembler? That's dumb." -hellbeard -# -# This program makes example how to deal with a keypad for directional -# movement, with both numlock on and off. +""" +A Dumb full-screen editor. + +This example program makes use of many context manager methods: +:meth:`~.Terminal.hidden_cursor`, :meth:`~.Terminal.raw`, +:meth:`~.Terminal.location`, :meth:`~.Terminal.fullscreen`, and +:meth:`~.Terminal.keypad`. + +Early curses work focused namely around writing screen editors, naturally +any serious editor would make liberal use of special modes. + +``Ctrl - L`` + refresh + +``Ctrl - C`` + quit + +``Ctrl - S`` + save +""" from __future__ import division, print_function import collections import functools + from blessed import Terminal +# python 2/3 compatibility, provide 'echo' function as an +# alias for "print without newline and flush" try: - print(end='', flush=True) - echo = lambda text: ( - functools.partial(print, end='', flush=True)(text)) + # pylint: disable=invalid-name + # Invalid constant name "echo" + echo = functools.partial(print, end='', flush=True) + echo(u'') except TypeError: # TypeError: 'flush' is an invalid keyword argument for this function - # python 2 import sys def echo(text): - sys.stdout.write(text) + """Display ``text`` and flush output.""" + sys.stdout.write(u'{}'.format(text)) sys.stdout.flush() -echo_yx = lambda cursor, text: ( - echo(cursor.term.move(cursor.y, cursor.x) + text)) +def input_filter(keystroke): + """ + For given keystroke, return whether it should be allowed as input. + + This somewhat requires that the interface use special + application keys to perform functions, as alphanumeric + input intended for persisting could otherwise be interpreted as a + command sequence. + """ + if keystroke.is_sequence: + # Namely, deny multi-byte sequences (such as '\x1b[A'), + return False + if ord(keystroke) < ord(u' '): + # or control characters (such as ^L), + return False + return True + +def echo_yx(cursor, text): + """Move to ``cursor`` and display ``text``.""" + echo(cursor.term.move(cursor.y, cursor.x) + text) Cursor = collections.namedtuple('Point', ('y', 'x', 'term')) -above = lambda csr, n: ( - Cursor(y=max(0, csr.y - n), - x=csr.x, - term=csr.term)) - -below = lambda csr, n: ( - Cursor(y=min(csr.term.height - 1, csr.y + n), - x=csr.x, - term=csr.term)) - -right_of = lambda csr, n: ( - Cursor(y=csr.y, - x=min(csr.term.width - 1, csr.x + n), - term=csr.term)) - -left_of = lambda csr, n: ( - Cursor(y=csr.y, - x=max(0, csr.x - n), - term=csr.term)) - -home = lambda csr: ( - Cursor(y=csr.y, - x=0, - term=csr.term)) - -end = lambda csr: ( - Cursor(y=csr.y, - x=csr.term.width - 1, - term=csr.term)) - -bottom = lambda csr: ( - Cursor(y=csr.term.height - 1, - x=csr.x, - term=csr.term)) - -top = lambda csr: ( - Cursor(y=1, - x=csr.x, - term=csr.term)) - -center = lambda csr: Cursor( - csr.term.height // 2, - csr.term.width // 2, - csr.term) - - -lookup_move = lambda inp_code, csr, term: { - # arrows, including angled directionals - csr.term.KEY_END: below(left_of(csr, 1), 1), - csr.term.KEY_KP_1: below(left_of(csr, 1), 1), - - csr.term.KEY_DOWN: below(csr, 1), - csr.term.KEY_KP_2: below(csr, 1), - - csr.term.KEY_PGDOWN: below(right_of(csr, 1), 1), - csr.term.KEY_LR: below(right_of(csr, 1), 1), - csr.term.KEY_KP_3: below(right_of(csr, 1), 1), - - csr.term.KEY_LEFT: left_of(csr, 1), - csr.term.KEY_KP_4: left_of(csr, 1), - - csr.term.KEY_CENTER: center(csr), - csr.term.KEY_KP_5: center(csr), - - csr.term.KEY_RIGHT: right_of(csr, 1), - csr.term.KEY_KP_6: right_of(csr, 1), - - csr.term.KEY_HOME: above(left_of(csr, 1), 1), - csr.term.KEY_KP_7: above(left_of(csr, 1), 1), - - csr.term.KEY_UP: above(csr, 1), - csr.term.KEY_KP_8: above(csr, 1), - - csr.term.KEY_PGUP: above(right_of(csr, 1), 1), - csr.term.KEY_KP_9: above(right_of(csr, 1), 1), - - # shift + arrows - csr.term.KEY_SLEFT: left_of(csr, 10), - csr.term.KEY_SRIGHT: right_of(csr, 10), - csr.term.KEY_SDOWN: below(csr, 10), - csr.term.KEY_SUP: above(csr, 10), - - # carriage return - csr.term.KEY_ENTER: home(below(csr, 1)), -}.get(inp_code, csr) - - def readline(term, width=20): - # a rudimentary readline function - string = u'' + """A rudimentary readline implementation.""" + text = u'' while True: inp = term.inkey() if inp.code == term.KEY_ENTER: break elif inp.code == term.KEY_ESCAPE or inp == chr(3): - string = None + text = None break - elif not inp.is_sequence and len(string) < width: - string += inp + elif not inp.is_sequence and len(text) < width: + text += inp echo(inp) elif inp.code in (term.KEY_BACKSPACE, term.KEY_DELETE): - string = string[:-1] - echo('\b \b') - return string + text = text[:-1] + echo(u'\b \b') + return text def save(screen, fname): + """Save screen contents to file.""" if not fname: return - with open(fname, 'w') as fp: + with open(fname, 'w') as fout: cur_row = cur_col = 0 for (row, col) in sorted(screen): char = screen[(row, col)] while row != cur_row: cur_row += 1 cur_col = 0 - fp.write(u'\n') + fout.write(u'\n') while col > cur_col: cur_col += 1 - fp.write(u' ') - fp.write(char) + fout.write(u' ') + fout.write(char) cur_col += 1 - fp.write(u'\n') + fout.write(u'\n') def redraw(term, screen, start=None, end=None): + """Redraw the screen.""" if start is None and end is None: echo(term.clear) start, end = (Cursor(y=min([y for (y, x) in screen or [(0, 0)]]), @@ -177,8 +127,88 @@ def redraw(term, screen, start=None, end=None): # just write past last one echo(screen[row, col]) - def main(): + """Program entry point.""" + above = lambda csr, n: ( + Cursor(y=max(0, csr.y - n), + x=csr.x, + term=csr.term)) + + below = lambda csr, n: ( + Cursor(y=min(csr.term.height - 1, csr.y + n), + x=csr.x, + term=csr.term)) + + right_of = lambda csr, n: ( + Cursor(y=csr.y, + x=min(csr.term.width - 1, csr.x + n), + term=csr.term)) + + left_of = lambda csr, n: ( + Cursor(y=csr.y, + x=max(0, csr.x - n), + term=csr.term)) + + home = lambda csr: ( + Cursor(y=csr.y, + x=0, + term=csr.term)) + + end = lambda csr: ( + Cursor(y=csr.y, + x=csr.term.width - 1, + term=csr.term)) + + bottom = lambda csr: ( + Cursor(y=csr.term.height - 1, + x=csr.x, + term=csr.term)) + + center = lambda csr: Cursor( + csr.term.height // 2, + csr.term.width // 2, + csr.term) + + lookup_move = lambda inp_code, csr, term: { + # arrows, including angled directionals + csr.term.KEY_END: below(left_of(csr, 1), 1), + csr.term.KEY_KP_1: below(left_of(csr, 1), 1), + + csr.term.KEY_DOWN: below(csr, 1), + csr.term.KEY_KP_2: below(csr, 1), + + csr.term.KEY_PGDOWN: below(right_of(csr, 1), 1), + csr.term.KEY_LR: below(right_of(csr, 1), 1), + csr.term.KEY_KP_3: below(right_of(csr, 1), 1), + + csr.term.KEY_LEFT: left_of(csr, 1), + csr.term.KEY_KP_4: left_of(csr, 1), + + csr.term.KEY_CENTER: center(csr), + csr.term.KEY_KP_5: center(csr), + + csr.term.KEY_RIGHT: right_of(csr, 1), + csr.term.KEY_KP_6: right_of(csr, 1), + + csr.term.KEY_HOME: above(left_of(csr, 1), 1), + csr.term.KEY_KP_7: above(left_of(csr, 1), 1), + + csr.term.KEY_UP: above(csr, 1), + csr.term.KEY_KP_8: above(csr, 1), + + csr.term.KEY_PGUP: above(right_of(csr, 1), 1), + csr.term.KEY_KP_9: above(right_of(csr, 1), 1), + + # shift + arrows + csr.term.KEY_SLEFT: left_of(csr, 10), + csr.term.KEY_SRIGHT: right_of(csr, 10), + csr.term.KEY_SDOWN: below(csr, 10), + csr.term.KEY_SUP: above(csr, 10), + + # carriage return + csr.term.KEY_ENTER: home(below(csr, 1)), + }.get(inp_code, csr) + term = Terminal() csr = Cursor(0, 0, term) screen = {} @@ -199,8 +229,8 @@ def main(): elif inp == chr(19): # ^s saves echo_yx(home(bottom(csr)), - term.ljust(term.bold_white('Filename: '))) - echo_yx(right_of(home(bottom(csr)), len('Filename: ')), u'') + term.ljust(term.bold_white(u'Filename: '))) + echo_yx(right_of(home(bottom(csr)), len(u'Filename: ')), u'') save(screen, readline(term)) echo_yx(home(bottom(csr)), term.clear_eol) redraw(term=term, screen=screen, @@ -212,13 +242,15 @@ def main(): # ^l refreshes redraw(term=term, screen=screen) - n_csr = lookup_move(inp.code, csr, term) + else: + n_csr = lookup_move(inp.code, csr, term) + if n_csr != csr: # erase old cursor, echo_yx(csr, screen.get((csr.y, csr.x), u' ')) csr = n_csr - elif not inp.is_sequence and inp.isprintable(): + elif input_filter(inp): echo_yx(csr, inp) screen[(csr.y, csr.x)] = inp.__str__() n_csr = right_of(csr, 1) diff --git a/bin/keymatrix.py b/bin/keymatrix.py index daaad990..eb83673f 100755 --- a/bin/keymatrix.py +++ b/bin/keymatrix.py @@ -1,90 +1,116 @@ #!/usr/bin/env python -from __future__ import division -from blessed import Terminal +""" +A simple "game": hit all application keys to win. + +Display all known key capabilities that may match the terminal. +As each key is pressed on input, it is lit up and points are scored. +""" +from __future__ import division, print_function +import functools import sys +from blessed import Terminal -def main(): - """ - Displays all known key capabilities that may match the terminal. - As each key is pressed on input, it is lit up and points are scored. - """ - term = Terminal() - score = level = hit_highbit = hit_unicode = 0 - dirty = True +# python 2/3 compatibility, provide 'echo' function as an +# alias for "print without newline and flush" +try: + # pylint: disable=invalid-name + # Invalid constant name "echo" + echo = functools.partial(print, end='', flush=True) + echo(u'') +except TypeError: + # TypeError: 'flush' is an invalid keyword argument for this function - def refresh(term, board, level, score, inp): - sys.stdout.write(term.home + term.clear) - level_color = level % 7 - if level_color == 0: - level_color = 4 - bottom = 0 - for keycode, attr in board.items(): - sys.stdout.write(u''.join(( - term.move(attr['row'], attr['column']), - term.color(level_color), - (term.reverse if attr['hit'] else term.bold), - keycode, - term.normal))) - bottom = max(bottom, attr['row']) - sys.stdout.write(term.move(term.height, 0) - + 'level: %s score: %s' % (level, score,)) + def echo(text): + """Display ``text`` and flush output.""" + sys.stdout.write(u'{}'.format(text)) sys.stdout.flush() - if bottom >= (term.height - 5): - sys.stderr.write( - ('\n' * (term.height // 2)) + - term.center(term.red_underline('cheater!')) + '\n') - sys.stderr.write( - term.center("(use a larger screen)") + - ('\n' * (term.height // 2))) - sys.exit(1) - for row, inp in enumerate(inps[(term.height - (bottom + 2)) * -1:]): - sys.stdout.write(term.move(bottom + row+1)) - sys.stdout.write('%r, %s, %s' % (inp.__str__() if inp.is_sequence - else inp, inp.code, inp.name, )) - sys.stdout.flush() - def build_gameboard(term): - column, row = 0, 0 - board = dict() - spacing = 2 - for keycode in sorted(term._keycodes.values()): - if (keycode.startswith('KEY_F') - and keycode[-1].isdigit() - and int(keycode[len('KEY_F'):]) > 24): - continue - if column + len(keycode) + (spacing * 2) >= term.width: - column = 0 - row += 1 - board[keycode] = {'column': column, - 'row': row, - 'hit': 0, - } - column += len(keycode) + (spacing * 2) - return board +def refresh(term, board, level, score, inps): + """Refresh the game screen.""" + echo(term.home + term.clear) + level_color = level % 7 + if level_color == 0: + level_color = 4 + bottom = 0 + for keycode, attr in board.items(): + echo(u''.join(( + term.move(attr['row'], attr['column']), + term.color(level_color), + (term.reverse if attr['hit'] else term.bold), + keycode, + term.normal))) + bottom = max(bottom, attr['row']) + echo(term.move(term.height, 0) + + 'level: %s score: %s' % (level, score,)) + if bottom >= (term.height - 5): + sys.stderr.write( + ('\n' * (term.height // 2)) + + term.center(term.red_underline('cheater!')) + '\n') + sys.stderr.write( + term.center("(use a larger screen)") + + ('\n' * (term.height // 2))) + sys.exit(1) + echo(term.move(bottom + 1, 0)) + echo('Press ^C to exit.') + for row, inp in enumerate(inps[(term.height - (bottom + 3)) * -1:], 1): + echo(term.move(bottom + row + 1, 0)) + echo('{0!r}, {1}, {2}'.format( + inp.__str__() if inp.is_sequence else inp, + inp.code, + inp.name)) + echo(term.clear_eol) - def add_score(score, pts, level): - lvl_multiplier = 10 - score += pts - if 0 == (score % (pts * lvl_multiplier)): - level += 1 - return score, level +def build_gameboard(term): + """Build the gameboard layout.""" + column, row = 0, 0 + board = dict() + spacing = 2 + for keycode in sorted(term._keycodes.values()): + if (keycode.startswith('KEY_F') + and keycode[-1].isdigit() + and int(keycode[len('KEY_F'):]) > 24): + continue + if column + len(keycode) + (spacing * 2) >= term.width: + column = 0 + row += 1 + board[keycode] = {'column': column, + 'row': row, + 'hit': 0, + } + column += len(keycode) + (spacing * 2) + return board + +def add_score(score, pts, level): + """Add points to score, determine and return new score and level.""" + lvl_multiplier = 10 + score += pts + if 0 == (score % (pts * lvl_multiplier)): + level += 1 + return score, level + + +def main(): + """Program entry point.""" + term = Terminal() + score = level = hit_highbit = hit_unicode = 0 + dirty = True - gb = build_gameboard(term) + gameboard = build_gameboard(term) inps = [] with term.raw(), term.keypad(), term.location(): inp = term.inkey(timeout=0) while inp != chr(3): if dirty: - refresh(term, gb, level, score, inps) + refresh(term, gameboard, level, score, inps) dirty = False inp = term.inkey(timeout=5.0) dirty = True if (inp.is_sequence and - inp.name in gb and - 0 == gb[inp.name]['hit']): - gb[inp.name]['hit'] = 1 + inp.name in gameboard and + 0 == gameboard[inp.name]['hit']): + gameboard[inp.name]['hit'] = 1 score, level = add_score(score, 100, level) elif inp and not inp.is_sequence and 128 <= ord(inp) <= 255: hit_highbit += 1 @@ -97,8 +123,8 @@ def add_score(score, pts, level): inps.append(inp) with term.cbreak(): - sys.stdout.write(term.move(term.height)) - sys.stdout.write( + echo(term.move(term.height)) + echo( u'{term.clear_eol}Your final score was {score} ' u'at level {level}{term.clear_eol}\n' u'{term.clear_eol}\n' diff --git a/bin/on_resize.py b/bin/on_resize.py index 7398260c..7a483376 100755 --- a/bin/on_resize.py +++ b/bin/on_resize.py @@ -4,32 +4,42 @@ Window size changes are caught by the 'on_resize' function using a traditional signal handler. Meanwhile, blocking keyboard input is displayed to stdout. -If a resize event is discovered, an empty string is returned by term.inkey() -when _intr_continue is False, as it is here. +If a resize event is discovered, an empty string is returned by +term.inkey(). """ +from __future__ import print_function import signal + from blessed import Terminal -term = Terminal() - - -def on_resize(sig, action): - # Its generally not a good idea to put blocking functions (such as print) - # within a signal handler -- if another SIGWINCH is recieved while this - # function blocks, an error will occur. In most programs, you'll want to - # set some kind of 'dirty' flag, perhaps by a Semaphore or global variable. - print('height={t.height}, width={t.width}\r'.format(t=term)) - -signal.signal(signal.SIGWINCH, on_resize) - -# note that, a terminal driver actually writes '\r\n' when '\n' is found, but -# in raw mode, we are allowed to write directly to the terminal without the -# interference of such driver -- so we must write \r\n ourselves; as python -# will append '\n' to our print statements, we simply end our statements with -# \r. -with term.raw(): - print("press 'X' to stop.\r") - inp = None - while inp != 'X': - inp = term.inkey(_intr_continue=False) - print(repr(inp) + u'\r') +def main(): + """Program entry point.""" + term = Terminal() + + def on_resize(*args): + # pylint: disable=unused-argument + # Unused argument 'args' + + # Its generally not a good idea to put blocking functions (such as + # print) within a signal handler -- if another SIGWINCH is received + # while this function blocks, an error will occur. + + # In most programs, you'll want to set some kind of 'dirty' flag, + # perhaps by a Semaphore like threading.Event or (thanks to the GIL) + # a simple global variable will suffice. + print('height={t.height}, width={t.width}\r'.format(t=term)) + + signal.signal(signal.SIGWINCH, on_resize) + + # display initial size + on_resize(term) + + with term.cbreak(): + print("press 'X' to stop.") + inp = None + while inp != 'X': + inp = term.inkey() + print(repr(inp)) + +if __name__ == '__main__': + main() diff --git a/bin/progress_bar.py b/bin/progress_bar.py index 4fd4ca09..037f1ad2 100755 --- a/bin/progress_bar.py +++ b/bin/progress_bar.py @@ -9,11 +9,13 @@ provides a proxy. """ from __future__ import print_function -from blessed import Terminal import sys +from blessed import Terminal + def main(): + """Program entry point.""" term = Terminal() assert term.hpa(1) != u'', ( 'Terminal does not support hpa (Horizontal position absolute)') @@ -29,9 +31,14 @@ def main(): offset = -1 elif col <= 1: offset = 1 - sys.stderr.write(term.move_x(col) + u'.' if offset == -1 else '=') + sys.stderr.write(term.move_x(col)) + if offset == -1: + sys.stderr.write(u'.') + else: + sys.stderr.write(u'=') col += offset - sys.stderr.write(term.move_x(col) + u'|\b') + sys.stderr.write(term.move_x(col)) + sys.stderr.write(u'|\b') sys.stderr.flush() inp = term.inkey(0.04) print() diff --git a/bin/tprint.py b/bin/tprint.py index e85f5807..27ea046e 100755 --- a/bin/tprint.py +++ b/bin/tprint.py @@ -1,18 +1,25 @@ #!/usr/bin/env python - +"""A simple cmd-line tool for displaying FormattingString capabilities.""" +from __future__ import print_function import argparse -from blessed import Terminal -parser = argparse.ArgumentParser( - description='displays argument as specified style') +def main(): + """Program entry point.""" + from blessed import Terminal + + parser = argparse.ArgumentParser( + description='displays argument as specified style') + + parser.add_argument('style', type=str, help='style formatter') + parser.add_argument('text', type=str, nargs='+') -parser.add_argument('style', type=str, help='style formatter') -parser.add_argument('text', type=str, nargs='+') + term = Terminal() + args = parser.parse_args() -term = Terminal() -args = parser.parse_args() + style = getattr(term, args.style) -style = getattr(term, args.style) + print(style(' '.join(args.text))) -print(style(' '.join(args.text))) +if __name__ == '__main__': + main() diff --git a/bin/worms.py b/bin/worms.py index 49416793..907667ec 100755 --- a/bin/worms.py +++ b/bin/worms.py @@ -7,22 +7,26 @@ from __future__ import division, print_function from collections import namedtuple -from random import randrange from functools import partial +from random import randrange + from blessed import Terminal # python 2/3 compatibility, provide 'echo' function as an # alias for "print without newline and flush" try: + # pylint: disable=invalid-name + # Invalid constant name "echo" echo = partial(print, end='', flush=True) - echo('begin.') + echo(u'') except TypeError: # TypeError: 'flush' is an invalid keyword argument for this function import sys - def echo(object): - sys.stdout.write(u'{}'.format(object)) + def echo(text): + """python 2 version of print(end='', flush=True).""" + sys.stdout.write(u'{0}'.format(text)) sys.stdout.flush() # a worm is a list of (y, x) segments Locations @@ -39,97 +43,132 @@ def echo(object): # these functions return a new Location instance, given # the direction indicated by their name. LEFT = (0, -1) -left_of = lambda segment, term: Location( - y=segment.y, - x=max(0, segment.x - 1)) - RIGHT = (0, 1) -right_of = lambda segment, term: Location( - y=segment.y, - x=min(term.width - 1, segment.x + 1)) - UP = (-1, 0) -above = lambda segment, term: Location( - y=max(0, segment.y - 1), - x=segment.x) - DOWN = (1, 0) -below = lambda segment, term: Location( - y=min(term.height - 1, segment.y + 1), - x=segment.x) - -# return a direction function that defines the new bearing for any matching -# keyboard code of inp_code; otherwise, the function for the current bearing. -next_bearing = lambda term, inp_code, bearing: { - term.KEY_LEFT: left_of, - term.KEY_RIGHT: right_of, - term.KEY_UP: above, - term.KEY_DOWN: below, -}.get(inp_code, - # direction function given the current bearing - {LEFT: left_of, - RIGHT: right_of, - UP: above, - DOWN: below}[(bearing.y, bearing.x)]) - - -# return new bearing given the movement f(x). -change_bearing = lambda f_mov, segment, term: Direction( - f_mov(segment, term).y - segment.y, - f_mov(segment, term).x - segment.x) - -# direction-flipped check, reject traveling in opposite direction. -bearing_flipped = lambda dir1, dir2: ( - (0, 0) == (dir1.y + dir2.y, dir1.x + dir2.x) -) - -# returns True if `loc' matches any (y, x) coordinates, -# within list `segments' -- such as a list composing a worm. -hit_any = lambda loc, segments: loc in segments - -# same as above, but `locations' is also an array of (y, x) coordinates. -hit_vany = lambda locations, segments: any( - hit_any(loc, segments) for loc in locations) - -# returns True if segments are same position (hit detection) -hit = lambda src, dst: src.x == dst.x and src.y == dst.y - -# returns new worm_length if current nibble is hit, -next_wormlength = lambda nibble, head, worm_length: ( - worm_length + nibble.value if hit(head, nibble.location) - else worm_length) - -# returns new speed if current nibble is hit, -next_speed = lambda nibble, head, speed, modifier: ( - speed * modifier if hit(head, nibble.location) - else speed) - -# when displaying worm head, show a different glyph for horizontal/vertical -head_glyph = lambda direction: (u':' if direction in (left_of, right_of) - else u'"') - - -# provide the next nibble -- continuously generate a random new nibble so -# long as the current nibble hits any location of the worm, otherwise -# return a nibble of the same location and value as provided. + +def left_of(segment, term): + """Return Location left-of given segment.""" + # pylint: disable=unused-argument + # Unused argument 'term' + return Location(y=segment.y, + x=max(0, segment.x - 1)) + +def right_of(segment, term): + """Return Location right-of given segment.""" + return Location(y=segment.y, + x=min(term.width - 1, segment.x + 1)) + +def above(segment, term): + """Return Location above given segment.""" + # pylint: disable=unused-argument + # Unused argument 'term' + return Location( + y=max(0, segment.y - 1), + x=segment.x) + +def below(segment, term): + """Return Location below given segment.""" + return Location( + y=min(term.height - 1, segment.y + 1), + x=segment.x) + +def next_bearing(term, inp_code, bearing): + """ + Return direction function for new bearing by inp_code. + + If no inp_code matches a bearing direction, return + a function for the current bearing. + """ + return { + term.KEY_LEFT: left_of, + term.KEY_RIGHT: right_of, + term.KEY_UP: above, + term.KEY_DOWN: below, + }.get(inp_code, + # direction function given the current bearing + {LEFT: left_of, + RIGHT: right_of, + UP: above, + DOWN: below}[(bearing.y, bearing.x)]) + + +def change_bearing(f_mov, segment, term): + """Return new bearing given the movement f(x).""" + return Direction( + f_mov(segment, term).y - segment.y, + f_mov(segment, term).x - segment.x) + +def bearing_flipped(dir1, dir2): + """ + direction-flipped check. + + Return true if dir2 travels in opposite direction of dir1. + """ + return (0, 0) == (dir1.y + dir2.y, dir1.x + dir2.x) + +def hit_any(loc, segments): + """Return True if `loc' matches any (y, x) coordinates within segments.""" + # `segments' -- a list composing a worm. + return loc in segments + +def hit_vany(locations, segments): + """Return True if any locations are found within any segments.""" + return any(hit_any(loc, segments) + for loc in locations) + +def hit(src, dst): + """Return True if segments are same position (hit detection).""" + return src.x == dst.x and src.y == dst.y + +def next_wormlength(nibble, head, worm_length): + """Return new worm_length if current nibble is hit.""" + if hit(head, nibble.location): + return worm_length + nibble.value + return worm_length + +def next_speed(nibble, head, speed, modifier): + """Return new speed if current nibble is hit.""" + if hit(head, nibble.location): + return speed * modifier + return speed + +def head_glyph(direction): + """Return character for worm head depending on horiz/vert orientation.""" + if direction in (left_of, right_of): + return u':' + return u'"' + + def next_nibble(term, nibble, head, worm): - l, v = nibble.location, nibble.value - while hit_vany([head] + worm, nibble_locations(l, v)): - l = Location(x=randrange(1, term.width - 1), + """ + Provide the next nibble. + + continuously generate a random new nibble so long as the current nibble + hits any location of the worm. Otherwise, return a nibble of the same + location and value as provided. + """ + loc, val = nibble.location, nibble.value + while hit_vany([head] + worm, nibble_locations(loc, val)): + loc = Location(x=randrange(1, term.width - 1), y=randrange(1, term.height - 1)) - v = nibble.value + 1 - return Nibble(l, v) + val = nibble.value + 1 + return Nibble(loc, val) -# generate an array of locations for the current nibble's location -- a digit -# such as '123' may be hit at 3 different (y, x) coordinates. def nibble_locations(nibble_location, nibble_value): + """Return array of locations for the current "nibble".""" + # generate an array of locations for the current nibble's location + # -- a digit such as '123' may be hit at 3 different (y, x) coordinates. return [Location(x=nibble_location.x + offset, y=nibble_location.y) for offset in range(0, 1 + len('{}'.format(nibble_value)) - 1)] def main(): + """Program entry point.""" + # pylint: disable=too-many-locals + # Too many local variables (20/15) term = Terminal() worm = [Location(x=term.width // 2, y=term.height // 2)] worm_length = 2 @@ -148,7 +187,8 @@ def main(): modifier = 0.93 inp = None - with term.hidden_cursor(), term.raw(): + echo(term.move(term.height, 0)) + with term.hidden_cursor(), term.cbreak(), term.location(): while inp not in (u'q', u'Q'): # delete the tail of the worm at worm_length @@ -175,10 +215,12 @@ def main(): if n_nibble != nibble: # erase the old one, careful to redraw the nibble contents # with a worm color for those portions that overlay. - for (y, x) in nibble_locations(*nibble): - echo(term.move(y, x) + (color_worm if (y, x) == head - else color_bg)(u' ')) - echo(term.normal) + for (yloc, xloc) in nibble_locations(*nibble): + echo(u''.join(( + term.move(yloc, xloc), + (color_worm if (yloc, xloc) == head + else color_bg)(u' '), + term.normal))) # and draw the new, echo(term.move(*n_nibble.location) + ( color_nibble('{}'.format(n_nibble.value)))) @@ -194,7 +236,7 @@ def main(): # wait for keyboard input, which may indicate # a new direction (up/down/left/right) - inp = term.inkey(speed) + inp = term.inkey(timeout=speed) # discover new direction, given keyboard input and/or bearing. nxt_direction = next_bearing(term, inp.code, bearing) @@ -202,8 +244,8 @@ def main(): # discover new bearing, given new direction compared to prev nxt_bearing = change_bearing(nxt_direction, head, term) - # disallow new bearing/direction when flipped (running into - # oneself, fe. travelling left while traveling right) + # disallow new bearing/direction when flipped: running into + # oneself, for example traveling left while traveling right. if not bearing_flipped(bearing, nxt_bearing): direction = nxt_direction bearing = nxt_bearing diff --git a/blessed/__init__.py b/blessed/__init__.py index 481468f0..88954e18 100644 --- a/blessed/__init__.py +++ b/blessed/__init__.py @@ -1,16 +1,18 @@ """ -A thin, practical wrapper around terminal capabilities in Python +A thin, practical wrapper around terminal capabilities in Python. http://pypi.python.org/pypi/blessed """ +# std imports import platform as _platform + +# local +from blessed.terminal import Terminal + if ('3', '0', '0') <= _platform.python_version_tuple() < ('3', '2', '2+'): # Good till 3.2.10 # Python 3.x < 3.2.3 has a bug in which tparm() erroneously takes a string. raise ImportError('Blessed needs Python 3.2.3 or greater for Python 3 ' 'support due to http://bugs.python.org/issue10570.') - -from .terminal import Terminal - __all__ = ('Terminal',) diff --git a/blessed/_binterms.py b/blessed/_binterms.py index 2b6ec663..e6ce5879 100644 --- a/blessed/_binterms.py +++ b/blessed/_binterms.py @@ -1,12 +1,13 @@ -""" Exports a list of binary terminals blessed is not able to cope with. """ -#: This list of terminals is manually managed, it describes all of the terminals -#: that blessed cannot measure the sequence length for; they contain -#: binary-packed capabilities instead of numerics, so it is not possible to -#: build regular expressions in the way that sequences.py does. +"""List of terminal definitions containing binary-packed sequences.""" + +#: This list of terminals is manually managed, it describes all of the +#: terminals that blessed cannot measure the sequence length for; they +#: contain binary-packed capabilities instead of numerics, so it is not +#: possible to build regular expressions in the way that sequences.py does. #: #: This may be generated by exporting TEST_BINTERMS, then analyzing the #: jUnit result xml written to the project folder. -binary_terminals = u""" +BINARY_TERMINALS = u""" 9term aaa+dec aaa+rv @@ -868,4 +869,10 @@ ztx """.split() -__all__ = ('binary_terminals',) +#: Message displayed when terminal containing binary-packed sequences +#: is instantiated -- the 'warnings' module is used and may be filtered away. +BINTERM_UNSUPPORTED_MSG = ( + u"Terminal kind {0!r} contains binary-packed capabilities, blessed " + u"is likely to fail to measure the length of its sequences.") + +__all__ = ('BINARY_TERMINALS', 'BINTERM_UNSUPPORTED_MSG',) diff --git a/blessed/formatters.py b/blessed/formatters.py index e77adcdb..df7bf531 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -1,58 +1,80 @@ -"This sub-module provides formatting functions." +"""This sub-module provides sequence-formatting functions.""" +# standard imports import curses -import sys -_derivatives = ('on', 'bright', 'on_bright',) +# 3rd-party +import six -_colors = set('black red green yellow blue magenta cyan white'.split()) -_compoundables = set('bold underline reverse blink dim italic shadow ' - 'standout subscript superscript'.split()) -#: Valid colors and their background (on), bright, and bright-bg derivatives. -COLORS = set(['_'.join((derivitive, color)) - for derivitive in _derivatives - for color in _colors]) | _colors +def _make_colors(): + """ + Return set of valid colors and their derivatives. + + :rtype: set + """ + derivatives = ('on', 'bright', 'on_bright',) + colors = set('black red green yellow blue magenta cyan white'.split()) + return set(['_'.join((_deravitive, _color)) + for _deravitive in derivatives + for _color in colors]) | colors -#: All valid compoundable names. -COMPOUNDABLES = (COLORS | _compoundables) -if sys.version_info[0] == 3: - text_type = str - basestring = str -else: - text_type = unicode # noqa +def _make_compoundables(colors): + """ + Return given set ``colors`` along with all "compoundable" attributes. + + :param set colors: set of color names as string. + :rtype: set + """ + _compoundables = set('bold underline reverse blink dim italic shadow ' + 'standout subscript superscript'.split()) + return colors | _compoundables -class ParameterizingString(text_type): - """A Unicode string which can be called as a parameterizing termcap. +#: Valid colors and their background (on), bright, +#: and bright-background derivatives. +COLORS = _make_colors() + +#: Attributes and colors which may be compounded by underscore. +COMPOUNDABLES = _make_compoundables(COLORS) + + +class ParameterizingString(six.text_type): + + r""" + A Unicode string which can be called as a parameterizing termcap. For example:: - >> term = Terminal() - >> color = ParameterizingString(term.color, term.normal, 'color') - >> color(9)('color #9') + >>> term = Terminal() + >>> color = ParameterizingString(term.color, term.normal, 'color') + >>> color(9)('color #9') u'\x1b[91mcolor #9\x1b(B\x1b[m' """ def __new__(cls, *args): - """P.__new__(cls, cap, [normal, [name]]) + """ + Class constructor accepting 3 positional arguments. :arg cap: parameterized string suitable for curses.tparm() - :arg normal: terminating sequence for this capability. - :arg name: name of this terminal capability. + :arg normal: terminating sequence for this capability (optional). + :arg name: name of this terminal capability (optional). """ assert len(args) and len(args) < 4, args - new = text_type.__new__(cls, args[0]) + new = six.text_type.__new__(cls, args[0]) new._normal = len(args) > 1 and args[1] or u'' new._name = len(args) > 2 and args[2] or u'' return new def __call__(self, *args): - """P(*args) -> FormattingString() + """ + Returning :class:`FormattingString` instance for given parameters. Return evaluated terminal capability (self), receiving arguments ``*args``, followed by the terminating sequence (self.normal) into - a FormattingString capable of being called. + a :class:`FormattingString` capable of being called. + + :rtype: :class:`FormattingString` or :class:`NullCallableString` """ try: # Re-encode the cap, because tparm() takes a bytestring in Python @@ -63,7 +85,7 @@ def __call__(self, *args): except TypeError as err: # If the first non-int (i.e. incorrect) arg was a string, suggest # something intelligent: - if len(args) and isinstance(args[0], basestring): + if len(args) and isinstance(args[0], six.string_types): raise TypeError( "A native or nonexistent capability template, %r received" " invalid argument %r: %s. You probably misspelled a" @@ -76,65 +98,80 @@ def __call__(self, *args): # ignore 'tparm() returned NULL', you won't get any styling, # even if does_styling is True. This happens on win32 platforms # with http://www.lfd.uci.edu/~gohlke/pythonlibs/#curses installed - if "tparm() returned NULL" not in text_type(err): + if "tparm() returned NULL" not in six.text_type(err): raise return NullCallableString() -class ParameterizingProxyString(text_type): - """A Unicode string which can be called to proxy missing termcap entries. +class ParameterizingProxyString(six.text_type): - For example:: + r""" + A Unicode string which can be called to proxy missing termcap entries. - >>> from blessed import Terminal - >>> term = Terminal('screen') - >>> hpa = ParameterizingString(term.hpa, term.normal, 'hpa') - >>> hpa(9) - u'' - >>> fmt = u'\x1b[{0}G' - >>> fmt_arg = lambda *arg: (arg[0] + 1,) - >>> hpa = ParameterizingProxyString((fmt, fmt_arg), term.normal, 'hpa') - >>> hpa(9) - u'\x1b[10G' + This class supports the function :func:`get_proxy_string`, and mirrors + the behavior of :class:`ParameterizingString`, except that instead of + a capability name, receives a format string, and callable to filter the + given positional ``*args`` of :meth:`ParameterizingProxyString.__call__` + into a terminal sequence. + + For example: + + >>> from blessed import Terminal + >>> term = Terminal('screen') + >>> hpa = ParameterizingString(term.hpa, term.normal, 'hpa') + >>> hpa(9) + u'' + >>> fmt = u'\x1b[{0}G' + >>> fmt_arg = lambda *arg: (arg[0] + 1,) + >>> hpa = ParameterizingProxyString((fmt, fmt_arg), term.normal, 'hpa') + >>> hpa(9) + u'\x1b[10G' """ def __new__(cls, *args): - """P.__new__(cls, (fmt, callable), [normal, [name]]) + """ + Class constructor accepting 4 positional arguments. :arg fmt: format string suitable for displaying terminal sequences. :arg callable: receives __call__ arguments for formatting fmt. - :arg normal: terminating sequence for this capability. - :arg name: name of this terminal capability. + :arg normal: terminating sequence for this capability (optional). + :arg name: name of this terminal capability (optional). """ assert len(args) and len(args) < 4, args - assert type(args[0]) is tuple, args[0] + assert isinstance(args[0], tuple), args[0] assert callable(args[0][1]), args[0][1] - new = text_type.__new__(cls, args[0][0]) + new = six.text_type.__new__(cls, args[0][0]) new._fmt_args = args[0][1] new._normal = len(args) > 1 and args[1] or u'' new._name = len(args) > 2 and args[2] or u'' return new def __call__(self, *args): - """P(*args) -> FormattingString() + """ + Returning :class:`FormattingString` instance for given parameters. + + Arguments are determined by the capability. For example, ``hpa`` + (move_x) receives only a single integer, whereas ``cup`` (move) + receives two integers. See documentation in terminfo(5) for the + given capability. - Return evaluated terminal capability format, (self), using callable - ``self._fmt_args`` receiving arguments ``*args``, followed by the - terminating sequence (self.normal) into a FormattingString capable - of being called. + :rtype: FormattingString """ return FormattingString(self.format(*self._fmt_args(*args)), self._normal) def get_proxy_string(term, attr): - """ Proxy and return callable StringClass for proxied attributes. - - We know that some kinds of terminal kinds support sequences that the - terminfo database always report -- such as the 'move_x' attribute for - terminal type 'screen' and 'ansi', or 'hide_cursor' for 'ansi'. - - Returns instance of ParameterizingProxyString or NullCallableString. + """ + Proxy and return callable string for proxied attributes. + + :param Terminal term: :class:`~.Terminal` instance. + :param str attr: terminal capability name that may be proxied. + :rtype: None or :class:`ParameterizingProxyString`. + :returns: :class:`ParameterizingProxyString` for some attributes + of some terminal types that support it, where the terminfo(5) + database would otherwise come up empty, such as ``move_x`` + attribute for ``term.kind`` of ``screen``. Otherwise, None. """ # normalize 'screen-256color', or 'ansi.sys' to its basic names term_kind = next(iter(_kind for _kind in ('screen', 'ansi',) @@ -161,55 +198,68 @@ def get_proxy_string(term, attr): }.get(term_kind, {}).get(attr, None) -class FormattingString(text_type): - """A Unicode string which can be called using ``text``, - returning a new string, ``attr`` + ``text`` + ``normal``:: +class FormattingString(six.text_type): - >> style = FormattingString(term.bright_blue, term.normal) - >> style('Big Blue') - u'\x1b[94mBig Blue\x1b(B\x1b[m' + r""" + A Unicode string which doubles as a callable. + + This is used for terminal attributes, so that it may be used both + directly, or as a callable. When used directly, it simply emits + the given terminal sequence. When used as a callable, it wraps the + given (string) argument with the 2nd argument used by the class + constructor. + + >>> style = FormattingString(term.bright_blue, term.normal) + >>> print(repr(style)) + u'\x1b[94m' + >>> style('Big Blue') + u'\x1b[94mBig Blue\x1b(B\x1b[m' """ def __new__(cls, *args): - """P.__new__(cls, sequence, [normal]) + """ + Class constructor accepting 2 positional arguments. + :arg sequence: terminal attribute sequence. - :arg normal: terminating sequence for this attribute. + :arg normal: terminating sequence for this attribute (optional). """ assert 1 <= len(args) <= 2, args - new = text_type.__new__(cls, args[0]) + new = six.text_type.__new__(cls, args[0]) new._normal = len(args) > 1 and args[1] or u'' return new def __call__(self, text): - """P(text) -> unicode - - Return string ``text``, joined by specified video attribute, - (self), and followed by reset attribute sequence (term.normal). - """ + """Return ``text`` joined by ``sequence`` and ``normal``.""" if len(self): return u''.join((self, text, self._normal)) return text -class NullCallableString(text_type): - """A dummy callable Unicode to stand in for ``FormattingString`` and - ``ParameterizingString`` for terminals that cannot perform styling. +class NullCallableString(six.text_type): + + """ + A dummy callable Unicode alternative to :class:`FormattingString`. + + This is used for colors on terminals that do not support colors, + it is just a basic form of unicode that may also act as a callable. """ + def __new__(cls): - new = text_type.__new__(cls, u'') + """Class constructor.""" + new = six.text_type.__new__(cls, u'') return new def __call__(self, *args): - """Return a Unicode or whatever you passed in as the first arg - (hopefully a string of some kind). + """ + Allow empty string to be callable, returning given string, if any. When called with an int as the first arg, return an empty Unicode. An - int is a good hint that I am a ``ParameterizingString``, as there are - only about half a dozen string-returning capabilities listed in + int is a good hint that I am a :class:`ParameterizingString`, as there + are only about half a dozen string-returning capabilities listed in terminfo(5) which accept non-int arguments, they are seldom used. When called with a non-int as the first arg (no no args at all), return - the first arg, acting in place of ``FormattingString`` without + the first arg, acting in place of :class:`FormattingString` without any attributes. """ if len(args) != 1 or isinstance(args[0], int): @@ -237,27 +287,37 @@ def __call__(self, *args): def split_compound(compound): - """Split a possibly compound format string into segments. + """ + Split compound formating string into segments. >>> split_compound('bold_underline_bright_blue_on_red') ['bold', 'underline', 'bright_blue', 'on_red'] + :param str compound: a string that may contain compounds, + separated by underline (``_``). + :rtype: list """ merged_segs = [] # These occur only as prefixes, so they can always be merged: mergeable_prefixes = ['on', 'bright', 'on_bright'] - for s in compound.split('_'): + for segment in compound.split('_'): if merged_segs and merged_segs[-1] in mergeable_prefixes: - merged_segs[-1] += '_' + s + merged_segs[-1] += '_' + segment else: - merged_segs.append(s) + merged_segs.append(segment) return merged_segs def resolve_capability(term, attr): - """Return a Unicode string for the terminal capability ``attr``, - or an empty string if not found, or if terminal is without styling - capabilities. + """ + Resolve a raw terminal capability using :func:`tigetstr`. + + :param Terminal term: :class:`~.Terminal` instance. + :param str attr: terminal capability name. + :returns: string of the given terminal capability named by ``attr``, + which may be empty (u'') if not found or not supported by the + given :attr:`~.Terminal.kind`. + :rtype: str """ # Decode sequences as latin1, as they are always 8-bit bytes, so when # b'\xff' is returned, this must be decoded to u'\xff'. @@ -268,18 +328,29 @@ def resolve_capability(term, attr): def resolve_color(term, color): - """resolve_color(T, color) -> FormattingString() - - Resolve a ``color`` name to callable capability, ``FormattingString`` - unless ``term.number_of_colors`` is 0, then ``NullCallableString``. - - Valid ``color`` capabilities names are any of the simple color - names, such as ``red``, or compounded, such as ``on_bright_green``. """ + Resolve a simple color name to a callable capability. + + This function supports :func:`resolve_attribute`. + + :param Terminal term: :class:`~.Terminal` instance. + :param str color: any string found in set :const:`COLORS`. + :returns: a string class instance which emits the terminal sequence + for the given color, and may be used as a callable to wrap the + given string with such sequence. + :returns: :class:`NullCallableString` when + :attr:`~.Terminal.number_of_colors` is 0, + otherwise :class:`FormattingString`. + :rtype: :class:`NullCallableString` or :class:`FormattingString` + """ + if term.number_of_colors == 0: + return NullCallableString() + # NOTE(erikrose): Does curses automatically exchange red and blue and cyan # and yellow when a terminal supports setf/setb rather than setaf/setab? # I'll be blasted if I can find any documentation. The following - # assumes it does. + # assumes it does: to terminfo(5) describes color(1) as COLOR_RED when + # using setaf, but COLOR_BLUE when using setf. color_cap = (term._background_color if 'on_' in color else term._foreground_color) @@ -287,8 +358,6 @@ def resolve_color(term, color): # bright colors at 8-15: offset = 8 if 'bright_' in color else 0 base_color = color.rsplit('_', 1)[-1] - if term.number_of_colors == 0: - return NullCallableString() attr = 'COLOR_%s' % (base_color.upper(),) fmt_attr = color_cap(getattr(curses, attr) + offset) @@ -296,11 +365,21 @@ def resolve_color(term, color): def resolve_attribute(term, attr): - """Resolve a sugary or plain capability name, color, or compound - formatting name into a *callable* unicode string capability, - ``ParameterizingString`` or ``FormattingString``. """ - # A simple color, such as `red' or `blue'. + Resolve a terminal attribute name into a capability class. + + :param Terminal term: :class:`~.Terminal` instance. + :param str attr: Sugary, ordinary, or compound formatted terminal + capability, such as "red_on_white", "normal", "red", or + "bold_on_black", respectively. + :returns: a string class instance which emits the terminal sequence + for the given terminal capability, or may be used as a callable to + wrap the given string with such sequence. + :returns: :class:`NullCallableString` when + :attr:`~.Terminal.number_of_colors` is 0, + otherwise :class:`FormattingString`. + :rtype: :class:`NullCallableString` or :class:`FormattingString` + """ if attr in COLORS: return resolve_color(term, attr) diff --git a/blessed/keyboard.py b/blessed/keyboard.py index 28c26b76..79352bb7 100644 --- a/blessed/keyboard.py +++ b/blessed/keyboard.py @@ -1,105 +1,99 @@ -"This sub-module provides 'keyboard awareness'." - -__author__ = 'Jeff Quast ' -__license__ = 'MIT' - -__all__ = ('Keystroke', 'get_keyboard_codes', 'get_keyboard_sequences',) +"""This sub-module provides 'keyboard awareness'.""" +# std imports import curses.has_key -import collections import curses -import sys - -if hasattr(collections, 'OrderedDict'): - OrderedDict = collections.OrderedDict -else: - # python 2.6 requires 3rd party library - import ordereddict - OrderedDict = ordereddict.OrderedDict - -get_curses_keycodes = lambda: dict( - ((keyname, getattr(curses, keyname)) - for keyname in dir(curses) - if keyname.startswith('KEY_')) -) -# override a few curses constants with easier mnemonics, -# there may only be a 1:1 mapping, so for those who desire -# to use 'KEY_DC' from, perhaps, ported code, recommend -# that they simply compare with curses.KEY_DC. -CURSES_KEYCODE_OVERRIDE_MIXIN = ( - ('KEY_DELETE', curses.KEY_DC), - ('KEY_INSERT', curses.KEY_IC), - ('KEY_PGUP', curses.KEY_PPAGE), - ('KEY_PGDOWN', curses.KEY_NPAGE), - ('KEY_ESCAPE', curses.KEY_EXIT), - ('KEY_SUP', curses.KEY_SR), - ('KEY_SDOWN', curses.KEY_SF), - ('KEY_UP_LEFT', curses.KEY_A1), - ('KEY_UP_RIGHT', curses.KEY_A3), - ('KEY_CENTER', curses.KEY_B2), - ('KEY_BEGIN', curses.KEY_BEG), -) +# 3rd party +import six + +try: + from collections import OrderedDict +except ImportError: + # python 2.6 requires 3rd party library (backport) + # + # pylint: disable=import-error + # Unable to import 'ordereddict' + from ordereddict import OrderedDict + + +class Keystroke(six.text_type): -# Inject KEY_{names} that we think would be useful, there are no curses -# definitions for the keypad keys. We need keys that generate multibyte -# sequences, though it is useful to have some aliases for basic control -# characters such as TAB. -_lastval = max(get_curses_keycodes().values()) -for key in ('TAB', 'KP_MULTIPLY', 'KP_ADD', 'KP_SEPARATOR', 'KP_SUBTRACT', - 'KP_DECIMAL', 'KP_DIVIDE', 'KP_EQUAL', 'KP_0', 'KP_1', 'KP_2', - 'KP_3', 'KP_4', 'KP_5', 'KP_6', 'KP_7', 'KP_8', 'KP_9'): - _lastval += 1 - setattr(curses, 'KEY_{0}'.format(key), _lastval) - -if sys.version_info[0] == 3: - text_type = str - unichr = chr -else: - text_type = unicode # noqa - - -class Keystroke(text_type): - """A unicode-derived class for describing keyboard input returned by - the ``inkey()`` method of ``Terminal``, which may, at times, be a - multibyte sequence, providing properties ``is_sequence`` as ``True`` - when the string is a known sequence, and ``code``, which returns an - integer value that may be compared against the terminal class attributes - such as ``KEY_LEFT``. """ + A unicode-derived class for describing a single keystroke. + + A class instance describes a single keystroke received on input, + which may contain multiple characters as a multibyte sequence, + which is indicated by properties :attr:`is_sequence` returning + ``True``. + + When the string is a known sequence, :attr:`code` matches terminal + class attributes for comparison, such as ``term.KEY_LEFT``. + + The string-name of the sequence, such as ``u'KEY_LEFT'`` is accessed + by property :attr:`name`, and is used by the :meth:`__repr__` method + to display a human-readable form of the Keystroke this class + instance represents. It may otherwise by joined, split, or evaluated + just as as any other unicode string. + """ + def __new__(cls, ucs='', code=None, name=None): - new = text_type.__new__(cls, ucs) + """Class constructor.""" + new = six.text_type.__new__(cls, ucs) new._name = name new._code = code return new @property def is_sequence(self): - "Whether the value represents a multibyte sequence (bool)." + """Whether the value represents a multibyte sequence (bool).""" return self._code is not None def __repr__(self): - return self._name is None and text_type.__repr__(self) or self._name - __repr__.__doc__ = text_type.__doc__ + """Docstring overwritten.""" + return (self._name is None and + six.text_type.__repr__(self) or + self._name) + __repr__.__doc__ = six.text_type.__doc__ @property def name(self): - "String-name of key sequence, such as ``'KEY_LEFT'`` (str)." + """String-name of key sequence, such as ``u'KEY_LEFT'`` (str).""" return self._name @property def code(self): - "Integer keycode value of multibyte sequence (int)." + """Integer keycode value of multibyte sequence (int).""" return self._code +def get_curses_keycodes(): + """ + Return mapping of curses key-names paired by their keycode integer value. + + :rtype: dict + + Returns dictionary of (name, code) pairs for curses keyboard constant + values and their mnemonic name. Such as code ``260``, with the value of + its key-name identity, ``u'KEY_LEFT'``. + """ + _keynames = [attr for attr in dir(curses) + if attr.startswith('KEY_')] + return dict( + [(keyname, getattr(curses, keyname)) + for keyname in _keynames]) + + def get_keyboard_codes(): - """get_keyboard_codes() -> dict + """ + Return mapping of keycode integer values paired by their curses key-name. + + :rtype: dict Returns dictionary of (code, name) pairs for curses keyboard constant values and their mnemonic name. Such as key ``260``, with the value of - its identity, ``KEY_LEFT``. These are derived from the attributes by the - same of the curses module, with the following exceptions: + its identity, ``u'KEY_LEFT'``. These are derived from the attributes by + the same of the curses module, with the following exceptions: * ``KEY_DELETE`` in place of ``KEY_DC`` * ``KEY_INSERT`` in place of ``KEY_IC`` @@ -108,6 +102,12 @@ def get_keyboard_codes(): * ``KEY_ESCAPE`` in place of ``KEY_EXIT`` * ``KEY_SUP`` in place of ``KEY_SR`` * ``KEY_SDOWN`` in place of ``KEY_SF`` + + This function is the inverse of :func:`get_curses_keycodes`. With the + given override "mixins" listed above, the keycode for the delete key will + map to our imaginary ``KEY_DELETE`` mnemonic, effectively erasing the + phrase ``KEY_DC`` from our code vocabulary for anyone that wishes to use + the return value to determine the key-name by keycode. """ keycodes = OrderedDict(get_curses_keycodes()) keycodes.update(CURSES_KEYCODE_OVERRIDE_MIXIN) @@ -118,14 +118,20 @@ def get_keyboard_codes(): def _alternative_left_right(term): - """_alternative_left_right(T) -> dict + r""" + Determine and return mapping of left and right arrow keys sequences. - Return dict of sequences ``term._cuf1``, and ``term._cub1``, - valued as ``KEY_RIGHT``, ``KEY_LEFT`` when appropriate if available. + :param blessed.Terminal term: :class:`~.Terminal` instance. + :rtype: dict - some terminals report a different value for *kcuf1* than *cuf1*, but - actually send the value of *cuf1* for right arrow key (which is - non-destructive space). + This function supports :func:`get_terminal_sequences` to discover + the preferred input sequence for the left and right application keys. + + Return dict of sequences ``term._cuf1``, and ``term._cub1``, + valued as ``KEY_RIGHT``, ``KEY_LEFT`` (when appropriate). It is + necessary to check the value of these sequences to ensure we do not + use ``u' '`` and ``u'\b'`` for ``KEY_RIGHT`` and ``KEY_LEFT``, + preferring their true application key sequence, instead. """ keymap = dict() if term._cuf1 and term._cuf1 != u' ': @@ -136,13 +142,23 @@ def _alternative_left_right(term): def get_keyboard_sequences(term): - """get_keyboard_sequences(T) -> (OrderedDict) + r""" + Return mapping of keyboard sequences paired by keycodes. + + :param blessed.Terminal term: :class:`~.Terminal` instance. + :returns: mapping of keyboard unicode sequences paired by keycodes + as integer. This is used as the argument ``mapper`` to + the supporting function :func:`resolve_sequence`. + :rtype: OrderedDict Initialize and return a keyboard map and sequence lookup table, - (sequence, constant) from blessed Terminal instance ``term``, - where ``sequence`` is a multibyte input sequence, such as u'\x1b[D', - and ``constant`` is a constant, such as term.KEY_LEFT. The return - value is an OrderedDict instance, with their keys sorted longest-first. + (sequence, keycode) from :class:`~.Terminal` instance ``term``, + where ``sequence`` is a multibyte input sequence of unicode + characters, such as ``u'\x1b[D'``, and ``keycode`` is an integer + value, matching curses constant such as term.KEY_LEFT. + + The return value is an OrderedDict instance, with their keys + sorted longest-first. """ # A small gem from curses.has_key that makes this all possible, # _capability_names: a lookup table of terminal capability names for @@ -176,44 +192,84 @@ def get_keyboard_sequences(term): def resolve_sequence(text, mapper, codes): - """resolve_sequence(text, mapper, codes) -> Keystroke() - - Returns first matching Keystroke() instance for sequences found in - ``mapper`` beginning with input ``text``, where ``mapper`` is an - OrderedDict of unicode multibyte sequences, such as u'\x1b[D' paired by - their integer value (260), and ``codes`` is a dict of integer values (260) - paired by their mnemonic name, 'KEY_LEFT'. + r""" + Return :class:`Keystroke` instance for given sequence ``text``. + + The given ``text`` may extend beyond a matching sequence, such as + ``u\x1b[Dxxx`` returns a :class:`Keystroke` instance of attribute + :attr:`Keystroke.sequence` valued only ``u\x1b[D``. It is up to + determine that ``xxx`` remains unresolved. + + :param text: string of characters received from terminal input stream. + :param OrderedDict mapper: an OrderedDict of unicode multibyte sequences, + such as u'\x1b[D' paired by their integer value (260) + :param dict codes: a :type:`dict` of integer values (such as 260) paired + by their mnemonic name, such as ``'KEY_LEFT'``. + :rtype: Keystroke """ for sequence, code in mapper.items(): if text.startswith(sequence): return Keystroke(ucs=sequence, code=code, name=codes[code]) return Keystroke(ucs=text and text[0] or u'') -"""In a perfect world, terminal emulators would always send exactly what -the terminfo(5) capability database plans for them, accordingly by the -value of the ``TERM`` name they declare. -But this isn't a perfect world. Many vt220-derived terminals, such as -those declaring 'xterm', will continue to send vt220 codes instead of -their native-declared codes, for backwards-compatibility. +def _inject_curses_keynames(): + r""" + Inject KEY_NAMES that we think would be useful into the curses module. + + This function compliments the global constant + :obj:`DEFAULT_SEQUENCE_MIXIN`. It is important to note that this + function has the side-effect of **injecting** new attributes to the + curses module, and is called from the global namespace at time of + import. -This goes for many: rxvt, putty, iTerm. + Though we may determine keynames and codes for keyboard input that + generate multibyte sequences, it is also especially useful to aliases + a few basic ASCII characters such as ``KEY_TAB`` instead of ``u'\t'`` for + uniformity. -These "mixins" are used for *all* terminals, regardless of their type. + Furthermore, many key-names for application keys enabled only by context + manager :meth:`~.Terminal.keypad` are surprisingly absent. We inject them + here directly into the curses module. -Furthermore, curses does not provide sequences sent by the keypad, -at least, it does not provide a way to distinguish between keypad 0 -and numeric 0. -""" + It is not necessary to directly "monkeypatch" the curses module to + contain these constants, as they will also be accessible as attributes + of the Terminal class instance, they are provided only for convenience + when mixed in with other curses code. + """ + _lastval = max(get_curses_keycodes().values()) + for key in ('TAB', 'KP_MULTIPLY', 'KP_ADD', 'KP_SEPARATOR', 'KP_SUBTRACT', + 'KP_DECIMAL', 'KP_DIVIDE', 'KP_EQUAL', 'KP_0', 'KP_1', 'KP_2', + 'KP_3', 'KP_4', 'KP_5', 'KP_6', 'KP_7', 'KP_8', 'KP_9'): + _lastval += 1 + setattr(curses, 'KEY_{0}'.format(key), _lastval) + +_inject_curses_keynames() + +#: In a perfect world, terminal emulators would always send exactly what +#: the terminfo(5) capability database plans for them, accordingly by the +#: value of the ``TERM`` name they declare. +#: +#: But this isn't a perfect world. Many vt220-derived terminals, such as +#: those declaring 'xterm', will continue to send vt220 codes instead of +#: their native-declared codes, for backwards-compatibility. +#: +#: This goes for many: rxvt, putty, iTerm. +#: +#: These "mixins" are used for *all* terminals, regardless of their type. +#: +#: Furthermore, curses does not provide sequences sent by the keypad, +#: at least, it does not provide a way to distinguish between keypad 0 +#: and numeric 0. DEFAULT_SEQUENCE_MIXIN = ( # these common control characters (and 127, ctrl+'?') mapped to # an application key definition. - (unichr(10), curses.KEY_ENTER), - (unichr(13), curses.KEY_ENTER), - (unichr(8), curses.KEY_BACKSPACE), - (unichr(9), curses.KEY_TAB), - (unichr(27), curses.KEY_EXIT), - (unichr(127), curses.KEY_DC), + (six.unichr(10), curses.KEY_ENTER), + (six.unichr(13), curses.KEY_ENTER), + (six.unichr(8), curses.KEY_BACKSPACE), + (six.unichr(9), curses.KEY_TAB), + (six.unichr(27), curses.KEY_EXIT), + (six.unichr(127), curses.KEY_DC), (u"\x1b[A", curses.KEY_UP), (u"\x1b[B", curses.KEY_DOWN), @@ -250,7 +306,6 @@ def resolve_sequence(text, mapper, codes): (u"\x1bOx", curses.KEY_KP_8), # 8 (u"\x1bOy", curses.KEY_KP_9), # 9 - # # keypad, numlock off (u"\x1b[1~", curses.KEY_FIND), # find (u"\x1b[2~", curses.KEY_IC), # insert (0) @@ -275,3 +330,22 @@ def resolve_sequence(text, mapper, codes): (u"\x1bOR", curses.KEY_F3), (u"\x1bOS", curses.KEY_F4), ) + +#: Override mixins for a few curses constants with easier +#: mnemonics: there may only be a 1:1 mapping when only a +#: keycode (int) is given, where these phrases are preferred. +CURSES_KEYCODE_OVERRIDE_MIXIN = ( + ('KEY_DELETE', curses.KEY_DC), + ('KEY_INSERT', curses.KEY_IC), + ('KEY_PGUP', curses.KEY_PPAGE), + ('KEY_PGDOWN', curses.KEY_NPAGE), + ('KEY_ESCAPE', curses.KEY_EXIT), + ('KEY_SUP', curses.KEY_SR), + ('KEY_SDOWN', curses.KEY_SF), + ('KEY_UP_LEFT', curses.KEY_A1), + ('KEY_UP_RIGHT', curses.KEY_A3), + ('KEY_CENTER', curses.KEY_B2), + ('KEY_BEGIN', curses.KEY_BEG), +) + +__all__ = ('Keystroke', 'get_keyboard_codes', 'get_keyboard_sequences',) diff --git a/blessed/sequences.py b/blessed/sequences.py index c5625861..3c8ab0c8 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -1,48 +1,65 @@ # encoding: utf-8 -" This sub-module provides 'sequence awareness' for blessed." +"""This module provides 'sequence awareness'.""" -__author__ = 'Jeff Quast ' -__license__ = 'MIT' - -__all__ = ('init_sequence_patterns', 'Sequence', 'SequenceTextWrapper',) - -# built-ins +# std imports import functools import textwrap import warnings import math -import sys import re # local -from ._binterms import binary_terminals as _BINTERM_UNSUPPORTED +from blessed._binterms import BINARY_TERMINALS, BINTERM_UNSUPPORTED_MSG -# 3rd-party -import wcwidth # https://github.com/jquast/wcwidth +# 3rd party +import wcwidth +import six + +__all__ = ('init_sequence_patterns', 'Sequence', 'SequenceTextWrapper',) -_BINTERM_UNSUPPORTED_MSG = ( - u"Terminal kind {0!r} contains binary-packed capabilities, blessed " - u"is likely to fail to measure the length of its sequences.") -if sys.version_info[0] == 3: - text_type = str -else: - text_type = unicode # noqa +def _sort_sequences(regex_seqlist): + """ + Sort, filter, and return ``regex_seqlist`` in ascending order of length. + :param list regex_seqlist: list of strings. + :rtype: list + :returns: given list filtered and sorted. -def _merge_sequences(inp): - """Merge a list of input sequence patterns for use in a regular expression. + Any items that are Falsey (such as ``None``, ``''``) are removed from + the return list. The longest expressions are returned first. + Merge a list of input sequence patterns for use in a regular expression. Order by lengthyness (full sequence set precedent over subset), and exclude any empty (u'') sequences. """ - return sorted(list(filter(None, inp)), key=len, reverse=True) + # The purpose of sorting longest-first, is that we should want to match + # a complete, longest-matching final sequence in preference of a + # shorted sequence that partially matches another. This does not + # typically occur for output sequences, though with so many + # programmatically generated regular expressions for so many terminal + # types, it is feasible. + # pylint: disable=bad-builtin + # Used builtin function 'filter' + return sorted(list(filter(None, regex_seqlist)), key=len, reverse=True) def _build_numeric_capability(term, cap, optional=False, base_num=99, nparams=1): r""" - Build regexp from capabilities having matching numeric - parameter contained within termcap value: n->(\d+). + Return regular expression for capabilities containing specified digits. + + This differs from function :func:`_build_any_numeric_capability` + in that, for the given ``base_num`` and ``nparams``, the value of + ``-1``, through ``+1`` inclusive is replaced + by regular expression pattern ``\d``. Any other digits found are + *not* replaced. + + :param blessed.Terminal term: :class:`~.Terminal` instance. + :param str cap: terminal capability name. + :param int num: the numeric to use for parameterized capability. + :param int nparams: the number of parameters to use for capability. + :rtype: str + :returns: regular expression for the given capability. """ _cap = getattr(term, cap) opt = '?' if optional else '' @@ -61,13 +78,22 @@ def _build_numeric_capability(term, cap, optional=False, def _build_any_numeric_capability(term, cap, num=99, nparams=1): r""" - Build regexp from capabilities having *any* digit parameters - (substitute matching \d with pattern \d and return). + Return regular expression for capabilities containing any numerics. + + :param blessed.Terminal term: :class:`~.Terminal` instance. + :param str cap: terminal capability name. + :param int num: the numeric to use for parameterized capability. + :param int nparams: the number of parameters to use for capability. + :rtype: str + :returns: regular expression for the given capability. + + Build regular expression from capabilities having *any* digit parameters: + substitute any matching ``\d`` with literal ``\d`` and return. """ _cap = getattr(term, cap) if _cap: cap_re = re.escape(_cap(*((num,) * nparams))) - cap_re = re.sub('(\d+)', r'(\d+)', cap_re) + cap_re = re.sub(r'(\d+)', r'(\d+)', cap_re) if r'(\d+)' in cap_re: return cap_re warnings.warn('Missing numerics in %r, %r' % (cap, cap_re)) @@ -75,8 +101,11 @@ def _build_any_numeric_capability(term, cap, num=99, nparams=1): def get_movement_sequence_patterns(term): - """ Build and return set of regexp for capabilities of ``term`` known - to cause movement. + """ + Get list of regular expressions for sequences that cause movement. + + :param blessed.Terminal term: :class:`~.Terminal` instance. + :rtype: list """ bnc = functools.partial(_build_numeric_capability, term) @@ -116,12 +145,17 @@ def get_movement_sequence_patterns(term): def get_wontmove_sequence_patterns(term): - """ Build and return set of regexp for capabilities of ``term`` known - not to cause any movement. + """ + Get list of regular expressions for sequences not causing movement. + + :param blessed.Terminal term: :class:`~.Terminal` instance. + :rtype: list """ bnc = functools.partial(_build_numeric_capability, term) bna = functools.partial(_build_any_numeric_capability, term) + # pylint: disable=bad-builtin + # Used builtin function 'map' return list([ # print_screen: Print contents of screen re.escape(term.mc0), @@ -236,62 +270,77 @@ def get_wontmove_sequence_patterns(term): def init_sequence_patterns(term): - """Given a Terminal instance, ``term``, this function processes + """ + Build database of regular expressions of terminal sequences. + + Given a Terminal instance, ``term``, this function processes and parses several known terminal capabilities, and builds and - returns a dictionary database of regular expressions, which may - be re-attached to the terminal by attributes of the same key-name: + returns a dictionary database of regular expressions, which is + re-attached to the terminal by attributes of the same key-name. + + :param blessed.Terminal term: :class:`~.Terminal` instance. + :rtype: dict + :returns: dictionary containing mappings of sequence "groups", + containing a compiled regular expression which it matches: + + - ``_re_will_move`` + + Any sequence matching this pattern will cause the terminal + cursor to move (such as *term.home*). + + - ``_re_wont_move`` + + Any sequence matching this pattern will not cause the cursor + to move (such as *term.bold*). + + - ``_re_cuf`` + + Regular expression that matches term.cuf(N) (move N characters + forward), or None if temrinal is without cuf sequence. - ``_re_will_move`` - any sequence matching this pattern will cause the terminal - cursor to move (such as *term.home*). + - ``_cuf1`` - ``_re_wont_move`` - any sequence matching this pattern will not cause the cursor - to move (such as *term.bold*). + *term.cuf1* sequence (cursor forward 1 character) as a static value. - ``_re_cuf`` - regular expression that matches term.cuf(N) (move N characters forward), - or None if temrinal is without cuf sequence. + - ``_re_cub`` - ``_cuf1`` - *term.cuf1* sequence (cursor forward 1 character) as a static value. + Regular expression that matches term.cub(N) (move N characters + backward), or None if terminal is without cub sequence. - ``_re_cub`` - regular expression that matches term.cub(N) (move N characters backward), - or None if terminal is without cub sequence. + - ``_cub1`` - ``_cub1`` - *term.cuf1* sequence (cursor backward 1 character) as a static value. + *term.cuf1* sequence (cursor backward 1 character) as a static value. These attributes make it possible to perform introspection on strings containing sequences generated by this terminal, to determine the printable length of a string. """ - if term.kind in _BINTERM_UNSUPPORTED: - warnings.warn(_BINTERM_UNSUPPORTED_MSG.format(term.kind)) + if term.kind in BINARY_TERMINALS: + warnings.warn(BINTERM_UNSUPPORTED_MSG.format(term.kind)) # Build will_move, a list of terminal capabilities that have # indeterminate effects on the terminal cursor position. _will_move = set() if term.does_styling: - _will_move = _merge_sequences(get_movement_sequence_patterns(term)) + _will_move = _sort_sequences(get_movement_sequence_patterns(term)) # Build wont_move, a list of terminal capabilities that mainly affect # video attributes, for use with measure_length(). _wont_move = set() if term.does_styling: - _wont_move = _merge_sequences(get_wontmove_sequence_patterns(term)) + _wont_move = _sort_sequences(get_wontmove_sequence_patterns(term)) _wont_move += [ # some last-ditch match efforts; well, xterm and aixterm is going # to throw \x1b(B and other oddities all around, so, when given # input such as ansi art (see test using wall.ans), and well, - # theres no reason a vt220 terminal shouldn't be able to recognize - # blue_on_red, even if it didn't cause it to be generated. these - # are final "ok, i will match this, anyway" - re.escape(u'\x1b') + r'\[(\d+)m', - re.escape(u'\x1b') + r'\[(\d+)\;(\d+)m', - re.escape(u'\x1b') + r'\[(\d+)\;(\d+)\;(\d+)m', + # there is no reason a vt220 terminal shouldn't be able to + # recognize blue_on_red, even if it didn't cause it to be + # generated. These are final "ok, i will match this, anyway" for + # basic SGR sequences. re.escape(u'\x1b') + r'\[(\d+)\;(\d+)\;(\d+)\;(\d+)m', + re.escape(u'\x1b') + r'\[(\d+)\;(\d+)\;(\d+)m', + re.escape(u'\x1b') + r'\[(\d+)\;(\d+)m', + re.escape(u'\x1b') + r'\[(\d+)m', re.escape(u'\x1b(B'), ] @@ -325,16 +374,27 @@ def init_sequence_patterns(term): class SequenceTextWrapper(textwrap.TextWrapper): + + """This docstring overridden.""" + def __init__(self, width, term, **kwargs): + """ + Class initializer. + + This class supports the :meth:`~.Terminal.wrap` method. + """ self.term = term textwrap.TextWrapper.__init__(self, width, **kwargs) def _wrap_chunks(self, chunks): """ - escape-sequence aware variant of _wrap_chunks. Though - movement sequences, such as term.left() are certainly not - honored, sequences such as term.bold() are, and are not - broken mid-sequence. + Sequence-aware variant of :meth:`textwrap.TextWrapper._wrap_chunks`. + + This simply ensures that word boundaries are not broken mid-sequence, + as standard python textwrap would incorrectly determine the length + of a string containing sequences, and may also break consider sequences + part of a "word" that may be broken by hyphen (``-``), where this + implementation corrects both. """ lines = [] if self.width <= 0 or not isinstance(self.width, int): @@ -372,12 +432,13 @@ def _wrap_chunks(self, chunks): return lines def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width): - """_handle_long_word(chunks : [string], - cur_line : [string], - cur_len : int, width : int) + """Sequence-aware :meth:`textwrap.TextWrapper._handle_long_word`. - Handle a chunk of text (most likely a word, not whitespace) that - is too long to fit in any line. + This simply ensures that word boundaries are not broken mid-sequence, + as standard python textwrap would incorrectly determine the length + of a string containing sequences, and may also break consider sequences + part of a "word" that may be broken by hyphen (``-``), where this + implementation corrects both. """ # Figure out when indent is larger than the specified width, and make # sure at least one character is stripped off on every pass @@ -426,159 +487,193 @@ def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width): SequenceTextWrapper.__doc__ = textwrap.TextWrapper.__doc__ -class Sequence(text_type): +class Sequence(six.text_type): + """ + A "sequence-aware" version of the base :class:`str` class. + This unicode-derived class understands the effect of escape sequences - of printable length, allowing a properly implemented .rjust(), .ljust(), - .center(), and .len() + of printable length, allowing a properly implemented :meth:`rjust`, + :meth:`ljust`, :meth:`center`, and :meth:`length`. """ def __new__(cls, sequence_text, term): - """Sequence(sequence_text, term) -> unicode object + """ + Class constructor. - :arg sequence_text: A string containing sequences. - :arg term: Terminal instance this string was created with. + :param sequence_text: A string that may contain sequences. + :param blessed.Terminal term: :class:`~.Terminal` instance. """ - new = text_type.__new__(cls, sequence_text) + new = six.text_type.__new__(cls, sequence_text) new._term = term return new def ljust(self, width, fillchar=u' '): - """S.ljust(width, fillchar) -> unicode + """ + Return string containing sequences, left-adjusted. - Returns string derived from unicode string ``S``, left-adjusted - by trailing whitespace padding ``fillchar``.""" - rightside = fillchar * int((max(0.0, float(width - self.length()))) - / float(len(fillchar))) + :param int width: Total width given to right-adjust ``text``. If + unspecified, the width of the attached terminal is used (default). + :param str fillchar: String for padding right-of ``text``. + :returns: String of ``text``, right-aligned by ``width``. + :rtype: str + """ + rightside = fillchar * int( + (max(0.0, float(width - self.length()))) / float(len(fillchar))) return u''.join((self, rightside)) def rjust(self, width, fillchar=u' '): - """S.rjust(width, fillchar=u'') -> unicode + """ + Return string containing sequences, right-adjusted. - Returns string derived from unicode string ``S``, right-adjusted - by leading whitespace padding ``fillchar``.""" - leftside = fillchar * int((max(0.0, float(width - self.length()))) - / float(len(fillchar))) + :param int width: Total width given to right-adjust ``text``. If + unspecified, the width of the attached terminal is used (default). + :param str fillchar: String for padding left-of ``text``. + :returns: String of ``text``, right-aligned by ``width``. + :rtype: str + """ + leftside = fillchar * int( + (max(0.0, float(width - self.length()))) / float(len(fillchar))) return u''.join((leftside, self)) def center(self, width, fillchar=u' '): - """S.center(width, fillchar=u'') -> unicode + """ + Return string containing sequences, centered. - Returns string derived from unicode string ``S``, centered - and surrounded with whitespace padding ``fillchar``.""" + :param int width: Total width given to center ``text``. If + unspecified, the width of the attached terminal is used (default). + :param str fillchar: String for padding left and right-of ``text``. + :returns: String of ``text``, centered by ``width``. + :rtype: str + """ split = max(0.0, float(width) - self.length()) / 2 - leftside = fillchar * int((max(0.0, math.floor(split))) - / float(len(fillchar))) - rightside = fillchar * int((max(0.0, math.ceil(split))) - / float(len(fillchar))) + leftside = fillchar * int( + (max(0.0, math.floor(split))) / float(len(fillchar))) + rightside = fillchar * int( + (max(0.0, math.ceil(split))) / float(len(fillchar))) return u''.join((leftside, self, rightside)) def length(self): - """S.length() -> int - - Returns printable length of unicode string ``S`` that may contain - terminal sequences. - - Although accounted for, strings containing sequences such as - ``term.clear`` will not give accurate returns, it is not - considered lengthy (a length of 0). Combining characters, - are also not considered lengthy. + r""" + Return the printable length of string containing sequences. Strings containing ``term.left`` or ``\b`` will cause "overstrike", but a length less than 0 is not ever returned. So ``_\b+`` is a - length of 1 (``+``), but ``\b`` is simply a length of 0. + length of 1 (displays as ``+``), but ``\b`` alone is simply a + length of 0. Some characters may consume more than one cell, mainly those CJK Unified Ideographs (Chinese, Japanese, Korean) defined by Unicode as half or full-width characters. - - For example: - >>> from blessed import Terminal - >>> from blessed.sequences import Sequence - >>> term = Terminal() - >>> Sequence(term.clear + term.red(u'コンニチハ')).length() - 5 """ # because combining characters may return -1, "clip" their length to 0. clip = functools.partial(max, 0) return sum(clip(wcwidth.wcwidth(w_char)) for w_char in self.strip_seqs()) - def strip(self, chars=None): - """S.strip([chars]) -> unicode + # we require ur"" for the docstring, but it is not supported by pep257 + # tool: https://github.com/GreenSteam/pep257/issues/116 + length.__doc__ += ( + u"""For example: - Return a copy of the string S with terminal sequences removed, and - leading and trailing whitespace removed. + >>> from blessed import Terminal + >>> from blessed.sequences import Sequence + >>> term = Terminal() + >>> Sequence(term.clear + term.red(u'コンニチハ'), term).length() + 10 - If chars is given and not None, remove characters in chars instead. + .. note:: Although accounted for, strings containing sequences such as + ``term.clear`` will not give accurate returns, it is not + considered lengthy (a length of 0). + """) + + def strip(self, chars=None): + """ + Return string of sequences, leading, and trailing whitespace removed. + + :param str chars: Remove characters in chars instead of whitespace. + :rtype: str """ return self.strip_seqs().strip(chars) def lstrip(self, chars=None): - """S.lstrip([chars]) -> unicode - - Return a copy of the string S with terminal sequences and leading - whitespace removed. + """ + Return string of all sequences and leading whitespace removed. - If chars is given and not None, remove characters in chars instead. + :param str chars: Remove characters in chars instead of whitespace. + :rtype: str """ return self.strip_seqs().lstrip(chars) def rstrip(self, chars=None): - """S.rstrip([chars]) -> unicode - - Return a copy of the string S with terminal sequences and trailing - whitespace removed. + """ + Return string of all sequences and trailing whitespace removed. - If chars is given and not None, remove characters in chars instead. + :param str chars: Remove characters in chars instead of whitespace. + :rtype: str """ return self.strip_seqs().rstrip(chars) def strip_seqs(self): - """S.strip_seqs() -> unicode - - Return a string without sequences for a string that contains - sequences for the Terminal with which they were created. - - Where sequence ``move_right(n)`` is detected, it is replaced with - ``n * u' '``, and where ``move_left()`` or ``\\b`` is detected, - those last-most characters are destroyed. + r""" + Return string of all sequences removed. - All other sequences are simply removed. An example, >>> from blessed import Terminal >>> from blessed.sequences import Sequence >>> term = Terminal() - >>> Sequence(term.clear + term.red(u'test')).strip_seqs() - u'test' + >>> Sequence(term.cuf(5) + term.red(u'test'), term).strip_seqs() + u' test' + + :rtype: str + + This method is used to determine the printable width of a string, + and is the first pass of :meth:`length`. + + .. note:: Non-destructive sequences that adjust horizontal distance + (such as ``\b`` or ``term.cuf(5)``) are replaced by destructive + space or erasing. """ # nxt: points to first character beyond current escape sequence. # width: currently estimated display length. - input = self.padd() + inp = self.padd() outp = u'' nxt = 0 - for idx in range(0, len(input)): + for idx in range(0, len(inp)): if idx == nxt: # at sequence, point beyond it, - nxt = idx + measure_length(input[idx:], self._term) + nxt = idx + measure_length(inp[idx:], self._term) if nxt <= idx: # append non-sequence to outp, - outp += input[idx] + outp += inp[idx] # point beyond next sequence, if any, # otherwise point to next character - nxt = idx + measure_length(input[idx:], self._term) + 1 + nxt = idx + measure_length(inp[idx:], self._term) + 1 return outp def padd(self): - """S.padd() -> unicode - Make non-destructive space or backspace into destructive ones. + r""" + Transform non-destructive space or backspace into destructive ones. + + >>> from blessed import Terminal + >>> from blessed.sequences import Sequence + >>> term = Terminal() + >>> seq = term.cuf(10) + '-->' + '\b\b' + >>> padded = Sequence(seq, Terminal()).padd() + >>> print(seq, padded) + (u'\x1b[10C-->\x08\x08', u' -') + + :rtype: str - Where sequence ``move_right(n)`` is detected, it is replaced with - ``n * u' '``. Where sequence ``move_left(n)`` or ``\\b`` is + This method is used to determine the printable width of a string, + and is the first pass of :meth:`strip_seqs`. + + Where sequence ``term.cuf(n)`` is detected, it is replaced with + ``n * u' '``, and where sequence ``term.cub1(n)`` or ``\\b`` is detected, those last-most characters are destroyed. """ outp = u'' nxt = 0 - for idx in range(0, text_type.__len__(self)): + for idx in range(0, six.text_type.__len__(self)): width = horizontal_distance(self[idx:], self._term) if width != 0: nxt = idx + measure_length(self[idx:], self._term) @@ -593,21 +688,31 @@ def padd(self): def measure_length(ucs, term): - """measure_length(S, term) -> int + r""" + Return non-zero for string ``ucs`` that begins with a terminal sequence. - Returns non-zero for string ``S`` that begins with a terminal sequence, - that is: the width of the first unprintable sequence found in S. For use - as a *next* pointer to skip past sequences. If string ``S`` is not a - sequence, 0 is returned. + :param str ucs: String that may begin with a terminal sequence. + :param blessed.Terminal term: :class:`~.Terminal` instance. + :rtype: int + :returns: length of the sequence beginning at ``ucs``, if any. + Otherwise 0 if ``ucs`` does not begin with a terminal + sequence. + + Returns non-zero for string ``ucs`` that begins with a terminal + sequence, of the length of characters in ``ucs`` until the *first* + matching sequence ends. + + This is used as a *next* pointer to iterate over sequences. If the string + ``ucs`` does not begin with a sequence, ``0`` is returned. A sequence may be a typical terminal sequence beginning with Escape (``\x1b``), especially a Control Sequence Initiator (``CSI``, ``\x1b[``, ...), or those of ``\a``, ``\b``, ``\r``, ``\n``, ``\xe0`` (shift in), - ``\x0f`` (shift out). They do not necessarily have to begin with CSI, they - need only match the capabilities of attributes ``_re_will_move`` and - ``_re_wont_move`` of terminal ``term``. + and ``\x0f`` (shift out). They do not necessarily have to begin with CSI, + they need only match the capabilities of attributes ``_re_will_move`` and + ``_re_wont_move`` of :class:`~.Terminal` which are constructed at time + of class initialization. """ - # simple terminal control characters, ctrl_seqs = u'\a\b\r\n\x0e\x0f' @@ -623,7 +728,7 @@ def measure_length(ucs, term): ) if matching_seq: - start, end = matching_seq.span() + _, end = matching_seq.span() return end # none found, must be printable! @@ -631,20 +736,34 @@ def measure_length(ucs, term): def termcap_distance(ucs, cap, unit, term): - """termcap_distance(S, cap, unit, term) -> int + r""" + Return distance of capabilities ``cub``, ``cub1``, ``cuf``, and ``cuf1``. + + :param str ucs: Terminal sequence created using any of ``cub(n)``, + ``cub1``, ``cuf(n)``, or ``cuf1``. + :param str cap: ``cub`` or ``cuf`` only. + :param int unit: Unit multiplier, should always be ``1`` or ``-1``. + :param blessed.Terminal term: :class:`~.Terminal` instance. + :rtype: int + :returns: the printable distance determined by the given sequence. If + the given sequence does not match any of the ``cub`` or ``cuf`` - Match horizontal distance by simple ``cap`` capability name, ``cub1`` or - ``cuf1``, with string matching the sequences identified by Terminal - instance ``term`` and a distance of ``unit`` *1* or *-1*, for right and - left, respectively. + This supports the higher level function :func:`horizontal_distance`. + + Match horizontal distance by simple ``cap`` capability name, either + from termcap ``cub`` or ``cuf``, with string matching the sequences + identified by Terminal instance ``term`` and a distance of ``unit`` + *1* or *-1*, for right and left, respectively. Otherwise, by regular expression (using dynamic regular expressions built - using ``cub(n)`` and ``cuf(n)``. Failing that, any of the standard SGR - sequences (``\033[C``, ``\033[D``, ``\033[nC``, ``\033[nD``). + when :class:`~.Terminal` is first initialized) of ``cub(n)`` and + ``cuf(n)``. Failing that, any of the standard SGR sequences + (``\033[C``, ``\033[D``, ``\033[C``, ``\033[D``). Returns 0 if unmatched. """ - assert cap in ('cuf', 'cub') + assert cap in ('cuf', 'cub'), cap + assert unit in (1, -1), unit # match cub1(left), cuf1(right) one = getattr(term, '_%s1' % (cap,)) if one and ucs.startswith(one): @@ -660,22 +779,29 @@ def termcap_distance(ucs, cap, unit, term): def horizontal_distance(ucs, term): - """horizontal_distance(S, term) -> int + r""" + Determine the horizontal distance of single terminal sequence, ``ucs``. - Returns Integer ```` in SGR sequence of form ``[C`` - (T.move_right(n)), or ``-(n)`` in sequence of form ``[D`` - (T.move_left(n)). Returns -1 for backspace (0x08), Otherwise 0. + :param ucs: terminal sequence, which may be any of the following: - Tabstop (``\t``) cannot be correctly calculated, as the relative column - position cannot be determined: 8 is always (and, incorrectly) returned. - """ + - move_right (fe. ``[C``): returns value ``(n)``. + - move left (fe. ``[D``): returns value ``-(n)``. + - backspace (``\b``) returns value -1. + - tab (``\t``) returns value 8. + :param blessed.Terminal term: :class:`~.Terminal` instance. + :rtype: int + + .. note:: Tabstop (``\t``) cannot be correctly calculated, as the relative + column position cannot be determined: 8 is always (and, incorrectly) + returned. + """ if ucs.startswith('\b'): return -1 elif ucs.startswith('\t'): # As best as I can prove it, a tabstop is always 8 by default. - # Though, given that blessings is: + # Though, given that blessed is: # # 1. unaware of the output device's current cursor position, and # 2. unaware of the location the callee may chose to output any diff --git a/blessed/terminal.py b/blessed/terminal.py index 1e17d736..2c4fbce2 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -1,81 +1,73 @@ -"This primary module provides the Terminal class." -# standard modules +# encoding: utf-8 +"""This module contains :class:`Terminal`, the primary API entry point.""" +# pylint: disable=too-many-lines +# Too many lines in module (1027/1000) +import codecs import collections import contextlib -import functools -import warnings -import platform -import codecs import curses +import functools +import io import locale +import os +import platform import select import struct -import time import sys -import os +import time +import warnings try: import termios import fcntl import tty + HAS_TTY = True except ImportError: - tty_methods = ('setraw', 'cbreak', 'kbhit', 'height', 'width') - msg_nosupport = ( + _TTY_METHODS = ('setraw', 'cbreak', 'kbhit', 'height', 'width') + _MSG_NOSUPPORT = ( "One or more of the modules: 'termios', 'fcntl', and 'tty' " "are not found on your platform '{0}'. The following methods " "of Terminal are dummy/no-op unless a deriving class overrides " - "them: {1}".format(sys.platform.lower(), ', '.join(tty_methods))) - warnings.warn(msg_nosupport) + "them: {1}".format(sys.platform.lower(), ', '.join(_TTY_METHODS))) + warnings.warn(_MSG_NOSUPPORT) HAS_TTY = False -else: - HAS_TTY = True - -try: - from io import UnsupportedOperation as IOUnsupportedOperation -except ImportError: - class IOUnsupportedOperation(Exception): - """A dummy exception to take the place of Python 3's - ``io.UnsupportedOperation`` in Python 2.5""" try: - _ = InterruptedError - del _ + InterruptedError except NameError: # alias py2 exception to py3 InterruptedError = select.error # local imports -from .formatters import ( - ParameterizingString, - NullCallableString, - resolve_capability, - resolve_attribute, -) - -from .sequences import ( - init_sequence_patterns, - SequenceTextWrapper, - Sequence, -) - -from .keyboard import ( - get_keyboard_sequences, - get_keyboard_codes, - resolve_sequence, -) +from .formatters import (ParameterizingString, + NullCallableString, + resolve_capability, + resolve_attribute, + ) +from .sequences import (init_sequence_patterns, + SequenceTextWrapper, + Sequence, + ) + +from .keyboard import (get_keyboard_sequences, + get_keyboard_codes, + resolve_sequence, + ) -class Terminal(object): - """A wrapper for curses and related terminfo(5) terminal capabilities. - Instance attributes: +class Terminal(object): + """ + An abstraction for color, style, positioning, and input in the terminal. - ``stream`` - The stream the terminal outputs to. It's convenient to pass the stream - around with the terminal; it's almost always needed when the terminal - is and saves sticking lots of extra args on client functions in - practice. + This keeps the endless calls to ``tigetstr()`` and ``tparm()`` out of your + code, acts intelligently when somebody pipes your output to a non-terminal, + and abstracts over the complexity of unbuffered keyboard input. It uses the + terminfo database to remain portable across terminal types. """ + # pylint: disable=too-many-instance-attributes,too-many-public-methods + # Too many public methods (28/20) + # Too many instance attributes (12/7) #: Sugary names for commonly-used capabilities _sugar = dict( @@ -114,67 +106,69 @@ class Terminal(object): no_underline='rmul') def __init__(self, kind=None, stream=None, force_styling=False): - """Initialize the terminal. - - If ``stream`` is not a tty, I will default to returning an empty - Unicode string for all capability values, so things like piping your - output to a file won't strew escape sequences all over the place. The - ``ls`` command sets a precedent for this: it defaults to columnar - output when being sent to a tty and one-item-per-line when not. - - :arg kind: A terminal string as taken by ``setupterm()``. Defaults to - the value of the ``TERM`` environment variable. - :arg stream: A file-like object representing the terminal. Defaults to - the original value of stdout, like ``curses.initscr()`` does. - :arg force_styling: Whether to force the emission of capabilities, even - if we don't seem to be in a terminal. This comes in handy if users - are trying to pipe your output through something like ``less -r``, - which supports terminal codes just fine but doesn't appear itself - to be a terminal. Just expose a command-line option, and set - ``force_styling`` based on it. Terminal initialization sequences - will be sent to ``stream`` if it has a file descriptor and to - ``sys.__stdout__`` otherwise. (``setupterm()`` demands to send them - somewhere, and stdout is probably where the output is ultimately - headed. If not, stderr is probably bound to the same terminal.) - - If you want to force styling to not happen, pass - ``force_styling=None``. + """ + Initialize the terminal. + + :param str kind: A terminal string as taken by + :func:`curses.setupterm`. Defaults to the value of the ``TERM`` + environment variable. + + .. note:: Terminals withing a single process must share a common + ``kind``. See :obj:`_CUR_TERM`. + + :param file stream: A file-like object representing the Terminal + output. Defaults to the original value of :obj:`sys.__stdout__`, + like :func:`curses.initscr` does. + If ``stream`` is not a tty, empty Unicode strings are returned for + all capability values, so things like piping your program output to + a pipe or file does not emit terminal sequences. + + :param bool force_styling: Whether to force the emission of + capabilities even if :obj:`sys.__stdout__` does not seem to be + connected to a terminal. If you want to force styling to not + happen, use ``force_styling=None``. + + This comes in handy if users are trying to pipe your output through + something like ``less -r`` or build systems which support decoding + of terminal sequences. """ + # pylint: disable=global-statement,too-many-branches global _CUR_TERM - self.keyboard_fd = None + self._keyboard_fd = None - # default stream is stdout, keyboard only valid as stdin when - # output stream is stdout and output stream is a tty + # Default stream is stdout, keyboard valid as stdin only when + # output stream is stdout is a tty. if stream is None or stream == sys.__stdout__: stream = sys.__stdout__ - self.keyboard_fd = sys.__stdin__.fileno() + self._keyboard_fd = sys.__stdin__.fileno() try: - stream_fd = (stream.fileno() if hasattr(stream, 'fileno') - and callable(stream.fileno) else None) - except IOUnsupportedOperation: + stream_fd = (stream.fileno() if hasattr(stream, 'fileno') and + callable(stream.fileno) else None) + except io.UnsupportedOperation: stream_fd = None self._is_a_tty = stream_fd is not None and os.isatty(stream_fd) self._does_styling = ((self.is_a_tty or force_styling) and force_styling is not None) - # keyboard_fd only non-None if both stdin and stdout is a tty. - self.keyboard_fd = (self.keyboard_fd - if self.keyboard_fd is not None and - self.is_a_tty and os.isatty(self.keyboard_fd) - else None) + # _keyboard_fd only non-None if both stdin and stdout is a tty. + self._keyboard_fd = (self._keyboard_fd + if self._keyboard_fd is not None and + self.is_a_tty and os.isatty(self._keyboard_fd) + else None) self._normal = None # cache normal attr, preventing recursive lookups # The descriptor to direct terminal initialization sequences to. - # sys.__stdout__ seems to always have a descriptor of 1, even if output - # is redirected. - self._init_descriptor = (stream_fd is None and sys.__stdout__.fileno() - or stream_fd) + self._init_descriptor = (stream_fd is None and + sys.__stdout__.fileno() or + stream_fd) self._kind = kind or os.environ.get('TERM', 'unknown') if self.does_styling: + # Initialize curses (call setupterm). + # # Make things like tigetstr() work. Explicit args make setupterm() # work even when -s is passed to nosetests. Lean toward sending # init sequences to the stream if it has a file descriptor, and @@ -185,7 +179,8 @@ def __init__(self, kind=None, stream=None, force_styling=False): isinstance(self._kind, unicode)): # pypy/2.4.0_2/libexec/lib_pypy/_curses.py, line 1131 # TypeError: initializer for ctype 'char *' must be a str - curses.setupterm(self._kind.encode('ascii'), self._init_descriptor) + curses.setupterm(self._kind.encode('ascii'), + self._init_descriptor) else: curses.setupterm(self._kind, self._init_descriptor) except curses.error as err: @@ -199,55 +194,66 @@ def __init__(self, kind=None, stream=None, force_styling=False): else: warnings.warn( 'A terminal of kind "%s" has been requested; due to an' - ' internal python curses bug, terminal capabilities' + ' internal python curses bug, terminal capabilities' ' for a terminal of kind "%s" will continue to be' ' returned for the remainder of this process.' % ( self._kind, _CUR_TERM,)) + # Initialize keyboard data determined by capability. + # + # The following attributes are initialized: _keycodes, + # _keymap, _keyboard_buf, _encoding, and _keyboard_decoder. for re_name, re_val in init_sequence_patterns(self).items(): setattr(self, re_name, re_val) - # build database of int code <=> KEY_NAME + # Build database of int code <=> KEY_NAME. self._keycodes = get_keyboard_codes() - # store attributes as: self.KEY_NAME = code + # Store attributes as: self.KEY_NAME = code. for key_code, key_name in self._keycodes.items(): setattr(self, key_name, key_code) - # build database of sequence <=> KEY_NAME + # Build database of sequence <=> KEY_NAME. self._keymap = get_keyboard_sequences(self) self._keyboard_buf = collections.deque() - if self.keyboard_fd is not None: + if self._keyboard_fd is not None: locale.setlocale(locale.LC_ALL, '') self._encoding = locale.getpreferredencoding() or 'ascii' try: self._keyboard_decoder = codecs.getincrementaldecoder( self._encoding)() except LookupError as err: - warnings.warn('%s, fallback to ASCII for keyboard.' % (err,)) + warnings.warn('LookupError: {0}, fallback to ASCII for ' + 'keyboard.'.format(err)) self._encoding = 'ascii' self._keyboard_decoder = codecs.getincrementaldecoder( self._encoding)() - self.stream = stream + self._stream = stream def __getattr__(self, attr): - """Return a terminal capability as Unicode string. + r""" + Return a terminal capability as Unicode string. For example, ``term.bold`` is a unicode string that may be prepended to text to set the video attribute for bold, which should also be - terminated with the pairing ``term.normal``. + terminated with the pairing :attr:`normal`. This capability + returns a callable, so you can use ``term.bold("hi")`` which + results in the joining of ``(term.bold, "hi", term.normal)``. + + Compound formatters may also be used. For example:: + + >>> term.bold_blink_red_on_green("merry x-mas!") - This capability is also callable, so you can use ``term.bold("hi")`` - which results in the joining of (term.bold, "hi", term.normal). + For a parametrized capability such as ``move`` (or ``cup``), pass the + parameters as positional arguments:: - Compound formatters may also be used, for example: - ``term.bold_blink_red_on_green("merry x-mas!")``. + >>> term.move(line, column) - For a parametrized capability such as ``cup`` (cursor_address), pass - the parameters as arguments ``some_term.cup(line, column)``. See - manual page terminfo(5) for a complete list of capabilities. + See the manual page `terminfo(5) + `_ for a + complete list of capabilities and their arguments. """ if not self.does_styling: return NullCallableString() @@ -258,57 +264,101 @@ def __getattr__(self, attr): @property def kind(self): - """Name of this terminal type as string.""" + """ + Read-only property: Terminal kind determined on class initialization. + + :rtype: str + """ return self._kind @property def does_styling(self): - """Whether this instance will emit terminal sequences (bool).""" + """ + Read-only property: Whether this class instance may emit sequences. + + :rtype: bool + """ return self._does_styling @property def is_a_tty(self): - """Whether the ``stream`` associated with this instance is a terminal - (bool).""" + """ + Read-only property: Whether :attr:`~.stream` is a terminal. + + :rtype: bool + """ return self._is_a_tty @property def height(self): - """T.height -> int + """ + Read-only property: Height of the terminal (in number of lines). - The height of the terminal in characters. + :rtype: int """ return self._height_and_width().ws_row @property def width(self): - """T.width -> int + """ + Read-only property: Width of the terminal (in number of columns). - The width of the terminal in characters. + :rtype: int """ return self._height_and_width().ws_col @staticmethod def _winsize(fd): - """T._winsize -> WINSZ(ws_row, ws_col, ws_xpixel, ws_ypixel) + """ + Return named tuple describing size of the terminal by ``fd``. + + If the given platform does not have modules :mod:`termios`, + :mod:`fcntl`, or :mod:`tty`, window size of 80 columns by 25 + rows is always returned. - The tty connected by file desriptor fd is queried for its window size, - and returned as a collections.namedtuple instance WINSZ. + :param int fd: file descriptor queries for its window size. + :raises IOError: the file descriptor ``fd`` is not a terminal. + :rtype: WINSZ - May raise exception IOError. + WINSZ is a :class:`collections.namedtuple` instance, whose structure + directly maps to the return value of the :const:`termios.TIOCGWINSZ` + ioctl return value. The return parameters are: + + - ``ws_row``: width of terminal by its number of character cells. + - ``ws_col``: height of terminal by its number of character cells. + - ``ws_xpixel``: width of terminal by pixels (not accurate). + - ``ws_ypixel``: height of terminal by pixels (not accurate). """ if HAS_TTY: data = fcntl.ioctl(fd, termios.TIOCGWINSZ, WINSZ._BUF) return WINSZ(*struct.unpack(WINSZ._FMT, data)) - return WINSZ(ws_row=24, ws_col=80, ws_xpixel=0, ws_ypixel=0) + return WINSZ(ws_row=25, ws_col=80, ws_xpixel=0, ws_ypixel=0) def _height_and_width(self): - """Return a tuple of (terminal height, terminal width). """ - # TODO(jquast): hey kids, even if stdout is redirected to a file, - # we can still query sys.__stdin__.fileno() for our terminal size. - # -- of course, if both are redirected, we have no use for this fd. + Return a tuple of (terminal height, terminal width). + + If :attr:`stream` or :obj:`sys.__stdout__` is not a tty or does not + support :func:`fcntl.ioctl` of :const:`termios.TIOCGWINSZ`, a window + size of 80 columns by 25 rows is returned for any values not + represented by environment variables ``LINES`` and ``COLUMNS``, which + is the default text mode of IBM PC compatibles. + + :rtype: WINSZ + + WINSZ is a :class:`collections.namedtuple` instance, whose structure + directly maps to the return value of the :const:`termios.TIOCGWINSZ` + ioctl return value. The return parameters are: + + - ``ws_row``: width of terminal by its number of character cells. + - ``ws_col``: height of terminal by its number of character cells. + - ``ws_xpixel``: width of terminal by pixels (not accurate). + - ``ws_ypixel``: height of terminal by pixels (not accurate). + + """ for fd in (self._init_descriptor, sys.__stdout__): + # pylint: disable=pointless-except + # Except doesn't do anything try: if fd is not None: return self._winsize(fd) @@ -322,16 +372,17 @@ def _height_and_width(self): @contextlib.contextmanager def location(self, x=None, y=None): - """Return a context manager for temporarily moving the cursor. + """ + Return a context manager for temporarily moving the cursor. Move the cursor to a certain position on entry, let you print stuff there, then return the cursor to its original position:: term = Terminal() with term.location(2, 5): - print 'Hello, world!' for x in xrange(10): - print 'I can do it %i times!' % x + print('I can do it %i times!' % x) + print('We're back to the original location.') Specify ``x`` to move to a certain column, ``y`` to move to a certain row, both, or neither. If you specify neither, only the saving and @@ -339,7 +390,13 @@ def location(self, x=None, y=None): simply want to restore your place after doing some manual cursor movement. + .. note:: The store- and restore-cursor capabilities used internally + provide no stack. This means that :meth:`location` calls cannot be + nested: only one should be entered at a time. """ + # pylint: disable=invalid-name + # Invalid argument name "x" + # Save position and move to the requested column, row, or both: self.stream.write(self.save) if x is not None and y is not None: @@ -356,16 +413,20 @@ def location(self, x=None, y=None): @contextlib.contextmanager def fullscreen(self): - """Return a context manager that enters fullscreen mode while inside it - and restores normal mode on leaving. + """ + Context manager that switches to secondary screen, restoring on exit. - Fullscreen mode is characterized by instructing the terminal emulator - to store and save the current screen state (all screen output), switch - to "alternate screen". Upon exiting, the previous screen state is - returned. + Under the hood, this switches between the primary screen buffer and + the secondary one. The primary one is saved on entry and restored on + exit. Likewise, the secondary contents are also stable and are + faithfully restored on the next entry:: - This call may not be tested; only one screen state may be saved at a - time. + with term.fullscreen(): + main() + + .. note:: There is only one primary and one secondary screen buffer. + :meth:`fullscreen` calls cannot be nested, only one should be + entered at a time. """ self.stream.write(self.enter_fullscreen) try: @@ -375,8 +436,15 @@ def fullscreen(self): @contextlib.contextmanager def hidden_cursor(self): - """Return a context manager that hides the cursor upon entering, - and makes it visible again upon exiting.""" + """ + Context manager that hides the cursor, setting visibility on exit. + + with term.hidden_cursor(): + main() + + .. note:: :meth:`hidden_cursor` calls cannot be nested: only one + should be entered at a time. + """ self.stream.write(self.hide_cursor) try: yield @@ -385,15 +453,17 @@ def hidden_cursor(self): @property def color(self): - """Returns capability that sets the foreground color. + """ + A callable string that sets the foreground color. - The capability is unparameterized until called and passed a number - (0-15), at which point it returns another string which represents a + :arg int num: The foreground color index. This should be within the + bounds of :attr:`~.number_of_colors`. + :rtype: ParameterizingString + + The capability is unparameterized until called and passed a number, + 0-15, at which point it returns another string which represents a specific color change. This second string can further be called to color a piece of text and set everything back to normal afterward. - - :arg num: The number, 0-15, of the color - """ if not self.does_styling: return NullCallableString() @@ -402,7 +472,12 @@ def color(self): @property def on_color(self): - "Returns capability that sets the background color." + """ + A callable capability that sets the background color. + + :arg int num: The background color index. + :rtype: ParameterizingString + """ if not self.does_styling: return NullCallableString() return ParameterizingString(self._background_color, @@ -410,119 +485,206 @@ def on_color(self): @property def normal(self): - "Returns sequence that resets video attribute." + """ + A capability that resets all video attributes. + + :rtype: str + + ``normal`` is an alias for ``sgr0`` or ``exit_attribute_mode``. Any + styling attributes previously applied, such as foreground or + background colors, reverse video, or bold are reset to defaults. + """ if self._normal: return self._normal self._normal = resolve_capability(self, 'normal') return self._normal + @property + def stream(self): + """ + Read-only property: stream the terminal outputs to. + + This is a convenience attribute. It is used internally for implied + writes performed by context managers :meth:`~.hidden_cursor`, + :meth:`~.fullscreen`, :meth:`~.location`, and :meth:`~.keypad`. + """ + return self._stream + @property def number_of_colors(self): - """Return the number of colors the terminal supports. + """ + Read-only property: number of colors supported by terminal. + + Common values are 0, 8, 16, 88, and 256. - Common values are 0, 8, 16, 88, and 256. Most commonly - this may be used to test color capabilities at all:: + Most commonly, this may be used to test whether the terminal supports + colors. Though the underlying capability returns -1 when there is no + color support, we return 0. This lets you test more Pythonically:: if term.number_of_colors: - ...""" + ... + """ + # This is actually the only remotely useful numeric capability. We + # don't name it after the underlying capability, because we deviate + # slightly from its behavior, and we might someday wish to give direct + # access to it. + # trim value to 0, as tigetnum('colors') returns -1 if no support, - # -2 if no such capability. + # and -2 if no such capability. return max(0, self.does_styling and curses.tigetnum('colors') or -1) @property def _foreground_color(self): + """ + Convenience capability to support :attr:`~.on_color`. + + Prefers returning sequence for capability ``setaf``, "Set foreground + color to #1, using ANSI escape". If the given terminal does not + support such sequence, fallback to returning attribute ``setf``, + "Set foreground color #1". + """ return self.setaf or self.setf @property def _background_color(self): + """ + Convenience capability to support :attr:`~.on_color`. + + Prefers returning sequence for capability ``setab``, "Set background + color to #1, using ANSI escape". If the given terminal does not + support such sequence, fallback to returning attribute ``setb``, + "Set background color #1". + """ return self.setab or self.setb def ljust(self, text, width=None, fillchar=u' '): - """T.ljust(text, [width], [fillchar]) -> unicode + """ + Left-align ``text``, which may contain terminal sequences. - Return string ``text``, left-justified by printable length ``width``. - Padding is done using the specified fill character (default is a - space). Default ``width`` is the attached terminal's width. ``text`` - may contain terminal sequences.""" + :arg str text: String to be aligned + :arg int width: Total width to fill with aligned text. If + unspecified, the whole width of the terminal is filled. + :arg str fillchar: String for padding the right of ``text`` + :rtype: str + """ + # Left justification is different from left alignment, but we continue + # the vocabulary error of the str method for polymorphism. if width is None: width = self.width return Sequence(text, self).ljust(width, fillchar) def rjust(self, text, width=None, fillchar=u' '): - """T.rjust(text, [width], [fillchar]) -> unicode + """ + Right-align ``text``, which may contain terminal sequences. - Return string ``text``, right-justified by printable length ``width``. - Padding is done using the specified fill character (default is a - space). Default ``width`` is the attached terminal's width. ``text`` - may contain terminal sequences.""" + :arg str text: String to be aligned + :arg int width: Total width to fill with aligned text. If + unspecified, the whole width of the terminal is used. + :arg str fillchar: String for padding the left of ``text`` + :rtype: str + """ if width is None: width = self.width return Sequence(text, self).rjust(width, fillchar) def center(self, text, width=None, fillchar=u' '): - """T.center(text, [width], [fillchar]) -> unicode + """ + Center ``text``, which may contain terminal sequences. - Return string ``text``, centered by printable length ``width``. - Padding is done using the specified fill character (default is a - space). Default ``width`` is the attached terminal's width. ``text`` - may contain terminal sequences.""" + :arg str text: String to be centered + :arg int width: Total width in which to center text. If + unspecified, the whole width of the terminal is used. + :arg str fillchar: String for padding the left and right of ``text`` + :rtype: str + """ if width is None: width = self.width return Sequence(text, self).center(width, fillchar) def length(self, text): - """T.length(text) -> int + u""" + Return printable length of a string containing sequences. + + :arg str text: String to measure. May contain terminal sequences. + :rtype: int + :returns: The number of terminal character cells the string will occupy + when printed + + Wide characters that consume 2 character cells are supported: - Return the printable length of string ``text``, which may contain - terminal sequences. Strings containing sequences such as 'clear', - which repositions the cursor, does not give accurate results, and - their printable length is evaluated *0*.. + >>> term = Terminal() + >>> term.length(term.clear + term.red(u'コンニチハ')) + 10 + + .. note:: Sequences such as 'clear', which is considered as a + "movement sequence" because it would move the cursor to + (y, x)(0, 0), are evaluated as a printable length of + *0*. """ return Sequence(text, self).length() def strip(self, text, chars=None): - """T.strip(text) -> unicode + r""" + Return ``text`` without sequences and leading or trailing whitespace. + + :rtype: str - Return string ``text`` with terminal sequences removed, and leading - and trailing whitespace removed. + >>> term = blessed.Terminal() + >>> term.strip(u' \x1b[0;3m XXX ') + u'XXX' """ return Sequence(text, self).strip(chars) def rstrip(self, text, chars=None): - """T.rstrip(text) -> unicode + r""" + Return ``text`` without terminal sequences or trailing whitespace. - Return string ``text`` with terminal sequences and trailing whitespace - removed. + :rtype: str + + >>> term = blessed.Terminal() + >>> term.rstrip(u' \x1b[0;3m XXX ') + u' XXX' """ return Sequence(text, self).rstrip(chars) def lstrip(self, text, chars=None): - """T.lstrip(text) -> unicode + r""" + Return ``text`` without terminal sequences or leading whitespace. + + :rtype: str - Return string ``text`` with terminal sequences and leading whitespace - removed. + >>> term = blessed.Terminal() + >>> term.lstrip(u' \x1b[0;3m XXX ') + u'XXX ' """ return Sequence(text, self).lstrip(chars) def strip_seqs(self, text): - """T.strip_seqs(text) -> unicode + r""" + Return ``text`` stripped of only its terminal sequences. - Return string ``text`` stripped only of its sequences. + :rtype: str + + >>> term = blessed.Terminal() + >>> term.strip_seqs(u'\x1b[0;3mXXX') + u'XXX' """ return Sequence(text, self).strip_seqs() def wrap(self, text, width=None, **kwargs): - """T.wrap(text, [width=None, **kwargs ..]) -> list[unicode] - - Wrap paragraphs containing escape sequences ``text`` to the full - ``width`` of Terminal instance *T*, unless ``width`` is specified. - Wrapped by the virtual printable length, irregardless of the video - attribute sequences it may contain, allowing text containing colors, - bold, underline, etc. to be wrapped. - - Returns a list of strings that may contain escape sequences. See - ``textwrap.TextWrapper`` for all available additional kwargs to - customize wrapping behavior such as ``subsequent_indent``. + """ + Text-wrap a string, returning a list of wrapped lines. + + :arg str text: Unlike :func:`textwrap.wrap`, ``text`` may contain + terminal sequences, such as colors, bold, or underline. By + default, tabs in ``text`` are expanded by + :func:`string.expandtabs`. + :arg int width: Unlike :func:`textwrap.wrap`, ``width`` will + default to the width of the attached terminal. + :rtype: list + + See :class:`textwrap.TextWrapper` for keyword arguments that can + customize wrapping behaviour. """ width = self.width if width is None else width lines = [] @@ -535,47 +697,72 @@ def wrap(self, text, width=None, **kwargs): return lines def getch(self): - """T.getch() -> unicode - - Read and decode next byte from keyboard stream. May return u'' - if decoding is not yet complete, or completed unicode character. - Should always return bytes when self.kbhit() returns True. - - Implementors of input streams other than os.read() on the stdin fd - should derive and override this method. """ - assert self.keyboard_fd is not None - byte = os.read(self.keyboard_fd, 1) - return self._keyboard_decoder.decode(byte, final=False) + Read, decode, and return the next byte from the keyboard stream. - def kbhit(self, timeout=None, _intr_continue=True): - """T.kbhit([timeout=None]) -> bool + :rtype: unicode + :returns: a single unicode character, or ``u''`` if a multi-byte + sequence has not yet been fully received. - Returns True if a keypress has been detected on keyboard. + This method name and behavior mimics curses ``getch(void)``, and is + supports supports :meth:`inkey`, reading only one byte from + the keyboard string at a time. This method should always return + without blocking if called after :meth:`kbhit` has returned True. - When ``timeout`` is 0, this call is non-blocking, Otherwise blocking - until keypress is detected (default). When ``timeout`` is a positive - number, returns after ``timeout`` seconds have elapsed. + Implementors of alternate input stream methods should override + this method. + """ + assert self._keyboard_fd is not None + byte = os.read(self._keyboard_fd, 1) + return self._keyboard_decoder.decode(byte, final=False) - If input is not a terminal, False is always returned. + def kbhit(self, timeout=None, **_kwargs): """ - # Special care is taken to handle a custom SIGWINCH handler, which - # causes select() to be interrupted with errno 4 (EAGAIN) -- - # it is ignored, and a new timeout value is derived from the previous, - # unless timeout becomes negative, because signal handler has blocked - # beyond timeout, then False is returned. Otherwise, when timeout is 0, - # we continue to block indefinitely (default). + Return whether a keypress has been detected on the keyboard. + + This method is used by :meth:`inkey` to determine if a byte may + be read using :meth:`getch` without blocking. The standard + implementation simply uses the :func:`select.select` call on stdin. + + :arg float timeout: When ``timeout`` is 0, this call is + non-blocking, otherwise blocking indefinitely until keypress + is detected when None (default). When ``timeout`` is a + positive number, returns after ``timeout`` seconds have + elapsed (float). + :rtype: bool + :returns: True if a keypress is awaiting to be read on the keyboard + attached to this terminal. When input is not a terminal, False is + always returned. + """ + if _kwargs.pop('_intr_continue', None) is not None: + warnings.warn('keyword argument _intr_continue deprecated: ' + 'beginning v1.9.6, behavior is as though such ' + 'value is always True.') + if _kwargs: + raise TypeError('inkey() got unexpected keyword arguments {!r}' + .format(_kwargs)) + stime = time.time() - check_w, check_x, ready_r = [], [], [None, ] - check_r = [self.keyboard_fd] if self.keyboard_fd is not None else [] + ready_r = [None, ] + check_r = [self._keyboard_fd] if self._keyboard_fd is not None else [] while HAS_TTY and True: try: - ready_r, ready_w, ready_x = select.select( - check_r, check_w, check_x, timeout) + ready_r, _, _ = select.select(check_r, [], [], timeout) except InterruptedError: - if not _intr_continue: - return u'' + # Beginning with python3.5, IntrruptError is no longer thrown + # https://www.python.org/dev/peps/pep-0475/ + # + # For previous versions of python, we take special care to + # retry select on InterruptedError exception, namely to handle + # a custom SIGWINCH handler. When installed, it would cause + # select() to be interrupted with errno 4 (EAGAIN). + # + # Just as in python3.5, it is ignored, and a new timeout value + # is derived from the previous unless timeout becomes negative. + # because the signal handler has blocked beyond timeout, then + # False is returned. Otherwise, when timeout is None, we + # continue to block indefinitely (default). if timeout is not None: # subtract time already elapsed, timeout -= time.time() - stime @@ -587,37 +774,47 @@ def kbhit(self, timeout=None, _intr_continue=True): else: break - return False if self.keyboard_fd is None else check_r == ready_r + return False if self._keyboard_fd is None else check_r == ready_r @contextlib.contextmanager def cbreak(self): - """Return a context manager that enters 'cbreak' mode: disabling line - buffering of keyboard input, making characters typed by the user - immediately available to the program. Also referred to as 'rare' - mode, this is the opposite of 'cooked' mode, the default for most - shells. + """ + Allow each keystroke to be read immediately after it is pressed. + + This is a context manager for :func:`tty.setcbreak`. - In 'cbreak' mode, echo of input is also disabled: the application must - explicitly print any input received, if they so wish. + This context manager activates 'rare' mode, the opposite of 'cooked' + mode: On entry, :func:`tty.setcbreak` mode is activated disabling + line-buffering of keyboard input and turning off automatic echo of + input as output. - More information can be found in the manual page for curses.h, - http://www.openbsd.org/cgi-bin/man.cgi?query=cbreak + .. note:: You must explicitly print any user input you would like + displayed. If you provide any kind of editing, you must handle + backspace and other line-editing control functions in this mode + as well! - The python manual for curses, - http://docs.python.org/2/library/curses.html + **Normally**, characters received from the keyboard cannot be read + by Python until the *Return* key is pressed. Also known as *cooked* or + *canonical input* mode, it allows the tty driver to provide + line-editing before shuttling the input to your program and is the + (implicit) default terminal mode set by most unix shells before + executing programs. - Note also that setcbreak sets VMIN = 1 and VTIME = 0, - http://www.unixwiz.net/techtips/termios-vmin-vtime.html + Technically, this context manager sets the :mod:`termios` attributes + of the terminal attached to :obj:`sys.__stdin__`. + + .. note:: :func:`tty.setcbreak` sets ``VMIN = 1`` and ``VTIME = 0``, + see http://www.unixwiz.net/techtips/termios-vmin-vtime.html """ - if HAS_TTY and self.keyboard_fd is not None: - # save current terminal mode, - save_mode = termios.tcgetattr(self.keyboard_fd) - tty.setcbreak(self.keyboard_fd, termios.TCSANOW) + if HAS_TTY and self._keyboard_fd is not None: + # Save current terminal mode: + save_mode = termios.tcgetattr(self._keyboard_fd) + tty.setcbreak(self._keyboard_fd, termios.TCSANOW) try: yield finally: - # restore prior mode, - termios.tcsetattr(self.keyboard_fd, + # Restore prior mode: + termios.tcsetattr(self._keyboard_fd, termios.TCSAFLUSH, save_mode) else: @@ -625,21 +822,34 @@ def cbreak(self): @contextlib.contextmanager def raw(self): - """Return a context manager that enters *raw* mode. Raw mode is - similar to *cbreak* mode, in that characters typed are immediately - available to ``inkey()`` with one exception: the interrupt, quit, - suspend, and flow control characters are all passed through as their - raw character values instead of generating a signal. - """ - if HAS_TTY and self.keyboard_fd is not None: - # save current terminal mode, - save_mode = termios.tcgetattr(self.keyboard_fd) - tty.setraw(self.keyboard_fd, termios.TCSANOW) + r""" + A context manager for :func:`tty.setraw`. + + Raw mode differs from :meth:`cbreak` mode in that input and output + processing of characters is disabled, in similar in that they both + allow each keystroke to be read immediately after it is pressed. + + For input, the interrupt, quit, suspend, and flow control characters + are received as their raw control character values rather than + generating a signal. + + For output, the newline ``chr(10)`` is not sufficient enough to return + the carriage, requiring ``chr(13)`` printed explicitly by your + program:: + + with term.raw(): + print("printing in raw mode", end="\r\n") + + """ + if HAS_TTY and self._keyboard_fd is not None: + # Save current terminal mode: + save_mode = termios.tcgetattr(self._keyboard_fd) + tty.setraw(self._keyboard_fd, termios.TCSANOW) try: yield finally: - # restore prior mode, - termios.tcsetattr(self.keyboard_fd, + # Restore prior mode: + termios.tcsetattr(self._keyboard_fd, termios.TCSAFLUSH, save_mode) else: @@ -647,19 +857,21 @@ def raw(self): @contextlib.contextmanager def keypad(self): - """ - Context manager that enables keypad input (*keyboard_transmit* mode). + r""" + Return a context manager that enables directional keypad input. - This enables the effect of calling the curses function keypad(3x): - display terminfo(5) capability `keypad_xmit` (smkx) upon entering, - and terminfo(5) capability `keypad_local` (rmkx) upon exiting. + On entrying, this puts the terminal into "keyboard_transmit" mode by + emitting the keypad_xmit (smkx) capability. On exit, it emits + keypad_local (rmkx). - On an IBM-PC keypad of ttype *xterm*, with numlock off, the - lower-left diagonal key transmits sequence ``\\x1b[F``, ``KEY_END``. + On an IBM-PC keyboard with numeric keypad of terminal-type *xterm*, + with numlock off, the lower-left diagonal key transmits sequence + ``\\x1b[F``, translated to :class:`~.Terminal` attribute + ``KEY_END``. - However, upon entering keypad mode, ``\\x1b[OF`` is transmitted, - translating to ``KEY_LL`` (lower-left key), allowing diagonal - direction keys to be determined. + However, upon entering :meth:`keypad`, ``\\x1b[OF`` is transmitted, + translating to ``KEY_LL`` (lower-left key), allowing you to determine + diagonal direction keys. """ try: self.stream.write(self.smkx) @@ -667,47 +879,60 @@ def keypad(self): finally: self.stream.write(self.rmkx) - def inkey(self, timeout=None, esc_delay=0.35, _intr_continue=True): - """T.inkey(timeout=None, [esc_delay, [_intr_continue]]) -> Keypress() - - Receive next keystroke from keyboard (stdin), blocking until a - keypress is received or ``timeout`` elapsed, if specified. - - When used without the context manager ``cbreak``, stdin remains - line-buffered, and this function will block until return is pressed, - even though only one unicode character is returned at a time.. - - The value returned is an instance of ``Keystroke``, with properties - ``is_sequence``, and, when True, non-None values for attributes - ``code`` and ``name``. The value of ``code`` may be compared against - attributes of this terminal beginning with *KEY*, such as - ``KEY_ESCAPE``. - - To distinguish between ``KEY_ESCAPE``, and sequences beginning with - escape, the ``esc_delay`` specifies the amount of time after receiving - the escape character (chr(27)) to seek for the completion - of other application keys before returning ``KEY_ESCAPE``. - - Normally, when this function is interrupted by a signal, such as the - installment of SIGWINCH, this function will ignore this interruption - and continue to poll for input up to the ``timeout`` specified. If - you'd rather this function return ``u''`` early, specify a value - of ``False`` for ``_intr_continue``. - """ - # TODO(jquast): "meta sends escape", where alt+1 would send '\x1b1', - # what do we do with that? Surely, something useful. - # comparator to term.KEY_meta('x') ? - # TODO(jquast): Ctrl characters, KEY_CTRL_[A-Z], and the rest; - # KEY_CTRL_\, KEY_CTRL_{, etc. are not legitimate - # attributes. comparator to term.KEY_ctrl('z') ? - def _timeleft(stime, timeout): - """_timeleft(stime, timeout) -> float - - Returns time-relative time remaining before ``timeout`` - after time elapsed since ``stime``. + def inkey(self, timeout=None, esc_delay=0.35, **_kwargs): + """ + Read and return the next keyboard event within given timeout. + + Generally, this should be used inside the :meth:`raw` context manager. + + :arg float timeout: Number of seconds to wait for a keystroke before + returning. When ``None`` (default), this method may block + indefinitely. + :arg float esc_delay: To distinguish between the keystroke of + ``KEY_ESCAPE``, and sequences beginning with escape, the parameter + ``esc_delay`` specifies the amount of time after receiving escape + (``chr(27)``) to seek for the completion of an application key + before returning a :class:`~.Keystroke` instance for + ``KEY_ESCAPE``. + :rtype: :class:`~.Keystroke`. + :returns: :class:`~.Keystroke`, which may be empty (``u''``) if + ``timeout`` is specified and keystroke is not received. + :raises RuntimeError: When :attr:`stream` is not a terminal, having + no keyboard attached, a ``timeout`` value of ``None`` would block + indefinitely, prevented by by raising an exception. + + .. note:: When used without the context manager :meth:`cbreak`, or + :meth:`raw`, :obj:`sys.__stdin__` remains line-buffered, and this + function will block until the return key is pressed! + """ + if _kwargs.pop('_intr_continue', None) is not None: + warnings.warn('keyword argument _intr_continue deprecated: ' + 'beginning v1.9.6, behavior is as though such ' + 'value is always True.') + if _kwargs: + raise TypeError('inkey() got unexpected keyword arguments {!r}' + .format(_kwargs)) + if timeout is None and self._keyboard_fd is None: + raise RuntimeError( + 'Terminal.inkey() called, but no terminal with keyboard ' + 'attached to process. This call would hang forever.') + + def time_left(stime, timeout): + """ + Return time remaining since ``stime`` before given ``timeout``. + + This function assists determining the value of ``timeout`` for + class method :meth:`kbhit`. + + :arg float stime: starting time for measurement + :arg float timeout: timeout period, may be set to None to + indicate no timeout (where 0 is always returned). + :rtype: float or int + :returns: time remaining as float. If no time is remaining, + then the integer ``0`` is returned. """ if timeout is not None: - if timeout is 0: + if timeout == 0: return 0 return max(0, timeout - (time.time() - stime)) @@ -723,7 +948,7 @@ def _timeleft(stime, timeout): ucs += self._keyboard_buf.pop() # receive all immediately available bytes - while self.kbhit(0): + while self.kbhit(timeout=0): ucs += self.getch() # decode keystroke, if any @@ -732,7 +957,7 @@ def _timeleft(stime, timeout): # so long as the most immediately received or buffered keystroke is # incomplete, (which may be a multibyte encoding), block until until # one is received. - while not ks and self.kbhit(_timeleft(stime, timeout), _intr_continue): + while not ks and self.kbhit(timeout=time_left(stime, timeout)): ucs += self.getch() ks = resolve(text=ucs) @@ -741,10 +966,10 @@ def _timeleft(stime, timeout): # received. This is not optimal, but causes least delay when # (currently unhandled, and rare) "meta sends escape" is used, # or when an unsupported sequence is sent. - if ks.code is self.KEY_ESCAPE: + if ks.code == self.KEY_ESCAPE: esctime = time.time() - while (ks.code is self.KEY_ESCAPE and - self.kbhit(_timeleft(esctime, esc_delay))): + while (ks.code == self.KEY_ESCAPE and + self.kbhit(timeout=time_left(esctime, esc_delay))): ucs += self.getch() ks = resolve(text=ucs) @@ -752,34 +977,50 @@ def _timeleft(stime, timeout): self._keyboard_buf.extendleft(ucs[len(ks):]) return ks -# From libcurses/doc/ncurses-intro.html (ESR, Thomas Dickey, et. al): -# -# "After the call to setupterm(), the global variable cur_term is set to -# point to the current structure of terminal capabilities. By calling -# setupterm() for each terminal, and saving and restoring cur_term, it -# is possible for a program to use two or more terminals at once." -# -# However, if you study Python's ./Modules/_cursesmodule.c, you'll find: -# -# if (!initialised_setupterm && setupterm(termstr,fd,&err) == ERR) { -# -# Python - perhaps wrongly - will not allow for re-initialisation of new -# terminals through setupterm(), so the value of cur_term cannot be changed -# once set: subsequent calls to setupterm() have no effect. -# -# Therefore, the ``kind`` of each Terminal() is, in essence, a singleton. -# This global variable reflects that, and a warning is emitted if somebody -# expects otherwise. -_CUR_TERM = None +class WINSZ(collections.namedtuple('WINSZ', ( + 'ws_row', 'ws_col', 'ws_xpixel', 'ws_ypixel'))): + """ + Structure represents return value of :const:`termios.TIOCGWINSZ`. + + .. py:attribute:: ws_row -WINSZ = collections.namedtuple('WINSZ', ( - 'ws_row', # /* rows, in characters */ - 'ws_col', # /* columns, in characters */ - 'ws_xpixel', # /* horizontal size, pixels */ - 'ws_ypixel', # /* vertical size, pixels */ -)) -#: format of termios structure -WINSZ._FMT = 'hhhh' -#: buffer of termios structure appropriate for ioctl argument -WINSZ._BUF = '\x00' * struct.calcsize(WINSZ._FMT) + rows, in characters + + .. py:attribute:: ws_col + + columns, in characters + + .. py:attribute:: ws_xpixel + + horizontal size, pixels + + .. py:attribute:: ws_ypixel + + vertical size, pixels + """ + #: format of termios structure + _FMT = 'hhhh' + #: buffer of termios structure appropriate for ioctl argument + _BUF = '\x00' * struct.calcsize(_FMT) + + +#: From libcurses/doc/ncurses-intro.html (ESR, Thomas Dickey, et. al):: +#: +#: "After the call to setupterm(), the global variable cur_term is set to +#: point to the current structure of terminal capabilities. By calling +#: setupterm() for each terminal, and saving and restoring cur_term, it +#: is possible for a program to use two or more terminals at once." +#: +#: However, if you study Python's ``./Modules/_cursesmodule.c``, you'll find:: +#: +#: if (!initialised_setupterm && setupterm(termstr,fd,&err) == ERR) { +#: +#: Python - perhaps wrongly - will not allow for re-initialisation of new +#: terminals through :func:`curses.setupterm`, so the value of cur_term cannot +#: be changed once set: subsequent calls to :func:`setupterm` have no effect. +#: +#: Therefore, the :attr:`Terminal.kind` of each :class:`Terminal` is +#: essentially a singleton. This global variable reflects that, and a warning +#: is emitted if somebody expects otherwise. +_CUR_TERM = None diff --git a/blessed/tests/accessories.py b/blessed/tests/accessories.py index d908f5cd..bea4095a 100644 --- a/blessed/tests/accessories.py +++ b/blessed/tests/accessories.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """Accessories for automated py.test runner.""" -# std -from __future__ import with_statement +# standard imports +from __future__ import with_statement, print_function import contextlib import subprocess import functools @@ -15,15 +15,12 @@ # local from blessed import Terminal -from blessed._binterms import binary_terminals +from blessed._binterms import BINARY_TERMINALS -# 3rd +# 3rd-party import pytest +import six -if sys.version_info[0] == 3: - text_type = str -else: - text_type = unicode # noqa TestTerminal = functools.partial(Terminal, kind='xterm-256color') SEND_SEMAPHORE = SEMAPHORE = b'SEMAPHORE\n' @@ -45,7 +42,7 @@ else: available_terms = default_all_terms all_terms_params = list(set(available_terms) - ( - set(binary_terminals) if not os.environ.get('TEST_BINTERMS') + set(BINARY_TERMINALS) if not os.environ.get('TEST_BINTERMS') else set())) or default_all_terms @@ -61,8 +58,9 @@ def __init__(self, func): self.func = func def __call__(self, *args, **kwargs): + pid_testrunner = os.getpid() pid, master_fd = pty.fork() - if pid is self._CHILD_PID: + if pid == self._CHILD_PID: # child process executes function, raises exception # if failed, causing a non-zero exit code, using the # protected _exit() function of ``os``; to prevent the @@ -96,7 +94,11 @@ def __call__(self, *args, **kwargs): cov.save() os._exit(0) - exc_output = text_type() + if pid_testrunner != os.getpid(): + print('TEST RUNNER HAS FORKED, {0}=>{1}: EXIT' + .format(pid_testrunner, os.getpid()), file=sys.stderr) + os._exit(1) + exc_output = six.text_type() decoder = codecs.getincrementaldecoder(self.encoding)() while True: try: @@ -137,7 +139,7 @@ def read_until_semaphore(fd, semaphore=RECV_SEMAPHORE, # process will read xyz\\r\\n -- this is how pseudo terminals # behave; a virtual terminal requires both carriage return and # line feed, it is only for convenience that \\n does both. - outp = text_type() + outp = six.text_type() decoder = codecs.getincrementaldecoder(encoding)() semaphore = semaphore.decode('ascii') while not outp.startswith(semaphore): @@ -157,7 +159,7 @@ def read_until_semaphore(fd, semaphore=RECV_SEMAPHORE, def read_until_eof(fd, encoding='utf8'): """Read file descriptor ``fd`` until EOF. Return decoded string.""" decoder = codecs.getincrementaldecoder(encoding)() - outp = text_type() + outp = six.text_type() while True: try: _exc = os.read(fd, 100) @@ -209,7 +211,7 @@ def unicode_parm(cap, *parms): return u'' -@pytest.fixture(params=binary_terminals) +@pytest.fixture(params=BINARY_TERMINALS) def unsupported_sequence_terminals(request): """Terminals that emit warnings for unsupported sequence-awareness.""" return request.param diff --git a/blessed/tests/test_core.py b/blessed/tests/test_core.py index 9106b9f0..bc1a3bb6 100644 --- a/blessed/tests/test_core.py +++ b/blessed/tests/test_core.py @@ -2,10 +2,6 @@ "Core blessed Terminal() tests." # std -try: - from StringIO import StringIO -except ImportError: - from io import StringIO import collections import warnings import platform @@ -13,6 +9,7 @@ import sys import imp import os +import io # local from .accessories import ( @@ -25,6 +22,7 @@ # 3rd party import mock import pytest +import six def test_export_only_Terminal(): @@ -37,7 +35,7 @@ def test_null_location(all_terms): "Make sure ``location()`` with no args just does position restoration." @as_subprocess def child(kind): - t = TestTerminal(stream=StringIO(), force_styling=True) + t = TestTerminal(stream=six.StringIO(), force_styling=True) with t.location(): pass expected_output = u''.join( @@ -51,7 +49,7 @@ def test_flipped_location_move(all_terms): "``location()`` and ``move()`` receive counter-example arguments." @as_subprocess def child(kind): - buf = StringIO() + buf = six.StringIO() t = TestTerminal(stream=buf, force_styling=True) y, x = 10, 20 with t.location(y, x): @@ -67,7 +65,7 @@ def test_yield_keypad(): @as_subprocess def child(kind): # given, - t = TestTerminal(stream=StringIO(), force_styling=True) + t = TestTerminal(stream=six.StringIO(), force_styling=True) expected_output = u''.join((t.smkx, t.rmkx)) # exercise, @@ -85,7 +83,7 @@ def test_null_fileno(): @as_subprocess def child(): # This simulates piping output to another program. - out = StringIO() + out = six.StringIO() out.fileno = None t = TestTerminal(stream=out) assert (t.save == u'') @@ -97,12 +95,12 @@ def test_number_of_colors_without_tty(): "``number_of_colors`` should return 0 when there's no tty." @as_subprocess def child_256_nostyle(): - t = TestTerminal(stream=StringIO()) + t = TestTerminal(stream=six.StringIO()) assert (t.number_of_colors == 0) @as_subprocess def child_256_forcestyle(): - t = TestTerminal(stream=StringIO(), force_styling=True) + t = TestTerminal(stream=six.StringIO(), force_styling=True) assert (t.number_of_colors == 256) @as_subprocess @@ -112,13 +110,13 @@ def child_8_forcestyle(): # 'ansi' on freebsd returns 0 colors, we use the 'cons25' driver, # compatible with its kernel tty.c kind = 'cons25' - t = TestTerminal(kind=kind, stream=StringIO(), + t = TestTerminal(kind=kind, stream=six.StringIO(), force_styling=True) assert (t.number_of_colors == 8) @as_subprocess def child_0_forcestyle(): - t = TestTerminal(kind='vt220', stream=StringIO(), + t = TestTerminal(kind='vt220', stream=six.StringIO(), force_styling=True) assert (t.number_of_colors == 0) @@ -159,7 +157,7 @@ def test_init_descriptor_always_initted(all_terms): "Test height and width with non-tty Terminals." @as_subprocess def child(kind): - t = TestTerminal(kind=kind, stream=StringIO()) + t = TestTerminal(kind=kind, stream=six.StringIO()) assert t._init_descriptor == sys.__stdout__.fileno() assert (isinstance(t.height, int)) assert (isinstance(t.width, int)) @@ -247,15 +245,13 @@ def child(): term = TestTerminal(kind='xxXunknownXxx', force_styling=True) assert term.kind is None - assert term.does_styling is False + assert not term.does_styling assert term.number_of_colors == 0 warnings.resetwarnings() child() -@pytest.mark.skipif(platform.python_implementation() == 'PyPy', - reason='PyPy freezes') def test_missing_ordereddict_uses_module(monkeypatch): "ordereddict module is imported when without collections.OrderedDict." import blessed.keyboard @@ -279,8 +275,6 @@ def test_missing_ordereddict_uses_module(monkeypatch): assert platform.python_version_tuple() < ('2', '7') # reached by py2.6 -@pytest.mark.skipif(platform.python_implementation() == 'PyPy', - reason='PyPy freezes') def test_python3_2_raises_exception(monkeypatch): "Test python version 3.0 through 3.2 raises an exception." import blessed @@ -300,20 +294,6 @@ def test_python3_2_raises_exception(monkeypatch): assert False, 'Exception should have been raised' -def test_IOUnsupportedOperation_dummy(monkeypatch): - "Ensure dummy exception is used when io is without UnsupportedOperation." - import blessed.terminal - import io - if hasattr(io, 'UnsupportedOperation'): - monkeypatch.delattr('io.UnsupportedOperation') - - imp.reload(blessed.terminal) - assert blessed.terminal.IOUnsupportedOperation.__doc__.startswith( - "A dummy exception to take the place of") - monkeypatch.undo() - imp.reload(blessed.terminal) - - def test_without_dunder(): "Ensure dunder does not remain in module (py2x InterruptedError test." import blessed.terminal @@ -327,16 +307,16 @@ def child(): import blessed.terminal def side_effect(): - raise blessed.terminal.IOUnsupportedOperation + raise io.UnsupportedOperation mock_stream = mock.Mock() mock_stream.fileno = side_effect term = TestTerminal(stream=mock_stream) assert term.stream == mock_stream - assert term.does_styling is False - assert term.is_a_tty is False - assert term.number_of_colors is 0 + assert not term.does_styling + assert not term.is_a_tty + assert term.number_of_colors == 0 child() @@ -361,7 +341,7 @@ def test_yield_fullscreen(all_terms): "Ensure ``fullscreen()`` writes enter_fullscreen and exit_fullscreen." @as_subprocess def child(kind): - t = TestTerminal(stream=StringIO(), force_styling=True) + t = TestTerminal(stream=six.StringIO(), force_styling=True) t.enter_fullscreen = u'BEGIN' t.exit_fullscreen = u'END' with t.fullscreen(): @@ -376,7 +356,7 @@ def test_yield_hidden_cursor(all_terms): "Ensure ``hidden_cursor()`` writes hide_cursor and normal_cursor." @as_subprocess def child(kind): - t = TestTerminal(stream=StringIO(), force_styling=True) + t = TestTerminal(stream=six.StringIO(), force_styling=True) t.hide_cursor = u'BEGIN' t.normal_cursor = u'END' with t.hidden_cursor(): @@ -447,14 +427,16 @@ def __import__(name, *args, **kwargs): imp.reload(blessed.terminal) except UserWarning: err = sys.exc_info()[1] - assert err.args[0] == blessed.terminal.msg_nosupport + assert err.args[0] == blessed.terminal._MSG_NOSUPPORT warnings.filterwarnings("ignore", category=UserWarning) import blessed.terminal imp.reload(blessed.terminal) - assert blessed.terminal.HAS_TTY is False + assert not blessed.terminal.HAS_TTY term = blessed.terminal.Terminal('ansi') - assert term.height == 24 + # https://en.wikipedia.org/wiki/VGA-compatible_text_mode + # see section '#PC_common_text_modes' + assert term.height == 25 assert term.width == 80 finally: diff --git a/blessed/tests/test_formatters.py b/blessed/tests/test_formatters.py index b2e5f0c2..295d7290 100644 --- a/blessed/tests/test_formatters.py +++ b/blessed/tests/test_formatters.py @@ -107,7 +107,7 @@ def tparm_raises_TypeError(*args): def test_formattingstring(monkeypatch): """Test formatters.FormattingString""" - from blessed.formatters import (FormattingString) + from blessed.formatters import FormattingString # given, with arg pstr = FormattingString(u'attr', u'norm') @@ -184,8 +184,8 @@ def raises_exception(*args): def test_resolve_color(monkeypatch): """Test formatters.resolve_color.""" from blessed.formatters import (resolve_color, - FormattingString, - NullCallableString) + FormattingString, + NullCallableString) color_cap = lambda digit: 'seq-%s' % (digit,) monkeypatch.setattr(curses, 'COLOR_RED', 1984) @@ -247,7 +247,9 @@ def test_resolve_attribute_as_compoundable(monkeypatch): resolve_cap = lambda term, digit: 'seq-%s' % (digit,) COMPOUNDABLES = set(['JOINT', 'COMPOUND']) - monkeypatch.setattr(blessed.formatters, 'resolve_capability', resolve_cap) + monkeypatch.setattr(blessed.formatters, + 'resolve_capability', + resolve_cap) monkeypatch.setattr(blessed.formatters, 'COMPOUNDABLES', COMPOUNDABLES) term = mock.Mock() term.normal = 'seq-normal' @@ -264,8 +266,12 @@ def test_resolve_attribute_non_compoundables(monkeypatch): from blessed.formatters import resolve_attribute, ParameterizingString uncompoundables = lambda attr: ['split', 'compound'] resolve_cap = lambda term, digit: 'seq-%s' % (digit,) - monkeypatch.setattr(blessed.formatters, 'split_compound', uncompoundables) - monkeypatch.setattr(blessed.formatters, 'resolve_capability', resolve_cap) + monkeypatch.setattr(blessed.formatters, + 'split_compound', + uncompoundables) + monkeypatch.setattr(blessed.formatters, + 'resolve_capability', + resolve_cap) tparm = lambda *args: u'~'.join( arg.decode('latin1') if not num else '%s' % (arg,) for num, arg in enumerate(args)).encode('latin1') @@ -291,7 +297,9 @@ def test_resolve_attribute_recursive_compoundables(monkeypatch): # patch, resolve_cap = lambda term, digit: 'seq-%s' % (digit,) - monkeypatch.setattr(blessed.formatters, 'resolve_capability', resolve_cap) + monkeypatch.setattr(blessed.formatters, + 'resolve_capability', + resolve_cap) tparm = lambda *args: u'~'.join( arg.decode('latin1') if not num else '%s' % (arg,) for num, arg in enumerate(args)).encode('latin1') diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index c81154fd..448bcb6f 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -3,11 +3,6 @@ # std imports import functools import tempfile -try: - from StringIO import StringIO -except ImportError: - import io - StringIO = io.StringIO import platform import signal import curses @@ -35,6 +30,7 @@ # 3rd-party import pytest import mock +import six if sys.version_info[0] == 3: unichr = chr @@ -43,7 +39,7 @@ def test_kbhit_interrupted(): "kbhit() should not be interrupted with a signal handler." pid, master_fd = pty.fork() - if pid is 0: + if pid == 0: try: cov = __import__('cov_core_init').init() except ImportError: @@ -64,7 +60,7 @@ def on_resize(sig, action): with term.raw(): assert term.inkey(timeout=1.05) == u'' os.write(sys.__stdout__.fileno(), b'complete') - assert got_sigwinch is True + assert got_sigwinch if cov is not None: cov.stop() cov.save() @@ -86,7 +82,7 @@ def on_resize(sig, action): def test_kbhit_interrupted_nonetype(): "kbhit() should also allow interruption with timeout of None." pid, master_fd = pty.fork() - if pid is 0: + if pid == 0: try: cov = __import__('cov_core_init').init() except ImportError: @@ -107,7 +103,7 @@ def on_resize(sig, action): with term.raw(): term.inkey(timeout=1) os.write(sys.__stdout__.fileno(), b'complete') - assert got_sigwinch is True + assert got_sigwinch if cov is not None: cov.stop() cov.save() @@ -127,96 +123,7 @@ def on_resize(sig, action): assert math.floor(time.time() - stime) == 1.0 -def test_kbhit_interrupted_no_continue(): - "kbhit() may be interrupted when _intr_continue=False." - pid, master_fd = pty.fork() - if pid is 0: - try: - cov = __import__('cov_core_init').init() - except ImportError: - cov = None - - # child pauses, writes semaphore and begins awaiting input - global got_sigwinch - got_sigwinch = False - - def on_resize(sig, action): - global got_sigwinch - got_sigwinch = True - - term = TestTerminal() - signal.signal(signal.SIGWINCH, on_resize) - read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) - os.write(sys.__stdout__.fileno(), SEMAPHORE) - with term.raw(): - term.inkey(timeout=1.05, _intr_continue=False) - os.write(sys.__stdout__.fileno(), b'complete') - assert got_sigwinch is True - if cov is not None: - cov.stop() - cov.save() - os._exit(0) - - with echo_off(master_fd): - os.write(master_fd, SEND_SEMAPHORE) - read_until_semaphore(master_fd) - stime = time.time() - time.sleep(0.05) - os.kill(pid, signal.SIGWINCH) - output = read_until_eof(master_fd) - - pid, status = os.waitpid(pid, 0) - assert output == u'complete' - assert os.WEXITSTATUS(status) == 0 - assert math.floor(time.time() - stime) == 0.0 - - -def test_kbhit_interrupted_nonetype_no_continue(): - "kbhit() may be interrupted when _intr_continue=False with timeout None." - pid, master_fd = pty.fork() - if pid is 0: - try: - cov = __import__('cov_core_init').init() - except ImportError: - cov = None - - # child pauses, writes semaphore and begins awaiting input - global got_sigwinch - got_sigwinch = False - - def on_resize(sig, action): - global got_sigwinch - got_sigwinch = True - - term = TestTerminal() - signal.signal(signal.SIGWINCH, on_resize) - read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) - os.write(sys.__stdout__.fileno(), SEMAPHORE) - with term.raw(): - term.inkey(timeout=None, _intr_continue=False) - os.write(sys.__stdout__.fileno(), b'complete') - assert got_sigwinch is True - if cov is not None: - cov.stop() - cov.save() - os._exit(0) - - with echo_off(master_fd): - os.write(master_fd, SEND_SEMAPHORE) - read_until_semaphore(master_fd) - stime = time.time() - time.sleep(0.05) - os.kill(pid, signal.SIGWINCH) - os.write(master_fd, b'X') - output = read_until_eof(master_fd) - - pid, status = os.waitpid(pid, 0) - assert output == u'complete' - assert os.WEXITSTATUS(status) == 0 - assert math.floor(time.time() - stime) == 0.0 - - -def test_cbreak_no_kb(): +def test_break_input_no_kb(): "cbreak() should not call tty.setcbreak() without keyboard." @as_subprocess def child(): @@ -225,12 +132,25 @@ def child(): with mock.patch("tty.setcbreak") as mock_setcbreak: with term.cbreak(): assert not mock_setcbreak.called - assert term.keyboard_fd is None + assert term._keyboard_fd is None + child() + + +def test_raw_input_no_kb(): + "raw should not call tty.setraw() without keyboard." + @as_subprocess + def child(): + with tempfile.NamedTemporaryFile() as stream: + term = TestTerminal(stream=stream) + with mock.patch("tty.setraw") as mock_setraw: + with term.raw(): + assert not mock_setraw.called + assert term._keyboard_fd is None child() def test_notty_kb_is_None(): - "keyboard_fd should be None when os.isatty returns False." + "term._keyboard_fd should be None when os.isatty returns False." # in this scenerio, stream is sys.__stdout__, # but os.isatty(0) is False, # such as when piping output to less(1) @@ -239,20 +159,7 @@ def child(): with mock.patch("os.isatty") as mock_isatty: mock_isatty.return_value = False term = TestTerminal() - assert term.keyboard_fd is None - child() - - -def test_raw_no_kb(): - "raw() should not call tty.setraw() without keyboard." - @as_subprocess - def child(): - with tempfile.NamedTemporaryFile() as stream: - term = TestTerminal(stream=stream) - with mock.patch("tty.setraw") as mock_setraw: - with term.raw(): - assert not mock_setraw.called - assert term.keyboard_fd is None + assert term._keyboard_fd is None child() @@ -260,16 +167,16 @@ def test_kbhit_no_kb(): "kbhit() always immediately returns False without a keyboard." @as_subprocess def child(): - term = TestTerminal(stream=StringIO()) + term = TestTerminal(stream=six.StringIO()) stime = time.time() - assert term.keyboard_fd is None - assert term.kbhit(timeout=1.1) is False - assert (math.floor(time.time() - stime) == 1.0) + assert term._keyboard_fd is None + assert not term.kbhit(timeout=1.1) + assert math.floor(time.time() - stime) == 1.0 child() -def test_inkey_0s_cbreak_noinput(): - "0-second inkey without input; '' should be returned." +def test_keystroke_0s_cbreak_noinput(): + "0-second keystroke without input; '' should be returned." @as_subprocess def child(): term = TestTerminal() @@ -281,11 +188,11 @@ def child(): child() -def test_inkey_0s_cbreak_noinput_nokb(): - "0-second inkey without data in input stream and no keyboard/tty." +def test_keystroke_0s_cbreak_noinput_nokb(): + "0-second keystroke without data in input stream and no keyboard/tty." @as_subprocess def child(): - term = TestTerminal(stream=StringIO()) + term = TestTerminal(stream=six.StringIO()) with term.cbreak(): stime = time.time() inp = term.inkey(timeout=0) @@ -294,8 +201,8 @@ def child(): child() -def test_inkey_1s_cbreak_noinput(): - "1-second inkey without input; '' should be returned after ~1 second." +def test_keystroke_1s_cbreak_noinput(): + "1-second keystroke without input; '' should be returned after ~1 second." @as_subprocess def child(): term = TestTerminal() @@ -307,11 +214,11 @@ def child(): child() -def test_inkey_1s_cbreak_noinput_nokb(): - "1-second inkey without input or keyboard." +def test_keystroke_1s_cbreak_noinput_nokb(): + "1-second keystroke without input or keyboard." @as_subprocess def child(): - term = TestTerminal(stream=StringIO()) + term = TestTerminal(stream=six.StringIO()) with term.cbreak(): stime = time.time() inp = term.inkey(timeout=1) @@ -320,10 +227,10 @@ def child(): child() -def test_inkey_0s_cbreak_input(): - "0-second inkey with input; Keypress should be immediately returned." +def test_keystroke_0s_cbreak_with_input(): + "0-second keystroke with input; Keypress should be immediately returned." pid, master_fd = pty.fork() - if pid is 0: + if pid == 0: try: cov = __import__('cov_core_init').init() except ImportError: @@ -353,10 +260,10 @@ def test_inkey_0s_cbreak_input(): assert math.floor(time.time() - stime) == 0.0 -def test_inkey_cbreak_input_slowly(): - "0-second inkey with input; Keypress should be immediately returned." +def test_keystroke_cbreak_with_input_slowly(): + "0-second keystroke with input; Keypress should be immediately returned." pid, master_fd = pty.fork() - if pid is 0: + if pid == 0: try: cov = __import__('cov_core_init').init() except ImportError: @@ -395,11 +302,11 @@ def test_inkey_cbreak_input_slowly(): assert math.floor(time.time() - stime) == 0.0 -def test_inkey_0s_cbreak_multibyte_utf8(): - "0-second inkey with multibyte utf-8 input; should decode immediately." +def test_keystroke_0s_cbreak_multibyte_utf8(): + "0-second keystroke with multibyte utf-8 input; should decode immediately." # utf-8 bytes represent "latin capital letter upsilon". pid, master_fd = pty.fork() - if pid is 0: # child + if pid == 0: # child try: cov = __import__('cov_core_init').init() except ImportError: @@ -427,13 +334,12 @@ def test_inkey_0s_cbreak_multibyte_utf8(): assert math.floor(time.time() - stime) == 0.0 -@pytest.mark.skipif(os.environ.get('TRAVIS', None) is not None or - platform.python_implementation() == 'PyPy', - reason="travis-ci nor pypy handle ^C very well.") -def test_inkey_0s_raw_ctrl_c(): - "0-second inkey with raw allows receiving ^C." +@pytest.mark.skipif(os.environ.get('TRAVIS', None) is not None, + reason="travis-ci does not handle ^C very well.") +def test_keystroke_0s_raw_input_ctrl_c(): + "0-second keystroke with raw allows receiving ^C." pid, master_fd = pty.fork() - if pid is 0: # child + if pid == 0: # child try: cov = __import__('cov_core_init').init() except ImportError: @@ -463,10 +369,10 @@ def test_inkey_0s_raw_ctrl_c(): assert math.floor(time.time() - stime) == 0.0 -def test_inkey_0s_cbreak_sequence(): - "0-second inkey with multibyte sequence; should decode immediately." +def test_keystroke_0s_cbreak_sequence(): + "0-second keystroke with multibyte sequence; should decode immediately." pid, master_fd = pty.fork() - if pid is 0: # child + if pid == 0: # child try: cov = __import__('cov_core_init').init() except ImportError: @@ -493,10 +399,10 @@ def test_inkey_0s_cbreak_sequence(): assert math.floor(time.time() - stime) == 0.0 -def test_inkey_1s_cbreak_input(): - "1-second inkey w/multibyte sequence; should return after ~1 second." +def test_keystroke_1s_cbreak_with_input(): + "1-second keystroke w/multibyte sequence; should return after ~1 second." pid, master_fd = pty.fork() - if pid is 0: # child + if pid == 0: # child try: cov = __import__('cov_core_init').init() except ImportError: @@ -528,7 +434,7 @@ def test_inkey_1s_cbreak_input(): def test_esc_delay_cbreak_035(): "esc_delay will cause a single ESC (\\x1b) to delay for 0.35." pid, master_fd = pty.fork() - if pid is 0: # child + if pid == 0: # child try: cov = __import__('cov_core_init').init() except ImportError: @@ -563,7 +469,7 @@ def test_esc_delay_cbreak_035(): def test_esc_delay_cbreak_135(): "esc_delay=1.35 will cause a single ESC (\\x1b) to delay for 1.35." pid, master_fd = pty.fork() - if pid is 0: # child + if pid == 0: # child try: cov = __import__('cov_core_init').init() except ImportError: @@ -598,7 +504,7 @@ def test_esc_delay_cbreak_135(): def test_esc_delay_cbreak_timout_0(): """esc_delay still in effect with timeout of 0 ("nonblocking").""" pid, master_fd = pty.fork() - if pid is 0: # child + if pid == 0: # child try: cov = __import__('cov_core_init').init() except ImportError: @@ -639,7 +545,7 @@ def test_keystroke_default_args(): assert ks._code is None assert ks.code == ks._code assert u'x' == u'x' + ks - assert ks.is_sequence is False + assert not ks.is_sequence assert repr(ks) in ("u''", # py26, 27 "''",) # py33 @@ -648,12 +554,12 @@ def test_a_keystroke(): "Test keyboard.Keystroke constructor with set arguments." from blessed.keyboard import Keystroke ks = Keystroke(ucs=u'x', code=1, name=u'the X') - assert ks._name is u'the X' + assert ks._name == u'the X' assert ks.name == ks._name - assert ks._code is 1 + assert ks._code == 1 assert ks.code == ks._code assert u'xx' == u'x' + ks - assert ks.is_sequence is True + assert ks.is_sequence assert repr(ks) == "the X" @@ -787,8 +693,8 @@ def test_resolve_sequence(): ks = resolve_sequence(u'', mapper, codes) assert ks == u'' assert ks.name is None - assert ks.code is None - assert ks.is_sequence is False + assert ks.code == None + assert not ks.is_sequence assert repr(ks) in ("u''", # py26, 27 "''",) # py33 @@ -796,35 +702,35 @@ def test_resolve_sequence(): assert ks == u'n' assert ks.name is None assert ks.code is None - assert ks.is_sequence is False + assert not ks.is_sequence assert repr(ks) in (u"u'n'", "'n'",) ks = resolve_sequence(u'SEQ1', mapper, codes) assert ks == u'SEQ1' assert ks.name == u'KEY_SEQ1' - assert ks.code is 1 - assert ks.is_sequence is True + assert ks.code == 1 + assert ks.is_sequence assert repr(ks) in (u"KEY_SEQ1", "KEY_SEQ1") ks = resolve_sequence(u'LONGSEQ_longer', mapper, codes) assert ks == u'LONGSEQ' assert ks.name == u'KEY_LONGSEQ' - assert ks.code is 4 - assert ks.is_sequence is True + assert ks.code == 4 + assert ks.is_sequence assert repr(ks) in (u"KEY_LONGSEQ", "KEY_LONGSEQ") ks = resolve_sequence(u'LONGSEQ', mapper, codes) assert ks == u'LONGSEQ' assert ks.name == u'KEY_LONGSEQ' - assert ks.code is 4 - assert ks.is_sequence is True + assert ks.code == 4 + assert ks.is_sequence assert repr(ks) in (u"KEY_LONGSEQ", "KEY_LONGSEQ") ks = resolve_sequence(u'Lxxxxx', mapper, codes) assert ks == u'L' assert ks.name == u'KEY_L' - assert ks.code is 6 - assert ks.is_sequence is True + assert ks.code == 6 + assert ks.is_sequence assert repr(ks) in (u"KEY_L", "KEY_L") diff --git a/blessed/tests/test_length_sequence.py b/blessed/tests/test_length_sequence.py index 88096dcb..b096aeb4 100644 --- a/blessed/tests/test_length_sequence.py +++ b/blessed/tests/test_length_sequence.py @@ -1,4 +1,5 @@ # encoding: utf-8 +# std imports import itertools import platform import termios @@ -6,12 +7,9 @@ import fcntl import sys import os -try: - from StringIO import StringIO -except ImportError: - from io import StringIO -from .accessories import ( +# local +from blessed.tests.accessories import ( all_terms, as_subprocess, TestTerminal, @@ -19,7 +17,9 @@ many_lines, ) +# 3rd party import pytest +import six def test_length_cjk(): @@ -204,7 +204,7 @@ def child(): # set the pty's virtual window size os.environ['COLUMNS'] = '99' os.environ['LINES'] = '11' - t = TestTerminal(stream=StringIO()) + t = TestTerminal(stream=six.StringIO()) save_init = t._init_descriptor save_stdout = sys.__stdout__ try: diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index 160be47a..3ed8ba9b 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -1,10 +1,6 @@ # -*- coding: utf-8 -*- """Tests for Terminal() sequences and sequence-awareness.""" # std imports -try: - from StringIO import StringIO -except ImportError: - from io import StringIO import platform import random import sys @@ -21,9 +17,10 @@ unicode_cap, ) -# 3rd-party +# 3rd party import pytest import mock +import six def test_capability(): @@ -44,7 +41,7 @@ def test_capability_without_tty(): """Assert capability templates are '' when stream is not a tty.""" @as_subprocess def child(): - t = TestTerminal(stream=StringIO()) + t = TestTerminal(stream=six.StringIO()) assert t.save == u'' assert t.red == u'' @@ -55,7 +52,7 @@ def test_capability_with_forced_tty(): """force styling should return sequences even for non-ttys.""" @as_subprocess def child(): - t = TestTerminal(stream=StringIO(), force_styling=True) + t = TestTerminal(stream=six.StringIO(), force_styling=True) assert t.save == unicode_cap('sc') child() @@ -94,8 +91,7 @@ def child(): reason="travis-ci does not have binary-packed terminals.") def test_emit_warnings_about_binpacked(): """Test known binary-packed terminals (kermit, avatar) emit a warning.""" - from blessed.sequences import _BINTERM_UNSUPPORTED_MSG - from blessed._binterms import binary_terminals + from blessed._binterms import BINTERM_UNSUPPORTED_MSG @as_subprocess def child(kind): @@ -107,7 +103,7 @@ def child(kind): TestTerminal(kind=kind, force_styling=True) except UserWarning: err = sys.exc_info()[1] - assert (err.args[0] == _BINTERM_UNSUPPORTED_MSG.format(kind) or + assert (err.args[0] == BINTERM_UNSUPPORTED_MSG.format(kind) or err.args[0].startswith('Unknown parameter in ') or err.args[0].startswith('Failed to setupterm(') ), err @@ -115,43 +111,49 @@ def child(kind): assert 'warnings should have been emitted.' warnings.resetwarnings() - # any binary terminal should do. - child(binary_terminals[random.randrange(len(binary_terminals))]) + # Although any binary terminal should do, FreeBSD has "termcap entry bugs" + # that cause false negatives, because their underlying curses library emits + # some kind of "warning" to stderr, which our @as_subprocess decorator + # determines to be noteworthy enough to fail the test: + # https://gist.github.com/jquast/7b90af251fe4000baa09 + # + # so we chose only one of beautiful lineage: + # http://terminals.classiccmp.org/wiki/index.php/Tektronix_4207 + child(kind='tek4207-s') def test_unit_binpacked_unittest(): """Unit Test known binary-packed terminals emit a warning (travis-safe).""" import warnings - from blessed._binterms import binary_terminals - from blessed.sequences import (_BINTERM_UNSUPPORTED_MSG, - init_sequence_patterns) + from blessed._binterms import BINTERM_UNSUPPORTED_MSG + from blessed.sequences import init_sequence_patterns warnings.filterwarnings("error", category=UserWarning) term = mock.Mock() - term.kind = binary_terminals[random.randrange(len(binary_terminals))] + term.kind = 'tek4207-s' try: init_sequence_patterns(term) except UserWarning: err = sys.exc_info()[1] - assert err.args[0] == _BINTERM_UNSUPPORTED_MSG.format(term.kind) + assert err.args[0] == BINTERM_UNSUPPORTED_MSG.format(term.kind) else: assert False, 'Previous stmt should have raised exception.' warnings.resetwarnings() -def test_merge_sequences(): +def test_sort_sequences(): """Test sequences are filtered and ordered longest-first.""" - from blessed.sequences import _merge_sequences + from blessed.sequences import _sort_sequences input_list = [u'a', u'aa', u'aaa', u''] output_expected = [u'aaa', u'aa', u'a'] - assert (_merge_sequences(input_list) == output_expected) + assert (_sort_sequences(input_list) == output_expected) def test_location_with_styling(all_terms): """Make sure ``location()`` works on all terminals.""" @as_subprocess def child_with_styling(kind): - t = TestTerminal(kind=kind, stream=StringIO(), force_styling=True) + t = TestTerminal(kind=kind, stream=six.StringIO(), force_styling=True) with t.location(3, 4): t.stream.write(u'hi') expected_output = u''.join( @@ -168,7 +170,7 @@ def test_location_without_styling(): @as_subprocess def child_without_styling(): """No side effect for location as a context manager without styling.""" - t = TestTerminal(stream=StringIO(), force_styling=None) + t = TestTerminal(stream=six.StringIO(), force_styling=None) with t.location(3, 4): t.stream.write(u'hi') @@ -182,7 +184,7 @@ def test_horizontal_location(all_terms): """Make sure we can move the cursor horizontally without changing rows.""" @as_subprocess def child(kind): - t = TestTerminal(kind=kind, stream=StringIO(), force_styling=True) + t = TestTerminal(kind=kind, stream=six.StringIO(), force_styling=True) with t.location(x=5): pass expected_output = u''.join( @@ -201,7 +203,7 @@ def test_vertical_location(all_terms): """Make sure we can move the cursor horizontally without changing rows.""" @as_subprocess def child(kind): - t = TestTerminal(kind=kind, stream=StringIO(), force_styling=True) + t = TestTerminal(kind=kind, stream=six.StringIO(), force_styling=True) with t.location(y=5): pass expected_output = u''.join( @@ -219,7 +221,7 @@ def test_inject_move_x(): """Test injection of hpa attribute for screen/ansi (issue #55).""" @as_subprocess def child(kind): - t = TestTerminal(kind=kind, stream=StringIO(), force_styling=True) + t = TestTerminal(kind=kind, stream=six.StringIO(), force_styling=True) COL = 5 with t.location(x=COL): pass @@ -239,7 +241,7 @@ def test_inject_move_y(): """Test injection of vpa attribute for screen/ansi (issue #55).""" @as_subprocess def child(kind): - t = TestTerminal(kind=kind, stream=StringIO(), force_styling=True) + t = TestTerminal(kind=kind, stream=six.StringIO(), force_styling=True) ROW = 5 with t.location(y=ROW): pass @@ -259,7 +261,7 @@ def test_inject_civis_and_cnorm_for_ansi(): """Test injection of cvis attribute for ansi.""" @as_subprocess def child(kind): - t = TestTerminal(kind=kind, stream=StringIO(), force_styling=True) + t = TestTerminal(kind=kind, stream=six.StringIO(), force_styling=True) with t.hidden_cursor(): pass expected_output = u''.join( @@ -275,7 +277,7 @@ def test_zero_location(all_terms): """Make sure ``location()`` pays attention to 0-valued args.""" @as_subprocess def child(kind): - t = TestTerminal(kind=kind, stream=StringIO(), force_styling=True) + t = TestTerminal(kind=kind, stream=six.StringIO(), force_styling=True) with t.location(0, 0): pass expected_output = u''.join( @@ -349,7 +351,7 @@ def test_null_callable_numeric_colors(all_terms): """``color(n)`` should be a no-op on null terminals.""" @as_subprocess def child(kind): - t = TestTerminal(stream=StringIO(), kind=kind) + t = TestTerminal(stream=six.StringIO(), kind=kind) assert (t.color(5)('smoo') == 'smoo') assert (t.on_color(6)('smoo') == 'smoo') @@ -427,7 +429,7 @@ def test_formatting_functions_without_tty(all_terms): """Test crazy-ass formatting wrappers when there's no tty.""" @as_subprocess def child(kind): - t = TestTerminal(kind=kind, stream=StringIO(), force_styling=False) + t = TestTerminal(kind=kind, stream=six.StringIO(), force_styling=False) assert (t.bold(u'hi') == u'hi') assert (t.green('hi') == u'hi') # Test non-ASCII chars, no longer really necessary: @@ -479,7 +481,7 @@ def test_null_callable_string(all_terms): """Make sure NullCallableString tolerates all kinds of args.""" @as_subprocess def child(kind): - t = TestTerminal(stream=StringIO(), kind=kind) + t = TestTerminal(stream=six.StringIO(), kind=kind) assert (t.clear == '') assert (t.move(1 == 2) == '') assert (t.move_x(1) == '') diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 00000000..2a278785 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,46 @@ +API Documentation +================= + +terminal.py +----------- + +.. automodule:: blessed.terminal + :members: + :undoc-members: + :special-members: __getattr__ +.. autodata:: _CUR_TERM + +formatters.py +------------- + +.. automodule:: blessed.formatters + :members: + :undoc-members: + :private-members: + :special-members: __call__ +.. autodata:: COLORS +.. autodata:: COMPOUNDABLES + +keyboard.py +----------- + +.. automodule:: blessed.keyboard + :members: + :undoc-members: + :private-members: + :special-members: __new__ +.. autofunction:: _alternative_left_right +.. autofunction:: _inject_curses_keynames +.. autodata:: DEFAULT_SEQUENCE_MIXIN +.. autodata:: CURSES_KEYCODE_OVERRIDE_MIXIN + +sequences.py +------------ + +.. automodule:: blessed.sequences + :members: + :undoc-members: + :private-members: +.. autofunction:: _sort_sequences +.. autofunction:: _build_numeric_capability +.. autofunction:: _build_any_numeric_capability diff --git a/docs/conf.py b/docs/conf.py index 6975d7f1..7625622f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,27 +1,59 @@ -# -*- coding: utf-8 -*- -# -# blessed documentation build configuration file, created by -# sphinx-quickstart on Thu Mar 31 13:40:27 2011. -# +# std imports +import sys +import os +import json + +# 3rd-party +import sphinx_rtd_theme +import sphinx.environment +from docutils.utils import get_source_line + # This file is execfile()d with the current directory set to its # containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. -import sys -import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -here = os.path.dirname(__file__) -sys.path.insert(0, os.path.abspath(os.path.join(here, '..'))) +HERE = os.path.dirname(__file__) +sys.path.insert(0, os.path.abspath('sphinxext')) # for github.py +github_project_url = "https://github.com/jquast/blessed" -import blessed +# ! Monkey Patching! +# +# Seems many folks beside ourselves would like to see external image urls +# **not** generated as a warning. "nonlocal image URI found": we want our +# "badge icons" on pypi and github, but we don't want to miss out on any +# other "warnings as errors" which we wish to fail the build if it happens. +# +# https://github.com/SuperCowPowers/workbench/issues/172 +# https://groups.google.com/forum/#!topic/sphinx-users/GNx7PVXoZIU +# http://stackoverflow.com/a/28778969 +# +def _warn_node(self, msg, node): + if not msg.startswith('nonlocal image URI found:'): + self._warnfunc(msg, '%s:%s' % get_source_line(node)) +sphinx.environment.BuildEnvironment.warn_node = _warn_node + +# Monkey-patch functools.wraps and contextlib.wraps +# https://github.com/sphinx-doc/sphinx/issues/1711#issuecomment-93126473 +import functools +def no_op_wraps(func): + """ + Replaces functools.wraps in order to undo wrapping when generating Sphinx documentation + """ + import sys + if func.__module__ is None or 'blessed' not in func.__module__: + return functools.orig_wraps(func) + def wrapper(decorator): + sys.stderr.write('patched for function signature: {0!r}\n'.format(func)) + return func + return wrapper +functools.orig_wraps = functools.wraps +functools.wraps = no_op_wraps +import contextlib +contextlib.wraps = no_op_wraps +from blessed.terminal import * # -- General configuration ---------------------------------------------------- @@ -30,7 +62,12 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.viewcode', + 'github', + 'sphinx_paramlinks', + ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -46,14 +83,16 @@ # General information about the project. project = u'Blessed' -copyright = u'2014 Jeff Quast, 2011 Erik Rose' +copyright = u'2011 Erik Rose, Jeff Quast' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '1.9.5' +version = json.load( + open(os.path.join(HERE, os.pardir, 'version.json'), 'r') +)['version'] # The full version, including alpha/beta/rc tags. release = version @@ -77,11 +116,11 @@ # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -# add_function_parentheses = True +add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -# add_module_names = True +add_module_names = False # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. @@ -98,7 +137,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -106,7 +145,7 @@ # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -# html_theme_path = [] +html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". @@ -127,7 +166,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = [] +#html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. @@ -151,16 +190,16 @@ # html_use_index = True # If true, the index is split into individual pages for each letter. -# html_split_index = False +html_split_index = True # If true, links to the reST sources are added to the pages. -# html_show_sourcelink = True +html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -# html_show_sphinx = True +html_show_sphinx = False # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -# html_show_copyright = True +html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the @@ -187,7 +226,7 @@ # [howto/manual]). latex_documents = [ ('index', 'blessed.tex', u'Blessed Documentation', - u'Jeff Quast', 'manual'), + u'Erik Rose, Jeff Quast', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -220,9 +259,16 @@ # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'Blessed', u'Blessed Documentation', - [u'Jeff Quast'], 1) + [u'Erik Rose, Jeff Quast'], 1) ] + +# sort order of API documentation is by their appearance in source code autodoc_member_order = 'bysource' +# when linking to standard python library, use and prefer python 3 +# documentation. +intersphinx_mapping = {'https://docs.python.org/3/': None} -del blessed # imported but unused +# Both the class’ and the __init__ method’s docstring are concatenated and +# inserted. +autoclass_content = "both" diff --git a/docs/contributing.rst b/docs/contributing.rst new file mode 120000 index 00000000..798f2aa2 --- /dev/null +++ b/docs/contributing.rst @@ -0,0 +1 @@ +../CONTRIBUTING.rst \ No newline at end of file diff --git a/docs/examples.rst b/docs/examples.rst new file mode 100644 index 00000000..aacb2dc0 --- /dev/null +++ b/docs/examples.rst @@ -0,0 +1,68 @@ +Examples +======== + +A few programs are provided with blessed to help interactively +test the various API features, but also serve as examples of using +blessed to develop applications. + +These examples are not distributed with the package -- they are +only available in the github repository. You can retrieve them +by cloning the repository, or simply downloading the "raw" file +link. + +editor.py +--------- +https://github.com/jquast/blessed/blob/master/bin/editor.py + +This program demonstrates using the directional keys and noecho input +mode. It acts as a (very dumb) fullscreen editor, with support for +saving a file, which demonstrates how to provide a line-editor +rudimentary line-editor as well. + +keymatrix.py +------------ +https://github.com/jquast/blessed/blob/master/bin/keymatrix.py + +This program displays a "gameboard" of all known special KEY_NAME +constants. When the key is depressed, it is highlighted, as well +as displaying the unicode sequence, integer code, and friendly-name +of any key pressed. + +on_resize.py +------------ +https://github.com/jquast/blessed/blob/master/bin/on_resize.py + +This program installs a SIGWINCH signal handler, which detects +screen resizes while also polling for input, displaying keypresses. + +This demonstrates how a program can react to screen resize events. + +progress_bar.py +--------------- +https://github.com/jquast/blessed/blob/master/bin/progress_bar.py + +This program demonstrates a simple progress bar. All text is written +to stderr, to avoid the need to "flush" or emit newlines, and makes +use of the move_x (hpa) capability to "overstrike" the display a +scrolling progress bar. + +tprint.py +--------- +https://github.com/jquast/blessed/blob/master/bin/tprint.py + +This program demonstrates how users may customize FormattingString +styles. Accepting a string style, such as "bold" or "bright_red" +as the first argument, all subsequent arguments are displayed by +the given style. This shows how a program could provide +user-customizable compound formatting names to configure a program's +styling. + +worms.py +-------- +https://github.com/jquast/blessed/blob/master/bin/worms.py + +This program demonstrates how an interactive game could be made +with blessed. It is designed after the class game of WORMS.BAS, +distributed with early Microsoft Q-BASIC for PC-DOS, and later +more popularly known as "snake" as it was named on early mobile +platforms. diff --git a/docs/further.rst b/docs/further.rst new file mode 100644 index 00000000..f6f14c5a --- /dev/null +++ b/docs/further.rst @@ -0,0 +1,104 @@ +Further Reading +=============== + +As a developer's API, blessed is often bundled with frameworks and toolsets +that dive deeper into Terminal I/O programming than :class:`~.Terminal` offers. +Here are some recommended readings to help you along: + +- `terminfo(5) + `_ + manpage of your preferred posix-like operating system. The capabilities + available as attributes of :class:`~.Terminal` are directly mapped to those + listed in the column **Cap-name**. + +- `termios(4) + `_ + of your preferred posix-like operating system. + +- `The TTY demystified + `_ + by Linus Åkesson. + +- `A Brief Introduction to Termios + `_ by + Nelson Elhage. + +- Richard Steven's `Advance Unix Programming + `_ + ("AUP") provides two very good chapters, "Terminal I/O" and + "Pseudo Terminals". + +- GNU's `The Termcap Manual + `_ + by Richard M. Stallman. + +- `Chapter 4 `_ + of CUNY's course material for *Introduction to System Programming*, by + `Stewart Weiss `_ + +- `Chapter 11 + `_ + of the IEEE Open Group Base Specifications Issue 7, "General Terminal + Interface" + +- The GNU C Library documentation, section `Low-Level Terminal Interface + `_ + +- The source code of many popular terminal emulators. If there is ever any + question of "the meaning of a terminal capability", or whether or not your + preferred terminal emulator actually handles them, read the source! + + These are often written in the C language, and directly map the + "Control Sequence Inducers" (CSI, literally ``\x1b[`` for most modern + terminal types) emitted by most terminal capabilities to an action in a + series of ``case`` switch statements. + + - Many modern libraries are now based on `libvte + `_ (or just 'vte'): Gnome Terminal, + sakura, Terminator, Lilyterm, ROXTerm, evilvte, Termit, Termite, Tilda, + tinyterm, lxterminal. + - xterm, urxvt, SyncTerm, and EtherTerm. + - There are far too many to name, Chose one you like! + + +- The source code of the tty(4), pty(4), and the given "console driver" for + any posix-like operating system. If you search thoroughly enough, you will + eventually discover a terminal sequence decoder, usually a ``case`` switch + that translates ``\x1b[0m`` into a "reset color" action towards the video + driver. Though ``tty.c`` is linked here (the only kernel file common among + them), it is probably not the most interesting, but it can get you started: + + - `FreeBSD `_ + - `OpenBSD `_ + - `Illumos (Solaris) `_ + - `Minix `_ + - `Linux `_ + + The TTY driver is a great introduction to Kernel and Systems programming, + because familiar components may be discovered and experimented with. It is + available on all operating systems (except windows), and because of its + critical nature, examples of efficient file I/O, character buffers (often + implemented as "ring buffers") and even fine-grained kernel locking can be + found. + +- `Thomas E. Dickey `_ has been maintaining + `xterm `_, as well as a + primary maintainer of many related packages such as `ncurses + `_ for quite a long + while. + +- `termcap & terminfo (O'Reilly Nutshell) + `_ + by Linda Mui, Tim O'Reilly, and John Strang. + +- Note that System-V systems, also known as `Unix98 + `_ (SunOS, HP-UX, + AIX and others) use a `Streams `_ + interface. On these systems, the `ioctl(2) + `_ + interface provides the ``PUSH`` and ``POP`` parameters to communicate with + a Streams device driver, which differs significantly from Linux and BSD. + + Many of these systems provide compatible interfaces for Linux, but they may + not always be as complete as the counterpart they emulate, most especially + in regards to managing pseudo-terminals. diff --git a/docs/history.rst b/docs/history.rst new file mode 100644 index 00000000..00dbd3de --- /dev/null +++ b/docs/history.rst @@ -0,0 +1,203 @@ +Version History +=============== + +1.9 + * enhancement: :paramref:`~.Terminal.wrap.break_long_words` now supported by + :meth:`Terminal.wrap` + * Ignore :class:`curses.error` message ``'tparm() returned NULL'``: + this occurs on win32 or other platforms using a limited curses + implementation, such as PDCurses_, where :func:`curses.tparm` is + not implemented, or no terminal capability database is available. + * Context manager :meth:`~.keypad` emits sequences that enable + "application keys" such as the diagonal keys on the numpad. + This is equivalent to :meth:`curses.window.keypad`. + * bugfix: translate keypad application keys correctly. + * enhancement: no longer depend on the '2to3' tool for python 3 support. + * enhancement: allow ``civis`` and ``cnorm`` (*hide_cursor*, *normal_hide*) + to work with terminal-type *ansi* by emulating support by proxy. + * enhancement: new public attribute: :attr:`~.kind`: the very same as given + :paramref:`Terminal.__init__.kind` keyword argument. Or, when not given, + determined by and equivalent to the ``TERM`` Environment variable. + * deprecated: ``_intr_continue`` arguments introduced in 1.8 are now marked + deprecated in 1.9.6: beginning with python 3.5, the default behavior is as + though this argument is always True, `PEP-475 + `_. + +1.8 + * enhancement: export keyboard-read function as public method ``getch()``, + so that it may be overridden by custom terminal implementers. + * enhancement: allow :meth:`~.inkey` and :meth:`~.kbhit` to return early + when interrupted by signal by passing argument ``_intr_continue=False``. + * enhancement: allow ``hpa`` and ``vpa`` (*move_x*, *move_y*) to work on + tmux(1) or screen(1) by emulating support by proxy. + * enhancement: add :meth:`~.Terminal.rstrip` and :meth:`~.Terminal.lstrip`, + strips both sequences and trailing or leading whitespace, respectively. + * enhancement: include wcwidth_ library support for + :meth:`~.Terminal.length`: the printable width of many kinds of CJK + (Chinese, Japanese, Korean) ideographs and various combining characters + may now be determined. + * enhancement: better support for detecting the length or sequences of + externally-generated *ecma-48* codes when using ``xterm`` or ``aixterm``. + * bugfix: when :func:`locale.getpreferredencoding` returns empty string or + an encoding that is not valid for ``codecs.getincrementaldecoder``, + fallback to ASCII and emit a warning. + * bugfix: ensure :class:`~.FormattingString` and + :class:`~.ParameterizingString` may be pickled. + * bugfix: allow `~.inkey` and related to be called without a keyboard. + * **change**: ``term.keyboard_fd`` is set ``None`` if ``stream`` or + ``sys.stdout`` is not a tty, making ``term.inkey()``, ``term.cbreak()``, + ``term.raw()``, no-op. + * bugfix: ``\x1bOH`` (KEY_HOME) was incorrectly mapped as KEY_LEFT. + +1.7 + * Forked github project `erikrose/blessings`_ to `jquast/blessed`_, this + project was previously known as **blessings** version 1.6 and prior. + * introduced: context manager :meth:`~.cbreak`, which is equivalent to + entering terminal state by :func:`tty.setcbreak` and returning + on exit, as well as the lesser recommended :meth:`~.raw`, + pairing from :func:`tty.setraw`. + * introduced: :meth:`~.inkey`, which will return one or more characters + received by the keyboard as a unicode sequence, with additional attributes + :attr:`~.Keystroke.code` and :attr:`~.Keystroke.name`. This allows + application keys (such as the up arrow, or home key) to be detected. + Optional value :paramref:`~.inkey.timeout` allows for timed poll. + * introduced: :meth:`~.Terminal.center`, :meth:`~.Terminal.rjust`, + :meth:`~.Terminal.ljust`, allowing text containing sequences to be aligned + to detected horizontal screen width, or by + :paramref:`~.Terminal.center.width` specified. + * introduced: :meth:`~.wrap` method. Allows text containing sequences to be + word-wrapped without breaking mid-sequence, honoring their printable width. + * introduced: :meth:`~.Terminal.strip`, strips all sequences *and* + whitespace. + * introduced: :meth:`~.Terminal.strip_seqs` strip only sequences. + * introduced: :meth:`~.Terminal.rstrip` and :meth:`~.Terminal.lstrip` strips + both sequences and trailing or leading whitespace, respectively. + * bugfix: cannot call :func:`curses.setupterm` more than once per process + (from :meth:`Terminal.__init__`): Previously, blessed pretended + to support several instances of different Terminal :attr:`~.kind`, but was + actually using the :attr:`~.kind` specified by the first instantiation of + :class:`~.Terminal`. A warning is now issued. Although this is + misbehavior is still allowed, a :class:`warnings.WarningMessage` is now + emitted to notify about subsequent terminal misbehavior. + * bugfix: resolved issue where :attr:`~.number_of_colors` fails when + :attr:`~.does_styling` is ``False``. Resolves issue where piping tests + output would fail. + * bugfix: warn and set :attr:`~.does_styling` to ``False`` when the given + :attr:`~.kind`` is not found in the terminal capability database. + * bugfix: allow unsupported terminal capabilities to be callable just as + supported capabilities, so that the return value of + :attr:`~.color`\(n) may be called on terminals without color + capabilities. + * bugfix: for terminals without underline, such as vt220, + ``term.underline('text')`` would emit ``u'text' + term.normal``. + Now it emits only ``u'text'``. + * enhancement: some attributes are now properties, raise exceptions when + assigned. + * enhancement: pypy is now a supported python platform implementation. + * enhancement: removed pokemon ``curses.error`` exceptions. + * enhancement: do not ignore :class:`curses.error` exceptions, unhandled + curses errors are legitimate errors and should be reported as a bug. + * enhancement: converted nose tests to pytest, merged travis and tox. + * enhancement: pytest fixtures, paired with a new ``@as_subprocess`` + decorator + are used to test a multitude of terminal types. + * enhancement: test accessories ``@as_subprocess`` resolves various issues + with different terminal types that previously went untested. + * deprecation: python2.5 is no longer supported (as tox does not supported). + +1.6 + * Add :attr:`~.does_styling`. This takes :attr:`~.force_styling` + into account and should replace most uses of :attr:`~.is_a_tty`. + * Make :attr:`~.is_a_tty` a read-only property like :attr:`~.does_styling`. + Writing to it never would have done anything constructive. + * Add :meth:`~.fullscreen`` and :meth:`hidden_cursor` to the + auto-generated docs. + +1.5.1 + * Clean up fabfile, removing the redundant ``test`` command. + * Add Travis support. + * Make ``python setup.py test`` work without spurious errors on 2.6. + * Work around a tox parsing bug in its config file. + * Make context managers clean up after themselves even if there's an + exception (Vitja Makarov :ghpull:`29`). + * Parameterizing a capability no longer crashes when there is no tty + (Vitja Makarov :ghpull:`31`) + +1.5 + * Add syntactic sugar and documentation for ``enter_fullscreen`` + and ``exit_fullscreen``. + * Add context managers :meth:`~.fullscreen` and :meth:`~.hidden_cursor`. + * Now you can force a :class:`~.Terminal` to never to emit styles by + passing keyword argument ``force_styling=None``. + +1.4 + * Add syntactic sugar for cursor visibility control and single-space-movement + capabilities. + * Endorse the :meth:`~.location` context manager for restoring cursor + position after a series of manual movements. + * Fix a bug in which :meth:`~.location` that wouldn't do anything when + passed zeros. + * Allow tests to be run with ``python setup.py test``. + +1.3 + * Added :attr:`~.number_of_colors`, which tells you how many colors the + terminal supports. + * Made :attr:`~.color`\(n) and :attr:`~.on_color`\(n) callable to wrap a + string, like the named colors can. Also, make them both fall back to the + ``setf`` and ``setb`` capabilities (like the named colors do) if the + termcap entries for ``setaf`` and ``setab`` are not available. + * Allowed :attr:`~.color` to act as an unparametrized string, not just a + callable. + * Made :attr:`~.height` and :attr:`~.width` examine any passed-in stream + before falling back to stdout (This rarely if ever affects actual behavior; + it's mostly philosophical). + * Made caching simpler and slightly more efficient. + * Got rid of a reference cycle between :class:`~.Terminal` and + :class:`~.FormattingString`. + * Updated docs to reflect that terminal addressing (as in :meth:`~location`) + is 0-based. + +1.2 + * Added support for Python 3! We need 3.2.3 or greater, because the curses + library couldn't decide whether to accept strs or bytes before that + (http://bugs.python.org/issue10570). + * Everything that comes out of the library is now unicode. This lets us + support Python 3 without making a mess of the code, and Python 2 should + continue to work unless you were testing types (and badly). Please file a + bug if this causes trouble for you. + * Changed to the MIT License for better world domination. + * Added Sphinx docs. + +1.1 + * Added nicely named attributes for colors. + * Introduced compound formatting. + * Added wrapper behavior for styling and colors. + * Let you force capabilities to be non-empty, even if the output stream is + not a terminal. + * Added :attr:`~.is_a_tty` to determine whether the output stream is a + terminal. + * Sugared the remaining interesting string capabilities. + * Allow :meth:`~.location` to operate on just an x *or* y coordinate. + +1.0 + * Extracted Blessed from `nose-progressive`_. + + +.. _`nose-progressive`: http://pypi.python.org/pypi/nose-progressive/ +.. _`erikrose/blessings`: https://github.com/erikrose/blessings +.. _`jquast/blessed`: https://github.com/jquast/blessed +.. _`issue tracker`: https://github.com/jquast/blessed/issues/ +.. _curses: https://docs.python.org/library/curses.html +.. _couleur: https://pypi.python.org/pypi/couleur +.. _colorama: https://pypi.python.org/pypi/colorama +.. _wcwidth: https://pypi.python.org/pypi/wcwidth +.. _`cbreak(3)`: http://www.openbsd.org/cgi-bin/man.cgi?query=cbreak&apropos=0&sektion=3 +.. _`curs_getch(3)`: http://www.openbsd.org/cgi-bin/man.cgi?query=curs_getch&apropos=0&sektion=3 +.. _`termios(4)`: http://www.openbsd.org/cgi-bin/man.cgi?query=termios&apropos=0&sektion=4 +.. _`terminfo(5)`: http://www.openbsd.org/cgi-bin/man.cgi?query=terminfo&apropos=0&sektion=5 +.. _tigetstr: http://www.openbsd.org/cgi-bin/man.cgi?query=tigetstr&sektion=3 +.. _tparm: http://www.openbsd.org/cgi-bin/man.cgi?query=tparm&sektion=3 +.. _SIGWINCH: https://en.wikipedia.org/wiki/SIGWINCH +.. _`API Documentation`: http://blessed.rtfd.org +.. _`PDCurses`: http://www.lfd.uci.edu/~gohlke/pythonlibs/#curses +.. _`ansi`: https://github.com/tehmaze/ansi diff --git a/docs/index.rst b/docs/index.rst index 0383b3a1..b0ee657d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,50 +1,25 @@ -===================== -Blessed API Reference -===================== - -Read The Readme First -===================== - -This is the API documentation for the Blessed terminal library. - -Because Blessed uses quite a bit of dynamism, you should -`read the readme first`_ for a general guide and overview. - -However, if you're looking for the documentation of the internal -classes, their methods, and related functions that make up the -internals, you're in the right place. - -.. _`read the readme first`: http://pypi.python.org/pypi/blessed - -API Documentation -================= - -Internal modules are as follows. - -terminal module (primary) -------------------------- - -.. automodule:: blessed.terminal - :members: - :undoc-members: - -formatters module ------------------ - -.. automodule:: blessed.formatters - :members: - :undoc-members: - -keyboard module ---------------- - -.. automodule:: blessed.keyboard - :members: - :undoc-members: - -sequences module ----------------- - -.. automodule:: blessed.sequences - :members: - :undoc-members: +================================= +Welcome to Blessed documentation! +================================= + +Contents: + +.. toctree:: + :maxdepth: 3 + :glob: + + intro + overview + examples + further + pains + api + contributing + history + +======= +Indexes +======= + +* :ref:`genindex` +* :ref:`modindex` diff --git a/docs/intro.rst b/docs/intro.rst new file mode 100644 index 00000000..1cd10d0c --- /dev/null +++ b/docs/intro.rst @@ -0,0 +1,162 @@ +.. image:: https://img.shields.io/travis/jquast/blessed.svg + :alt: Travis Continous Integration + :target: https://travis-ci.org/jquast/blessed/ + +.. image:: https://img.shields.io/teamcity/http/teamcity-master.pexpect.org/s/Blessed_BuildHead.png + :alt: TeamCity Build status + :target: https://teamcity-master.pexpect.org/viewType.html?buildTypeId=Blessed_BuildHead&branch_Blessed=%3Cdefault%3E&tab=buildTypeStatusDiv + +.. image:: https://coveralls.io/repos/jquast/blessed/badge.png?branch=master + :alt: Coveralls Code Coverage + :target: https://coveralls.io/r/jquast/blessed?branch=master + +.. image:: https://img.shields.io/pypi/v/blessed.svg + :alt: Latest Version + :target: https://pypi.python.org/pypi/blessed + +.. image:: https://pypip.in/license/blessed/badge.svg + :alt: License + :target: http://opensource.org/licenses/MIT + +.. image:: https://img.shields.io/pypi/dm/blessed.svg + :alt: Downloads + :target: https://pypi.python.org/pypi/blessed + +Introduction +============ + +Blessed is a thin, practical wrapper around terminal capabilities in Python. + +Coding with *Blessed* looks like this... :: + + from blessed import Terminal + + t = Terminal() + + print(t.bold('Hi there!')) + print(t.bold_red_on_bright_green('It hurts my eyes!')) + + with t.location(0, t.height - 1): + print(t.center(t.blink('press any key to continue.'))) + + with t.cbreak(): + inp = t.inkey() + print('You pressed ' + repr(inp)) + + +Brief Overview +-------------- + +*Blessed* is a more simplified wrapper around curses_, providing : + +* Styles, color, and maybe a little positioning without necessarily + clearing the whole screen first. +* Works great with standard Python string formatting. +* Provides up-to-the-moment terminal height and width, so you can respond to + terminal size changes. +* Avoids making a mess if the output gets piped to a non-terminal: + outputs to any file-like object such as *StringIO*, files, or pipes. +* Uses the `terminfo(5)`_ database so it works with any terminal type + and supports any terminal capability: No more C-like calls to tigetstr_ + and tparm_. +* Keeps a minimum of internal state, so you can feel free to mix and match with + calls to curses or whatever other terminal libraries you like. +* Provides plenty of context managers to safely express terminal modes, + automatically restoring the terminal to a safe state on exit. +* Act intelligently when somebody redirects your output to a file, omitting + all of the terminal sequences such as styling, colors, or positioning. +* Dead-simple keyboard handling: safely decoding unicode input in your + system's preferred locale and supports application/arrow keys. +* Allows the printable length of strings containing sequences to be + determined. + +Blessed **does not** provide... + +* Windows command prompt support. A PDCurses_ build of python for windows + provides only partial support at this time -- there are plans to merge with + the ansi_ module in concert with colorama_ to resolve this. `Patches + welcome `_! + + +Before And After +---------------- + +With the built-in curses_ module, this is how you would typically +print some underlined text at the bottom of the screen:: + + from curses import tigetstr, setupterm, tparm + from fcntl import ioctl + from os import isatty + import struct + import sys + from termios import TIOCGWINSZ + + # If we want to tolerate having our output piped to other commands or + # files without crashing, we need to do all this branching: + if hasattr(sys.stdout, 'fileno') and isatty(sys.stdout.fileno()): + setupterm() + sc = tigetstr('sc') + cup = tigetstr('cup') + rc = tigetstr('rc') + underline = tigetstr('smul') + normal = tigetstr('sgr0') + else: + sc = cup = rc = underline = normal = '' + + # Save cursor position. + print(sc) + + if cup: + # tigetnum('lines') doesn't always update promptly, hence this: + height = struct.unpack('hhhh', ioctl(0, TIOCGWINSZ, '\000' * 8))[0] + + # Move cursor to bottom. + print(tparm(cup, height - 1, 0)) + + print('This is {under}underlined{normal}!' + .format(under=underline, normal=normal)) + + # Restore cursor position. + print(rc) + +The same program with *Blessed* is simply:: + + from blessed import Terminal + + term = Terminal() + with term.location(0, term.height - 1): + print('This is' + term.underline('underlined') + '!') + +Further Documentation +--------------------- + +More documentation can be found at http://blessed.readthedocs.org/en/latest/ + +Bugs, Contributing, Support +--------------------------- + +**Bugs** or suggestions? Visit the `issue tracker`_ and file an issue. +We welcome your bug reports and feature suggestions! + +Would you like to **contribute**? That's awesome! We've written a +`guide `_ +to help you. + +Are you stuck and need **support**? Give `stackoverflow`_ a try. If +you're still having trouble, we'd like to hear about it! Open an issue +in the `issue tracker`_ with a well-formed question. + +License +------- + +Blessed is under the MIT License. See the LICENSE file. + +.. _`issue tracker`: https://github.com/jquast/blessed/issues/ +.. _curses: https://docs.python.org/3/library/curses.html +.. _tigetstr: http://www.openbsd.org/cgi-bin/man.cgi/OpenBSD-current/man3/tigetstr.3 +.. _tparm: http://www.openbsd.org/cgi-bin/man.cgi/OpenBSD-current/man3/tparm.3 +.. _ansi: https://github.com/tehmaze/ansi +.. _colorama: https://pypi.python.org/pypi/colorama +.. _PDCurses: http://www.lfd.uci.edu/~gohlke/pythonlibs/#curses +.. _`terminfo(5)`: http://invisible-island.net/ncurses/man/terminfo.5.html +.. _`stackoverflow`: http://stackoverflow.com/ diff --git a/docs/overview.rst b/docs/overview.rst new file mode 100644 index 00000000..6d54be84 --- /dev/null +++ b/docs/overview.rst @@ -0,0 +1,578 @@ +Overview +======== + +Blessed provides just **one** top-level object: :class:`~.Terminal`. +Instantiating a :class:`~.Terminal` figures out whether you're on a terminal at +all and, if so, does any necessary setup: + + >>> term = Terminal() + +After that, you can proceed to ask it all sorts of things about the terminal, +such as its size: + + >>> term.height, term.width + (34, 102) + +Its color support: + + >>> term.number_of_colors + 256 + +And use construct strings containing color and styling: + + >>> term.green_reverse('ALL SYSTEMS GO') + u'\x1b[32m\x1b[7mALL SYSTEMS GO\x1b[m' + +Furthermore, the special sequences inserted with application keys +(arrow and function keys) are understood and decoded, as well as your +locale-specific encoded multibyte input, such as utf-8 characters. + + +Styling and Formatting +---------------------- + +Lots of handy formatting codes are available as attributes on a +:class:`~.Terminal` class instance. For example:: + + from blessed import Terminal + + term = Terminal() + + print('I am ' + term.bold + 'bold' + term.normal + '!') + +These capabilities (*bold*, *normal*) are translated to their sequences, which +when displayed simply change the video attributes. And, when used as a +callable, automatically wraps the given string with this sequence, and +terminates it with *normal*. + +The same can be written as:: + + print('I am' + term.bold('bold') + '!') + +You may also use the :class:`~.Terminal` instance as an argument for +the :meth:`str.format`` method, so that capabilities can be displayed in-line +for more complex strings:: + + print('{t.red_on_yellow}Candy corn{t.normal} for everyone!'.format(t=term)) + + +Capabilities +~~~~~~~~~~~~ + +The basic capabilities supported by most terminals are: + +``bold`` + Turn on 'extra bright' mode. +``reverse`` + Switch fore and background attributes. +``blink`` + Turn on blinking. +``normal`` + Reset attributes to default. + +The less commonly supported capabilities: + +``dim`` + Enable half-bright mode. +``underline`` + Enable underline mode. +``no_underline`` + Exit underline mode. +``italic`` + Enable italicized text. +``no_italic`` + Exit italics. +``shadow`` + Enable shadow text mode (rare). +``no_shadow`` + Exit shadow text mode. +``standout`` + Enable standout mode (often, an alias for ``reverse``). +``no_standout`` + Exit standout mode. +``subscript`` + Enable subscript mode. +``no_subscript`` + Exit subscript mode. +``superscript`` + Enable superscript mode. +``no_superscript`` + Exit superscript mode. +``flash`` + Visual bell, flashes the screen. + +Note that, while the inverse of *underline* is *no_underline*, the only way +to turn off *bold* or *reverse* is *normal*, which also cancels any custom +colors. + +Many of these are aliases, their true capability names (such as 'smul' for +'begin underline mode') may still be used. Any capability in the `terminfo(5)`_ +manual, under column **Cap-name**, may be used as an attribute of a +:class:`~.Terminal` instance. If it is not a supported capability, or a non-tty +is used as an output stream, an empty string is returned. + + +Colors +~~~~~~ + +Color terminals are capable of at least 8 basic colors. + +* ``black`` +* ``red`` +* ``green`` +* ``yellow`` +* ``blue`` +* ``magenta`` +* ``cyan`` +* ``white`` + +The same colors, prefixed with *bright_* (synonymous with *bold_*), +such as *bright_blue*, provides 16 colors in total. + +Prefixed with *on_*, the given color is used as the background color. +Some terminals also provide an additional 8 high-intensity versions using +*on_bright*, some example compound formats:: + + from blessed import Terminal + + term = Terminal() + + print(term.on_bright_blue('Blue skies!')) + + print(term.bright_red_on_bright_yellow('Pepperoni Pizza!')) + +You may also specify the :meth:`~.Terminal.color` index by number, which +should be within the bounds of value returned by +:attr:`~.Terminal.number_of_colors`:: + + from blessed import Terminal + + term = Terminal() + + for idx in range(term.number_of_colors): + print(term.color(idx)('Color {0}'.format(idx))) + +You can check whether the terminal definition used supports colors, and how +many, using the :attr:`~.Terminal.number_of_colors` property, which returns +any of *0*, *8* or *256* for terminal types such as *vt220*, *ansi*, and +*xterm-256color*, respectively. + +Colorless Terminals +~~~~~~~~~~~~~~~~~~~ + +If the terminal defined by the Environment variable **TERM** does not support +colors, these simply return empty strings. When used as a callable, the string +passed as an argument is returned as-is. Most sequences emitted to a terminal +that does not support them are usually harmless and have no effect. + +Colorless terminals (such as the amber or green monochrome *vt220*) do not +support colors but do support reverse video. For this reason, it may be +desirable in some applications to simply select a foreground color, followed +by reverse video to achieve the desired background color effect:: + + from blessed import Terminal + + term = Terminal() + + print(term.green_reverse('some terminals standout more than others')) + +Which appears as *black on green* on color terminals, but *black text +on amber or green* on monochrome terminals. Whereas the more declarative +formatter *black_on_green* would remain colorless. + +.. note:: On most color terminals, *bright_black* is not invisible -- it is + actually a very dark shade of gray! + +Compound Formatting +~~~~~~~~~~~~~~~~~~~ + +If you want to do lots of crazy formatting all at once, you can just mash it +all together:: + + from blessed import Terminal + + term = Terminal() + + print(term.bold_underline_green_on_yellow('Woo')) + +I'd be remiss if I didn't credit couleur_, where I probably got the idea for +all this mashing. + +This compound notation comes in handy if you want to allow users to customize +formatting, just allow compound formatters, like *bold_green*, as a command +line argument or configuration item:: + + #!/usr/bin/env python + import argparse + from blessed import Terminal + + parser = argparse.ArgumentParser( + description='displays argument as specified style') + parser.add_argument('style', type=str, help='style formatter') + parser.add_argument('text', type=str, nargs='+') + + term = Terminal() + + args = parser.parse_args() + + style = getattr(term, args.style) + + print(style(' '.join(args.text))) + +Saved as **tprint.py**, this could be used like:: + + $ ./tprint.py bright_blue_reverse Blue Skies + + +Moving The Cursor +----------------- + +When you want to move the cursor, you have a few choices: + +- ``location(x=None, y=None)`` context manager. +- ``move(row, col)`` capability. +- ``move_y(row)`` capability. +- ``move_x(col)`` capability. + +.. warning:: The :meth:`~.Terminal.location` method receives arguments in + positional order *(x, y)*, whereas the ``move()`` capability receives + arguments in order *(y, x)*. Please use keyword arguments as a later + release may correct the argument order of :meth:`~.Terminal.location`. + +Moving Temporarily +~~~~~~~~~~~~~~~~~~ + +A context manager, :meth:`~.Terminal.location` is provided to move the cursor +to an *(x, y)* screen position and restore the previous position upon exit:: + + from blessed import Terminal + + term = Terminal() + + with term.location(0, term.height - 1): + print('Here is the bottom.') + + print('This is back where I came from.') + +Parameters to :meth:`~.Terminal.location` are the **optional** *x* and/or *y* +keyword arguments:: + + with term.location(y=10): + print('We changed just the row.') + +When omitted, it saves the cursor position and restore it upon exit:: + + with term.location(): + print(term.move(1, 1) + 'Hi') + print(term.move(9, 9) + 'Mom') + +.. note:: calls to :meth:`~.Terminal.location` may not be nested. + + +Moving Permanently +~~~~~~~~~~~~~~~~~~ + +If you just want to move and aren't worried about returning, do something like +this:: + + from blessed import Terminal + + term = Terminal() + print(term.move(10, 1) + 'Hi, mom!') + +``move`` + Position the cursor, parameter in form of *(y, x)* +``move_x`` + Position the cursor at given horizontal column. +``move_y`` + Position the cursor at given vertical column. + +One-Notch Movement +~~~~~~~~~~~~~~~~~~ + +Finally, there are some parameterless movement capabilities that move the +cursor one character in various directions: + +* ``move_left`` +* ``move_right`` +* ``move_up`` +* ``move_down`` + +.. note:: *move_down* is often valued as *\\n*, which additionally returns + the carriage to column 0, depending on your terminal emulator, and may + also destructively destroy any characters at the given position to the + end of margin. + + +Height And Width +---------------- + +Use the :attr:`~.Terminal.height` and :attr:`~.Terminal.width` properties to +determine the size of the window:: + + from blessed import Terminal + + term = Terminal() + height, width = term.height, term.width + with term.location(x=term.width / 3, y=term.height / 3): + print('1/3 ways in!') + +These values are always current. To detect when the size of the window +changes, you may author a callback for SIGWINCH_ signals:: + + import signal + from blessed import Terminal + + term = Terminal() + + def on_resize(sig, action): + print('height={t.height}, width={t.width}'.format(t=term)) + + signal.signal(signal.SIGWINCH, on_resize) + + # wait for keypress + term.inkey() + + +Clearing The Screen +------------------- + +Blessed provides syntactic sugar over some screen-clearing capabilities: + +``clear`` + Clear the whole screen. +``clear_eol`` + Clear to the end of the line. +``clear_bol`` + Clear backward to the beginning of the line. +``clear_eos`` + Clear to the end of screen. + + +Full-Screen Mode +---------------- + +If you've ever noticed a program, such as an editor, restores the previous +screen (such as your shell prompt) after exiting, you're seeing the +*enter_fullscreen* and *exit_fullscreen* attributes in effect. + +``enter_fullscreen`` + Switch to alternate screen, previous screen is stored by terminal driver. +``exit_fullscreen`` + Switch back to standard screen, restoring the same terminal state. + +There's also a context manager you can use as a shortcut:: + + from __future__ import division + from blessed import Terminal + + term = Terminal() + with term.fullscreen(): + print(term.move_y(term.height // 2) + + term.center('press any key').rstrip()) + term.inkey() + + +Pipe Savvy +---------- + +If your program isn't attached to a terminal, such as piped to a program +like *less(1)* or redirected to a file, all the capability attributes on +:class:`~.Terminal` will return empty strings. You'll get a nice-looking +file without any formatting codes gumming up the works. + +If you want to override this, such as when piping output to *less -r*, pass +argument value *True* to the :paramref:`~.Terminal.force_styling` parameter. + +In any case, there is a :attr:`~.Terminal.does_styling` attribute that lets +you see whether the terminal attached to the output stream is capable of +formatting. If it is *False*, you may refrain from drawing progress +bars and other frippery and just stick to content:: + + from blessed import Terminal + + term = Terminal() + if term.does_styling: + with term.location(x=0, y=term.height - 1): + print('Progress: [=======> ]') + print(term.bold("60%")) + + +Sequence Awareness +------------------ + +Blessed may measure the printable width of strings containing sequences, +providing :meth:`~.Terminal.center`, :meth:`~.Terminal.ljust`, and +:meth:`~.Terminal.rjust` methods, using the terminal screen's width as +the default *width* value:: + + from __future__ import division + from blessed import Terminal + + term = Terminal() + with term.location(y=term.height // 2): + print(term.center(term.bold('bold and centered'))) + +Any string containing sequences may have its printable length measured using +the :meth:`~.Terminal.length` method. + +Additionally, a sequence-aware version of :func:`textwrap.wrap` is supplied as +class as method :meth:`~.Terminal.wrap` that is also sequence-aware, so now you +may word-wrap strings containing sequences. The following example displays a +poem word-wrapped to 25 columns:: + + from blessed import Terminal + + term = Terminal() + + poem = (term.bold_cyan('Plan difficult tasks'), + term.cyan('through the simplest tasks'), + term.bold_cyan('Achieve large tasks'), + term.cyan('through the smallest tasks')) + + for line in poem: + print('\n'.join(term.wrap(line, width=25, subsequent_indent=' ' * 4))) + + +Keyboard Input +-------------- + +The built-in python function :func:`raw_input` does not return a value until +the return key is pressed, and is not suitable for detecting each individual +keypress, much less arrow or function keys. + +Furthermore, when calling :func:`os.read` on input stream, only bytes are +received, which must be decoded to unicode using the locale-preferred encoding. +Finally, multiple bytes may be emitted which must be paired with some verb like +``KEY_LEFT``: blessed handles all of these special cases for you! + +cbreak +~~~~~~ + +The context manager :meth:`~.Terminal.cbreak` can be used to enter +*key-at-a-time* mode: Any keypress by the user is immediately consumed by read +calls:: + + from blessed import Terminal + import sys + + term = Terminal() + + with term.cbreak(): + # block until any single key is pressed. + sys.stdin.read(1) + +The mode entered using :meth:`~.Terminal.cbreak` is called +`cbreak(3)`_ in curses: + + The cbreak routine disables line buffering and erase/kill + character-processing (interrupt and flow control characters are unaffected), + making characters typed by the user immediately available to the program. + +:meth:`~.Terminal.raw` is similar to cbreak, but not recommended. + +inkey +~~~~~ + +The method :meth:`~.Terminal.inkey` combined with cbreak_ +completes the circle of providing key-at-a-time keyboard input with multibyte +encoding and awareness of application keys. + +:meth:`~.Terminal.inkey` resolves many issues with terminal input by +returning a unicode-derived :class:`~.Keystroke` instance. Its return value +may be printed, joined with, or compared like any other unicode strings, it +also provides the special attributes :attr:`~.Keystroke.is_sequence`, +:attr:`~.Keystroke.code`, and :attr:`~.Keystroke.name`:: + + from blessed import Terminal + + term = Terminal() + + print("press 'q' to quit.") + with term.cbreak(): + val = u'' + while val not in (u'q', u'Q',): + val = term.inkey(timeout=5) + if not val: + # timeout + print("It sure is quiet in here ...") + elif val.is_sequence: + print("got sequence: {0}.".format((str(val), val.name, val.code))) + elif val: + print("got {0}.".format(val)) + print('bye!') + +Its output might appear as:: + + got sequence: ('\x1b[A', 'KEY_UP', 259). + got sequence: ('\x1b[1;2A', 'KEY_SUP', 337). + got sequence: ('\x1b[17~', 'KEY_F6', 270). + got sequence: ('\x1b', 'KEY_ESCAPE', 361). + got sequence: ('\n', 'KEY_ENTER', 343). + got /. + It sure is quiet in here ... + got sequence: ('\x1bOP', 'KEY_F1', 265). + It sure is quiet in here ... + got q. + bye! + +A :paramref:`~.Terminal.inkey.timeout` value of *None* (default) will block +forever until a keypress is received. Any other value specifies the length of +time to poll for input: if no input is received after the given time has +elapsed, an empty string is returned. A +:paramref:`~.Terminal.inkey.timeout` value of *0* is non-blocking. + +keyboard codes +~~~~~~~~~~~~~~ + +When the :attr:`~.Keystroke.is_sequence` property tests *True*, the value +is a special application key of the keyboard. The :attr:`~.Keystroke.code` +attribute may then be compared with attributes of :class:`~.Terminal`, +which are duplicated from those found in `curs_getch(3)`_, or those +`constants `_ +in :mod:`curses` beginning with phrase *KEY_*. + +Some of these mnemonics are shorthand or predate modern PC terms and +are difficult to recall. The following helpful aliases are provided +instead: + +=================== ============= ==================== +blessed curses note +=================== ============= ==================== +``KEY_DELETE`` ``KEY_DC`` chr(127). +``KEY_TAB`` chr(9) +``KEY_INSERT`` ``KEY_IC`` +``KEY_PGUP`` ``KEY_PPAGE`` +``KEY_PGDOWN`` ``KEY_NPAGE`` +``KEY_ESCAPE`` ``KEY_EXIT`` +``KEY_SUP`` ``KEY_SR`` (shift + up) +``KEY_SDOWN`` ``KEY_SF`` (shift + down) +``KEY_DOWN_LEFT`` ``KEY_C1`` (keypad lower-left) +``KEY_UP_RIGHT`` ``KEY_A1`` (keypad upper-left) +``KEY_DOWN_RIGHT`` ``KEY_C3`` (keypad lower-left) +``KEY_UP_RIGHT`` ``KEY_A3`` (keypad lower-right) +``KEY_CENTER`` ``KEY_B2`` (keypad center) +``KEY_BEGIN`` ``KEY_BEG`` +=================== ============= ==================== + +The :attr:`~.Keystroke.name` property will prefer these +aliases over the built-in :mod:`curses` names. + +The following are **not** available in the :mod:`curses` module, but are +provided for keypad support, especially where the :meth:`~.Terminal.keypad` +context manager is used with numlock on: + +* ``KEY_KP_MULTIPLY`` +* ``KEY_KP_ADD`` +* ``KEY_KP_SEPARATOR`` +* ``KEY_KP_SUBTRACT`` +* ``KEY_KP_DECIMAL`` +* ``KEY_KP_DIVIDE`` +* ``KEY_KP_0`` through ``KEY_KP_9`` + +.. _couleur: https://pypi.python.org/pypi/couleur +.. _`cbreak(3)`: http://www.openbsd.org/cgi-bin/man.cgi/OpenBSD-current/man3/cbreak.3 +.. _`raw(3)`: http://www.openbsd.org/cgi-bin/man.cgi/OpenBSD-current/man3/raw.3 +.. _`curs_getch(3)`: http://www.openbsd.org/cgi-bin/man.cgi/OpenBSD-current/man3/curs_getch.3 +.. _`terminfo(5)`: http://www.openbsd.org/cgi-bin/man.cgi/OpenBSD-current/man4/termios.3 +.. _SIGWINCH: https://en.wikipedia.org/wiki/SIGWINCH diff --git a/docs/pains.rst b/docs/pains.rst new file mode 100644 index 00000000..a44ce574 --- /dev/null +++ b/docs/pains.rst @@ -0,0 +1,376 @@ +Growing Pains +============= + +When making terminal applications, there are a surprisingly number of +portability issues and edge cases. Although Blessed provides an abstraction +for the full curses capability database, it is not sufficient to secure +you from several considerations shared here. + +8 and 16 colors +--------------- + +Where 8 and 16 colors are used, they should be assumed to be the +`CGA Color Palette`_. Though there is no terminal standard that proclaims +that the CGA colors are used, their values are the best approximations +across all common hardware terminals and terminal emulators. + +A recent phenomenon of users is to customize their base 16 colors to provide +(often, more "washed out") color schemes. Furthermore, we are only recently +getting LCD displays of colorspaces that achieve close approximation to the +original video terminals. Some find these values uncomfortably intense: in +their original CRT form, their contrast and brightness was lowered by hardware +dials, whereas today's LCD's typically display well only near full intensity. + +Though we may not *detect* the colorspace of the remote terminal, **we can**: + +- Trust that a close approximation of the `CGA Color Palette`_ for the base + 16 colors will be displayed for **most** users. + +- Trust that users who have made the choice to adjust their palette have made + the choice to do so, and are able to re-adjust such palettes as necessary + to accommodate different programs (such as through the use of "Themes"). + +.. note:: + + It has become popular to use dynamic system-wide color palette adjustments + in software such as `f.lux`_, which adjust the system-wide "Color Profile" + of the entire graphics display depending on the time of day. One might + assume that ``term.blue("text")`` may be **completely** invisible to such + users during the night! + + +Where is brown, purple, or grey? +-------------------------------- + +There are **only 8 color names** on a 16-color terminal: The second set of +eight colors are "high intensity" versions of the first in direct series. + +The colors *brown*, *purple*, and *grey* are not named in the first series, +though they are available: + +- **brown**: *yellow is brown*, only high-intensity yellow + (``bright_yellow``) is yellow! + +- **purple**: *magenta is purple*. In earlier, 4-bit color spaces, there + were only black, cyan, magenta, and white of low and high intensity, such + as found on common home computers like the `ZX Spectrum`_. + + Additional "colors" were only possible through dithering. The color names + cyan and magenta on later graphics adapters are carried over from its + predecessors. Although the color cyan remained true in RGB value on + 16-color to its predecessor, magenta shifted farther towards blue from red + becoming purple (as true red was introduced as one of the new base 8 + colors). + +- **grey**: there are actually **three shades of grey** (or American spelling, + 'gray'), though the color attribute named 'grey' does not exist! + + In ascending order of intensity, the shades of grey are: + + - ``bold_black``: in lieu of the uselessness of an "intense black", this is + color is instead mapped to "dark grey". + - ``white``: white is actually mild compared to the true color 'white': this + is more officially mapped to "common grey", and is often the default + foreground color. + - ``bright_white``: is pure white (``#ffffff``). + + +white-on-black +~~~~~~~~~~~~~~ + +The default foreground and background should be assumed as *white-on-black*. + +For quite some time, the families of terminals produced by DEC, IBM, and +Tektronix dominated the computing world with the default color scheme of +*green-on-black* and less commonly *amber-on-black* monochrome displays: +The inverse was a non-default configuration. The IBM 3270 clients exclusively +used *green-on-black* in both hardware and software emulators, and is likely +a driving factor of the default *white-on-black* appearance of the first IBM +Personal Computer. + +The less common *black-on-white* "ink paper" style of emulators is a valid +concern for those designing terminal interfaces. The color scheme of +*black-on-white* directly conflicts with the intention of `bold is bright`_, +where ``term.bright_red('ATTENTION!')`` may become difficult to read, +as it appears as *pink on white*! + + +History of ink-paper inspired black-on-white +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Early home computers with color video adapters, such as the Commodore 64 +provided *white-on-blue* as their basic video terminal configuration. One can +only assume such appearances were provided to demonstrate their color +capabilities over competitors (such as the Apple ][). + +More common, X11's xterm and the software HyperTerm bundle with MS Windows +provided an "ink on paper" *black-on-white* appearance as their default +configuration. Two popular emulators continue to supply *black-on-white* by +default to this day: Xorg's xterm and Apple's Terminal.app. + +.. note:: Windows no longer supplies a terminal emulator: the "command prompt" + as we know it now uses the MSVCRT API routines to interact and does not + make use of terminal sequences, even ignoring those sequences that MS-DOS + family of systems previously interpreted through the ANSI.SYS driver, + though it continues to default to *white-on-black*. + + +Bold is bright +-------------- + +**Where Bold is used, it should be assumed to be *Bright***. + +Due to the influence of early graphics adapters providing a set of 8 +"low-intensity" and 8 "high intensity" versions of the first, the term +"bold" for terminals sequences is synonymous with "high intensity" in +almost all circumstances. + + +History of bold as "wide stroke" +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In typography, the true translation of "bold" is that a font should be +displayed *with emphasis*. In classical terms, this would be achieved by +pen be re-writing over the same letters. On a teletype or printer, this was +similarly achieved by writing a character, backspacing, then repeating the +same character in a form called **overstriking**. + +To bold a character, ``C``, one would emit the sequence ``C^HC`` where +``^H`` is backspace (0x08). To underline ``C``, one would would emit +``C^H_``. + +**Video terminals do not support overstriking**. Though the mdoc format for +manual pages continue to emit overstriking sequences for bold and underline, +translators such as mandoc will instead emit an appropriate terminal sequence. + +Many characters previously displayable by combining using overstriking of +ASCII characters on teletypes, such as: ±, ≠, or ⩝ were delegated to a +`code page`_ or lost entirely until the introduction of multibyte encodings. + +Much like the "ink paper" introduction in windowing systems for terminal +emulators, "wide stroke" bold was introduced only much later when combined +with operating systems that provided font routines such as TrueType. + + +Enforcing white-on-black +~~~~~~~~~~~~~~~~~~~~~~~~ + +In conclusion, *white-on-black* should be considered the default. If there is +a need to **enforce** *white-on-black* for terminal clients suspected to be +defaulted as *black-on-white*, one would want to trust that a combination of +``term.home + term.white_on_black + term.clear`` should repaint the entire +emulator's window with the desired effect. + +However, this cannot be trusted to **all** terminal emulators to perform +correctly! Depending on your audience, you may instead ensure that the +entire screen (including whitespace) is painted using the ``on_black`` +mnemonic. + +Beware of customized color schemes +---------------------------------- + +A recent phenomenon is for users to customize these first 16 colors of their +preferred emulator to colors of their own liking. Though this has always been +possible with ``~/.XResources``, the introduction of PuTTy and iTerm2 to +interactively adjustment these colors have made this much more common. + +This may cause your audience to see your intended interface in a wildly +different form. Your intended presentation may appear mildly unreadable. + +Users are certainly free to customize their colors however they like, but it +should be known that displaying ``term.black_on_red("DANGER!")`` may appear +as "grey on pastel red" to your audience, reducing the intended effect of +intensity. + + +256 colors can avoid customization +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The first instinct of a user who aliases ls(1) to ``ls -G`` or ``colorls``, +when faced with the particularly low intensity of the default ``blue`` +attribute is **to adjust their terminal emulator's color scheme of the base +16 colors**. + +This is not necessary: the environment variable ``LSCOLORS`` may be redefined +to map an alternative color for blue, or to use ``bright_blue`` in its place. + +Furthermore, all common terminal text editors such as emacs or vim may be +configured with "colorschemes" to make use of the 256-color support found in +most modern emulators. Many readable shades of blue are available, and many +programs that emit such colors can be configured to emit a higher or lower +intensity variant from the full 256 color space through program configuration. + + +Monochrome and reverse +---------------------- + +Note that ``reverse`` takes the current foreground and background colors and +reverses them. In contrast, the compound formatter ``black_on_red`` would +fail to set the background *or* foreground color on a monochrome display, +resulting in the same stylization as ``normal`` -- it would not appear any +different! + +If your userbase consists of monochrome terminals, you may wish to provide +"lightbars" and other such effects using the compound formatter +``red_reverse``. In the literal sense of "set foreground color to red, then +swap foreground and background", this produces a similar effect on +**both** color and monochrome displays. + +For text, very few ``{color}_on_{color}`` formatters are visible with the +base 16 colors, so you should generally wish for ``black_on_{color}`` +anyway. By using ``{color}_reverse`` you may be portable with monochrome +displays as well. + + +Multibyte Encodings and Code pages +---------------------------------- + +A terminal that supports both multibyte encodings (UTF-8) and legacy 8-bit +code pages (ISO 2022) may instruct the terminal to switch between both +modes using the following sequences: + + - ``\x1b%G`` activates UTF-8 with an unspecified implementation level + from ISO 2022 in a way that allows to go back to ISO 2022 again. + - ``\x1b%@`` goes back from UTF-8 to ISO 2022 in case UTF-8 had been + entered via ``\x1b%G``. + - ``\x1b%/G`` switches to UTF-8 Level 1 with no return. + - ``\x1b%/H`` switches to UTF-8 Level 2 with no return. + - ``\x1b%/I`` switches to UTF-8 Level 3 with no return. + +When a terminal is in ISO 2022 mode, you may use a sequence +to request a terminal to change its `code page`_. It begins by ``\x1b(``, +followed by an ASCII character representing a code page selection. For +example ``\x1b(U`` on the legacy VGA Linux console switches to the `IBM CP437`_ +`code page`_, allowing North American MS-DOS artwork to be displayed in its +natural 8-bit byte encoding. A list of standard codes and the expected code +page may be found on Thomas E. Dickey's xterm control sequences section on +sequences following the `Control-Sequence-Inducer`_. + +For more information, see `What are the issues related to UTF-8 terminal +emulators? `_ by +`Markus Kuhn `_ of the University of +Cambridge. + +Detecting multibyte +~~~~~~~~~~~~~~~~~~~ + +One can be assured that the connecting client is capable of representing +UTF-8 and other multibyte character encodings by the Environment variable +``LANG``. If this is not possible, there is an alternative method: + + - Emit Report Cursor Position (CPR), ``\x1b[6n`` and store response. + - Emit a multibyte UTF-8 character, such as ⦰ (``\x29\xb0``). + - Emit Report Cursor Position (CPR), ``\x1b[6n`` and store response. + - Determine the difference of the *(y, x)* location of the response. + If it is *1*, then the client decoded the two UTF-8 bytes as a + single character, and can be considered capable. If it is *2*, + the client is using a `code page`_ and is incapable of decoding + a UTF-8 bytestream. + +Note that both SSH and Telnet protocols provide means for forwarding +the ``LANG`` environment variable. However, some transports such as +a link by serial cable is incapable of forwarding Environment variables. + +Detecting screen size +~~~~~~~~~~~~~~~~~~~~~ + +While we're on the subject, there are times when :attr:`height` and +:attr:`width` are not accurate -- when a transport does not provide the means +to propagate the COLUMNS and ROWS Environment values, or propagate the +SIGWINCH signals, such as through a serial link. + +The same means described above for multibyte encoding detection may be used to +detect the remote client's window size: + + - Move cursor to row 999, 999. + - Emit Report Cursor Position (CPR), ``\x1b[6n`` and store response. + - The return value is the window dimensions of the client. + +This is the method used by the program ``resize`` provided in the Xorg +distribution, and its source may be viewed as file `resize.c`_. + +Alt or meta sends Escape +------------------------ + +Programs using GNU readline such as bash continue to provide default mappings +such as *ALT+u* to uppercase the word after cursor. This is achieved +by the configuration option altSendsEscape or `metaSendsEscape +`_ + +The default for most terminals, however, is that the meta key is bound by +the operating system (such as *META + F* for find), and that *ALT* is used +for inserting international keys (where the combination *ALT+u, a* is used +to insert the character ``ä``). + +It is therefore a recommendation to **avoid alt or meta keys entirely** in +applications, and instead prefer the ctrl-key combinations, so as to avoid +instructing your users to configure their terminal emulators to communicate +such sequences. + +If you wish to allow them optionally (such as through readline), the ability +to detect alt or meta key combinations is achieved by prefacing the combining +character with escape, so that *ALT+z* becomes *Escape + z* (or, in raw form +``\x1bz``). Blessings currently provides no further assistance in detecting +these key combinations. + + +Backspace sends delete +---------------------- + +Typically, backspace is ``^H`` (8, or 0x08) and delete is ^? (127, or 0x7f). + +On some systems however, the key for backspace is actually labeled and +transmitted as "delete", though its function in the operating system behaves +just as backspace. + +It is highly recommend to accept **both** ``KEY_DELETE`` and ``KEY_BACKSPACE`` +as having the same meaning except when implementing full screen editors, +and provide a choice to enable the delete mode by configuration. + +The misnomer of ANSI +-------------------- + +When people say 'ANSI Sequence', they are discussing: + +- Standard `ECMA-48`_: Control Functions for Coded Character Sets + +- `ANSI X3.64 `_ from + 1981, when the `American National Standards Institute + `_ adopted the `ECMA-48`_ as standard, which was later + withdrawn in 1997 (so in this sense it is *not* an ANSI standard). + +- The `ANSI.SYS`_ driver provided in MS-DOS and + clones. The popularity of the IBM Personal Computer and MS-DOS of the era, + and its ability to display colored text further populated the idea that such + text "is ANSI". + +- The various code pages used in MS-DOS Personal Computers, + providing "block art" characters in the 8th bit (int 127-255), paired + with `ECMA-48`_ sequences supported by the MS-DOS `ANSI.SYS`_ driver + to create artwork, known as `ANSI art `_. + +- The ANSI terminal database entry and its many descendants in the + `terminfo database + `_. This is mostly + due to terminals compatible with SCO UNIX, which was the successor of + Microsoft's Xenix, which brought some semblance of the Microsoft DOS + `ANSI.SYS`_ driver capabilities. + +- `Select Graphics Rendition (SGR) `_ + on vt100 clones, which include many of the common sequences in `ECMA-48`_. + +- Any sequence started by the `Control-Sequence-Inducer`_ is often + mistakenly termed as an "ANSI Escape Sequence" though not appearing in + `ECMA-48`_ or interpreted by the `ANSI.SYS`_ driver. The adjoining phrase + "Escape Sequence" is so termed because it follows the ASCII character + for the escape key (ESC, ``\x1b``). + +.. _code page: https://en.wikipedia.org/wiki/Code_page +.. _IBM CP437: https://en.wikipedia.org/wiki/Code_page_437 +.. _CGA Color Palette: https://en.wikipedia.org/wiki/Color_Graphics_Adapter#With_an_RGBI_monitor +.. _f.lux: https://justgetflux.com/ +.. _ZX Spectrum: https://en.wikipedia.org/wiki/List_of_8-bit_computer_hardware_palettes#ZX_Spectrum +.. _Control-Sequence-Inducer: http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Controls-beginning-with-ESC +.. _resize.c: http://www.opensource.apple.com/source/X11apps/X11apps-13/xterm/xterm-207/resize.c +.. _ANSI.SYS: http://www.kegel.com/nansi/ +.. _ECMA-48: http://www.ecma-international.org/publications/standards/Ecma-048.htm diff --git a/docs/sphinxext/github.py b/docs/sphinxext/github.py new file mode 100644 index 00000000..32fc4542 --- /dev/null +++ b/docs/sphinxext/github.py @@ -0,0 +1,155 @@ +"""Define text roles for GitHub + +* ghissue - Issue +* ghpull - Pull Request +* ghuser - User + +Adapted from bitbucket example here: +https://bitbucket.org/birkenfeld/sphinx-contrib/src/tip/bitbucket/sphinxcontrib/bitbucket.py + +Authors +------- + +* Doug Hellmann +* Min RK +""" +# +# Original Copyright (c) 2010 Doug Hellmann. All rights reserved. +# + +from docutils import nodes, utils +from docutils.parsers.rst.roles import set_classes + +def make_link_node(rawtext, app, type, slug, options): + """Create a link to a github resource. + + :param rawtext: Text being replaced with link node. + :param app: Sphinx application context + :param type: Link type (issues, changeset, etc.) + :param slug: ID of the thing to link to + :param options: Options dictionary passed to role func. + """ + + try: + base = app.config.github_project_url + if not base: + raise AttributeError + if not base.endswith('/'): + base += '/' + except AttributeError as err: + raise ValueError('github_project_url configuration value is not set (%s)' % str(err)) + + ref = base + type + '/' + slug + '/' + set_classes(options) + prefix = "#" + if type == 'pull': + prefix = "PR " + prefix + node = nodes.reference(rawtext, prefix + utils.unescape(slug), refuri=ref, + **options) + return node + +def ghissue_role(name, rawtext, text, lineno, inliner, options={}, content=[]): + """Link to a GitHub issue. + + Returns 2 part tuple containing list of nodes to insert into the + document and a list of system messages. Both are allowed to be + empty. + + :param name: The role name used in the document. + :param rawtext: The entire markup snippet, with role. + :param text: The text marked with the role. + :param lineno: The line number where rawtext appears in the input. + :param inliner: The inliner instance that called us. + :param options: Directive options for customization. + :param content: The directive content for customization. + """ + + try: + issue_num = int(text) + if issue_num <= 0: + raise ValueError + except ValueError: + msg = inliner.reporter.error( + 'GitHub issue number must be a number greater than or equal to 1; ' + '"%s" is invalid.' % text, line=lineno) + prb = inliner.problematic(rawtext, rawtext, msg) + return [prb], [msg] + app = inliner.document.settings.env.app + #app.info('issue %r' % text) + if 'pull' in name.lower(): + category = 'pull' + elif 'issue' in name.lower(): + category = 'issues' + else: + msg = inliner.reporter.error( + 'GitHub roles include "ghpull" and "ghissue", ' + '"%s" is invalid.' % name, line=lineno) + prb = inliner.problematic(rawtext, rawtext, msg) + return [prb], [msg] + node = make_link_node(rawtext, app, category, str(issue_num), options) + return [node], [] + +def ghuser_role(name, rawtext, text, lineno, inliner, options={}, content=[]): + """Link to a GitHub user. + + Returns 2 part tuple containing list of nodes to insert into the + document and a list of system messages. Both are allowed to be + empty. + + :param name: The role name used in the document. + :param rawtext: The entire markup snippet, with role. + :param text: The text marked with the role. + :param lineno: The line number where rawtext appears in the input. + :param inliner: The inliner instance that called us. + :param options: Directive options for customization. + :param content: The directive content for customization. + """ + app = inliner.document.settings.env.app + #app.info('user link %r' % text) + ref = 'https://github.com/' + text + node = nodes.reference(rawtext, text, refuri=ref, **options) + return [node], [] + +def ghcommit_role(name, rawtext, text, lineno, inliner, options={}, content=[]): + """Link to a GitHub commit. + + Returns 2 part tuple containing list of nodes to insert into the + document and a list of system messages. Both are allowed to be + empty. + + :param name: The role name used in the document. + :param rawtext: The entire markup snippet, with role. + :param text: The text marked with the role. + :param lineno: The line number where rawtext appears in the input. + :param inliner: The inliner instance that called us. + :param options: Directive options for customization. + :param content: The directive content for customization. + """ + app = inliner.document.settings.env.app + #app.info('user link %r' % text) + try: + base = app.config.github_project_url + if not base: + raise AttributeError + if not base.endswith('/'): + base += '/' + except AttributeError as err: + raise ValueError('github_project_url configuration value is not set (%s)' % str(err)) + + ref = base + text + node = nodes.reference(rawtext, text[:6], refuri=ref, **options) + return [node], [] + + +def setup(app): + """Install the plugin. + + :param app: Sphinx application context. + """ + app.info('Initializing GitHub plugin') + app.add_role('ghissue', ghissue_role) + app.add_role('ghpull', ghissue_role) + app.add_role('ghuser', ghuser_role) + app.add_role('ghcommit', ghcommit_role) + app.add_config_value('github_project_url', None, 'env') + return diff --git a/fabfile.py b/fabfile.py deleted file mode 100644 index 27b418e5..00000000 --- a/fabfile.py +++ /dev/null @@ -1,39 +0,0 @@ -"""Run this using ``fabric``. - -I can't remember any of this syntax on my own. - -""" -from functools import partial -from os import environ -from os.path import abspath, dirname - -from fabric.api import local, cd - - -local = partial(local, capture=False) - -ROOT = abspath(dirname(__file__)) - -environ['PYTHONPATH'] = (((environ['PYTHONPATH'] + ':') - if environ.get('PYTHONPATH') - else '') + ROOT) - - -def doc(kind='html'): - """Build Sphinx docs. - - Requires Sphinx to be installed. - - """ - with cd('docs'): - local('make clean %s' % kind) - - -def updoc(): - """Build Sphinx docs and upload them to packages.python.org. - - Requires Sphinx-PyPI-upload to be installed. - - """ - doc('html') - local('python setup.py upload_sphinx --upload-dir=docs/_build/html') diff --git a/requirements.txt b/requirements.txt index f05e0d33..10f88fa1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -wcwidth>=0.1.0 +wcwidth>=0.1.4 +six>=1.9.0 diff --git a/setup.py b/setup.py index 32ec3f91..496f1417 100755 --- a/setup.py +++ b/setup.py @@ -1,80 +1,71 @@ #!/usr/bin/env python -# std imports, -import subprocess -import sys +"""Distutils setup script.""" import os - -# 3rd-party import setuptools -import setuptools.command.develop -import setuptools.command.test -here = os.path.dirname(__file__) +def _get_install_requires(fname): + import sys + result = [req_line.strip() for req_line in open(fname) + if req_line.strip() and not req_line.startswith('#')] + + # support python2.6 by using backport of 'orderedict' + if sys.version_info < (2, 7): + result.append('ordereddict==1.1') -class SetupDevelop(setuptools.command.develop.develop): - def run(self): - # ensure a virtualenv is loaded, - assert os.getenv('VIRTUAL_ENV'), 'You should be in a virtualenv' - # ensure tox is installed - subprocess.check_call(('pip', 'install', 'tox', 'ipython')) - # install development egg-link - setuptools.command.develop.develop.run(self) + return result -class SetupTest(setuptools.command.test.test): - def run(self): - self.spawn(('tox',)) +def _get_version(fname): + import json + return json.load(open(fname, 'r'))['version'] -def main(): - extra = { - 'install_requires': [ - 'wcwidth>=0.1.0', - ] - } - if sys.version_info < (2, 7,): - extra['install_requires'].extend(['ordereddict>=1.1']) +def _get_long_description(fname): + import codecs + return codecs.open(fname, 'r', 'utf8').read() - setuptools.setup( - name='blessed', - version='1.9.5', - description="A feature-filled fork of Erik Rose's blessings project", - long_description=open(os.path.join(here, 'README.rst')).read(), - author='Jeff Quast', - author_email='contact@jeffquast.com', - license='MIT', - packages=['blessed', 'blessed.tests'], - url='https://github.com/jquast/blessed', - include_package_data=True, - test_suite='blessed.tests', - classifiers=[ - 'Intended Audience :: Developers', - 'Natural Language :: English', - 'Development Status :: 5 - Production/Stable', - 'Environment :: Console', - 'Environment :: Console :: Curses', - 'License :: OSI Approved :: MIT License', - 'Operating System :: POSIX', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.2', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Topic :: Software Development :: Libraries', - 'Topic :: Software Development :: User Interfaces', - 'Topic :: Terminals' - ], - keywords=['terminal', 'sequences', 'tty', 'curses', 'ncurses', - 'formatting', 'style', 'color', 'console', 'keyboard', - 'ansi', 'xterm'], - cmdclass={'develop': SetupDevelop, - 'test': SetupTest}, - zip_safe=True, - **extra - ) +HERE = os.path.dirname(__file__) -if __name__ == '__main__': - main() +setuptools.setup( + name='blessed', + version=_get_version( + fname=os.path.join(HERE, 'version.json')), + install_requires=_get_install_requires( + fname=os.path.join(HERE, 'requirements.txt')), + long_description='{0}\n\n{1}'.format( + _get_long_description(os.path.join(HERE, 'docs', 'intro.rst')), + _get_long_description(os.path.join(HERE, 'docs', 'history.rst')), + ), + description=('A thin, practical wrapper around terminal styling, ' + 'screen positioning, and keyboard input.'), + author='Jeff Quast, Erik Rose', + author_email='contact@jeffquast.com', + license='MIT', + packages=['blessed', 'blessed.tests'], + url='https://github.com/erikrose/blessed', + include_package_data=True, + zip_safe=True, + classifiers=[ + 'Intended Audience :: Developers', + 'Natural Language :: English', + 'Development Status :: 5 - Production/Stable', + 'Environment :: Console', + 'Environment :: Console :: Curses', + 'License :: OSI Approved :: MIT License', + 'Operating System :: POSIX', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Topic :: Software Development :: Libraries', + 'Topic :: Software Development :: User Interfaces', + 'Topic :: Terminals' + ], + keywords=['terminal', 'sequences', 'tty', 'curses', 'ncurses', + 'formatting', 'style', 'color', 'console', 'keyboard', + 'ansi', 'xterm'], +) diff --git a/tools/display-sighandlers.py b/tools/display-sighandlers.py index f3559f72..94832ae0 100755 --- a/tools/display-sighandlers.py +++ b/tools/display-sighandlers.py @@ -1,24 +1,32 @@ #!/usr/bin/env python -# Displays all signals, their values, and their handlers. +"""Displays all signals, their values, and their handlers to stdout.""" +# pylint: disable=invalid-name +# Invalid module name "display-sighandlers" from __future__ import print_function import signal -FMT = '{name:<10} {value:<5} {description}' -# header -print(FMT.format(name='name', value='value', description='description')) -print('-' * (33)) +def main(): + """Program entry point.""" + fmt = '{name:<10} {value:<5} {description}' -for name, value in [(signal_name, getattr(signal, signal_name)) - for signal_name in dir(signal) - if signal_name.startswith('SIG') - and not signal_name.startswith('SIG_')]: - try: - handler = signal.getsignal(value) - except ValueError: - # FreeBSD: signal number out of range - handler = 'out of range' - description = { - signal.SIG_IGN: "ignored(SIG_IGN)", - signal.SIG_DFL: "default(SIG_DFL)" - }.get(handler, handler) - print(FMT.format(name=name, value=value, description=description)) + # header + print(fmt.format(name='name', value='value', description='description')) + print('-' * (33)) + + for name, value in [(signal_name, getattr(signal, signal_name)) + for signal_name in dir(signal) + if signal_name.startswith('SIG') + and not signal_name.startswith('SIG_')]: + try: + handler = signal.getsignal(value) + except ValueError: + # FreeBSD: signal number out of range + handler = 'out of range' + description = { + signal.SIG_IGN: "ignored(SIG_IGN)", + signal.SIG_DFL: "default(SIG_DFL)" + }.get(handler, handler) + print(fmt.format(name=name, value=value, description=description)) + +if __name__ == '__main__': + main() diff --git a/tools/display-terminalinfo.py b/tools/display-terminalinfo.py index 15911d41..0d8cc46d 100755 --- a/tools/display-terminalinfo.py +++ b/tools/display-terminalinfo.py @@ -1,5 +1,7 @@ #!/usr/bin/env python -""" Display known information about our terminal. """ +"""Display known information about our terminal.""" +# pylint: disable=invalid-name +# Invalid module name "display-terminalinfo" from __future__ import print_function import termios import locale @@ -94,11 +96,11 @@ def display_bitmask(kind, bitmap, value): - """ Display all matching bitmask values for ``value`` given ``bitmap``. """ + """Display all matching bitmask values for ``value`` given ``bitmap``.""" col1_width = max(map(len, list(bitmap.keys()) + [kind])) col2_width = 7 - FMT = '{name:>{col1_width}} {value:>{col2_width}} {description}' - print(FMT.format(name=kind, + fmt = '{name:>{col1_width}} {value:>{col2_width}} {description}' + print(fmt.format(name=kind, value='Value', description='Description', col1_width=col1_width, @@ -112,7 +114,7 @@ def display_bitmask(kind, bitmap, value): bit_val = 'on' if bool(value & bitmask) else 'off' except AttributeError: bit_val = 'undef' - print(FMT.format(name=flag_name, + print(fmt.format(name=flag_name, value=bit_val, description=description, col1_width=col1_width, @@ -120,14 +122,14 @@ def display_bitmask(kind, bitmap, value): print() -def display_ctl_chars(index, cc): - """ Display all control character indicies, names, and values. """ +def display_ctl_chars(index, ctlc): + """Display all control character indicies, names, and values.""" title = 'Special Character' col1_width = len(title) col2_width = max(map(len, index.values())) - FMT = '{idx:<{col1_width}} {name:<{col2_width}} {value}' + fmt = '{idx:<{col1_width}} {name:<{col2_width}} {value}' print('Special line Characters'.center(40).rstrip()) - print(FMT.format(idx='Index', + print(fmt.format(idx='Index', name='Name', value='Value', col1_width=col1_width, @@ -138,14 +140,14 @@ def display_ctl_chars(index, cc): for index_name, name in index.items(): try: index = getattr(termios, index_name) - value = cc[index] + value = ctlc[index] if value == b'\xff': value = '_POSIX_VDISABLE' else: value = repr(value) except AttributeError: value = 'undef' - print(FMT.format(idx=index_name, + print(fmt.format(idx=index_name, name=name, value=value, col1_width=col1_width, @@ -154,9 +156,10 @@ def display_ctl_chars(index, cc): def display_conf(kind, names, getter): + """Helper displays results of os.pathconf_names values.""" col1_width = max(map(len, names)) - FMT = '{name:>{col1_width}} {value}' - print(FMT.format(name=kind, + fmt = '{name:>{col1_width}} {value}' + print(fmt.format(name=kind, value='value', col1_width=col1_width)) print('{0} {1}'.format('-' * col1_width, '-' * 27)) @@ -165,11 +168,12 @@ def display_conf(kind, names, getter): value = getter(name) except OSError as err: value = err - print(FMT.format(name=name, value=value, col1_width=col1_width)) + print(fmt.format(name=name, value=value, col1_width=col1_width)) print() def main(): + """Program entry point.""" fd = sys.stdin.fileno() locale.setlocale(locale.LC_ALL, '') encoding = locale.getpreferredencoding() @@ -182,8 +186,9 @@ def main(): getter=lambda name: os.fpathconf(fd, name)) try: - (iflag, oflag, cflag, lflag, ispeed, ospeed, cc - ) = termios.tcgetattr(fd) + (iflag, oflag, cflag, lflag, + _, _, # input / output speed (bps macros) + ctlc) = termios.tcgetattr(fd) except termios.error as err: print('stdin is not a typewriter: {0}'.format(err)) else: @@ -200,7 +205,7 @@ def main(): bitmap=BITMAP_LFLAG, value=lflag) display_ctl_chars(index=CTLCHAR_INDEX, - cc=cc) + ctlc=ctlc) print('os.ttyname({0}) => {1}'.format(fd, os.ttyname(fd))) print('os.ctermid() => {0}'.format(os.ttyname(fd))) diff --git a/tools/teamcity-runtests.sh b/tools/teamcity-runtests.sh index 07b06e9b..15b8e697 100755 --- a/tools/teamcity-runtests.sh +++ b/tools/teamcity-runtests.sh @@ -1,7 +1,4 @@ #!/bin/bash -# -# This script assumes that the project 'ptyprocess' is -# available in the parent of the project's folder. set -e set -o pipefail @@ -17,7 +14,7 @@ if [ X"$osrel" == X"Linux" ]; then # cannot create a virtualenv for python2.6 due to use of # "{}".format in virtualenv, throws exception # ValueError: zero length field name in format. - _cmd='tox -epy27,py33,py34,pypy' + _cmd='tox -epy27,py33,py34,docs,sa' fi ret=0 diff --git a/tox.ini b/tox.ini index adc52445..de393948 100644 --- a/tox.ini +++ b/tox.ini @@ -1,35 +1,79 @@ [tox] -envlist = static_analysis, - py26, - py27, - py33, - py34, - pypy - +envlist = sa, docs, py{26,27,33,34,35} skip_missing_interpreters = true [testenv] -whitelist_externals = /bin/bash /bin/mv +whitelist_externals = /bin/mv setenv = PYTHONIOENCODING=UTF8 -deps = pytest-cov +deps = pytest-xdist + pytest-cov pytest mock + +# note: run a specific py.testcase, +# +# $ tox -epy35 -- -k core --looponfail +# commands = {envbindir}/py.test \ - --strict \ - --junit-xml=results.{envname}.xml --verbose \ - --cov blessed blessed/tests --cov-report=term-missing \ + --strict --verbose --verbose --color=yes \ + --junit-xml=results.{envname}.xml \ + --cov blessed --cov-report=term-missing \ + blessed/tests \ {posargs} /bin/mv {toxinidir}/.coverage {toxinidir}/.coverage.{envname} -[testenv:static_analysis] -deps = prospector[with_everything] -commands = prospector \ +[testenv:sa] +# Instead of trusting whichever developer's environment python is used to +# invoke tox, explicitly define python2.7 because the 'doc8' tool does not +# appear to be python3-compatible: https://github.com/stackforge/doc8/commit/4d82c269ab46f0c5370c1f00be06e0c406164e85#commitcomment-10725927 +basepython=python2.7 +deps = prospector[with_frosted,with_pyroma] + restructuredtext_lint + doc8 + +# - prospector is configured using .prospector.yaml, and wraps several +# static analysis/linting and style-checker tools. +# - rst-lint ensures that README.rst will present correctly on pypi. +# - doc8 is like pep8 for rst documents. Namely, enforcing styling. +# ignore docs/further.rst:21: D000 Bullet list ends without a blank line; +# unexpected unindent. This is a tool error +commands = {envbindir}/prospector \ --die-on-tool-error \ - --doc-warnings \ + --zero-exit \ {toxinidir} + {envbindir}/rst-lint README.rst + {envbindir}/doc8 --ignore-path docs/_build --ignore D000 docs + +[testenv:docs] +whitelist_externals=echo +basepython=python3.4 +deps=sphinx + sphinx_rtd_theme + sphinx-paramlinks + +recreate = {env:RECREATE:false} +chklinks= {env:-blinkcheck + +# note: to verify URL links, +# +# $ tox -edocs -- -blinkcheck +# +commands = {envbindir}/sphinx-build -v -W -b html \ + -d {toxinidir}/docs/_build/doctrees \ + {posargs} docs \ + {toxinidir}/docs/_build/html + echo "open {toxinidir}/docs/_build/html/index.html for review" + +[testenv:linkcheck] +deps=sphinx + sphinx_rtd_theme + sphinx-paramlinks + +commands = {envbindir}/sphinx-build -v -W -b html \ + -d {toxinidir}/docs/_build/doctrees \ + docs \ + {toxinidir}/docs/_build/html + echo "open {toxinidir}/docs/_build/html/index.html for review" [pytest] -# py.test fixtures conflict with pyflakes -flakes-ignore = - UnusedImport - RedefinedWhileUnused +looponfailroots = blessed diff --git a/version.json b/version.json new file mode 100644 index 00000000..f828972f --- /dev/null +++ b/version.json @@ -0,0 +1 @@ +{"version": "1.9.6"} From 47526dcc771a3234c635e6753a4da743be13641b Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 2 Oct 2015 14:12:24 -0700 Subject: [PATCH 267/459] bugfix `` marker --- docs/history.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/history.rst b/docs/history.rst index 00dbd3de..91a3a587 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -83,7 +83,7 @@ Version History :attr:`~.does_styling` is ``False``. Resolves issue where piping tests output would fail. * bugfix: warn and set :attr:`~.does_styling` to ``False`` when the given - :attr:`~.kind`` is not found in the terminal capability database. + :attr:`~.kind` is not found in the terminal capability database. * bugfix: allow unsupported terminal capabilities to be callable just as supported capabilities, so that the return value of :attr:`~.color`\(n) may be called on terminals without color From a3f8881e6c9aa65be2f6835007d77e242658772f Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 2 Oct 2015 14:22:19 -0700 Subject: [PATCH 268/459] bugfix length of '\x1b[m' ('normal') --- blessed/sequences.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blessed/sequences.py b/blessed/sequences.py index 3c8ab0c8..cde3dd6e 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -340,7 +340,7 @@ def init_sequence_patterns(term): re.escape(u'\x1b') + r'\[(\d+)\;(\d+)\;(\d+)\;(\d+)m', re.escape(u'\x1b') + r'\[(\d+)\;(\d+)\;(\d+)m', re.escape(u'\x1b') + r'\[(\d+)\;(\d+)m', - re.escape(u'\x1b') + r'\[(\d+)m', + re.escape(u'\x1b') + r'\[(\d+)?m', re.escape(u'\x1b(B'), ] From 18da49dc2d99860b0683a6c44a0f5ef097d1510d Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 2 Oct 2015 15:53:19 -0700 Subject: [PATCH 269/459] integrate display-{sighandlers,terminalinfo} in CI --- {tools => bin}/display-sighandlers.py | 0 {tools => bin}/display-terminalinfo.py | 20 +++++++++----------- tox.ini | 18 +++++++----------- 3 files changed, 16 insertions(+), 22 deletions(-) rename {tools => bin}/display-sighandlers.py (100%) rename {tools => bin}/display-terminalinfo.py (93%) diff --git a/tools/display-sighandlers.py b/bin/display-sighandlers.py similarity index 100% rename from tools/display-sighandlers.py rename to bin/display-sighandlers.py diff --git a/tools/display-terminalinfo.py b/bin/display-terminalinfo.py similarity index 93% rename from tools/display-terminalinfo.py rename to bin/display-terminalinfo.py index 0d8cc46d..f8da0896 100755 --- a/tools/display-terminalinfo.py +++ b/bin/display-terminalinfo.py @@ -155,14 +155,13 @@ def display_ctl_chars(index, ctlc): print() -def display_conf(kind, names, getter): +def display_pathconf(names, getter): """Helper displays results of os.pathconf_names values.""" col1_width = max(map(len, names)) - fmt = '{name:>{col1_width}} {value}' - print(fmt.format(name=kind, - value='value', + fmt = '{name:>{col1_width}} {value}' + print(fmt.format(name='pathconf'.ljust(col1_width), value='value', col1_width=col1_width)) - print('{0} {1}'.format('-' * col1_width, '-' * 27)) + print('{0} {1}'.format('-' * col1_width, '-' * 27)) for name in names: try: value = getter(name) @@ -181,9 +180,8 @@ def main(): print('os.isatty({0}) => {1}'.format(fd, os.isatty(fd))) print('locale.getpreferredencoding() => {0}'.format(encoding)) - display_conf(kind='pathconf', - names=os.pathconf_names, - getter=lambda name: os.fpathconf(fd, name)) + display_pathconf(names=os.pathconf_names, + getter=lambda name: os.fpathconf(fd, name)) try: (iflag, oflag, cflag, lflag, @@ -192,16 +190,16 @@ def main(): except termios.error as err: print('stdin is not a typewriter: {0}'.format(err)) else: - display_bitmask(kind='Input Mode', + display_bitmask(kind=' Input Mode', bitmap=BITMAP_IFLAG, value=iflag) - display_bitmask(kind='Output Mode', + display_bitmask(kind=' Output Mode', bitmap=BITMAP_OFLAG, value=oflag) display_bitmask(kind='Control Mode', bitmap=BITMAP_CFLAG, value=cflag) - display_bitmask(kind='Local Mode', + display_bitmask(kind=' Local Mode', bitmap=BITMAP_LFLAG, value=lflag) display_ctl_chars(index=CTLCHAR_INDEX, diff --git a/tox.ini b/tox.ini index de393948..6d228970 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = sa, docs, py{26,27,33,34,35} +envlist = about, sa, docs, py{26,27,33,34,35} skip_missing_interpreters = true [testenv] @@ -10,11 +10,9 @@ deps = pytest-xdist pytest mock -# note: run a specific py.testcase, -# -# $ tox -epy35 -- -k core --looponfail -# -commands = {envbindir}/py.test \ +commands = python {toxinidir}/bin/display-sighandlers.py + python {toxinidir}/bin/display-terminalinfo.py + {envbindir}/py.test \ --strict --verbose --verbose --color=yes \ --junit-xml=results.{envname}.xml \ --cov blessed --cov-report=term-missing \ @@ -31,15 +29,16 @@ deps = prospector[with_frosted,with_pyroma] restructuredtext_lint doc8 +# - ensure all python files compile ! # - prospector is configured using .prospector.yaml, and wraps several # static analysis/linting and style-checker tools. # - rst-lint ensures that README.rst will present correctly on pypi. # - doc8 is like pep8 for rst documents. Namely, enforcing styling. # ignore docs/further.rst:21: D000 Bullet list ends without a blank line; # unexpected unindent. This is a tool error -commands = {envbindir}/prospector \ +commands = python -m compileall -fq {toxinidir}/blessed + {envbindir}/prospector \ --die-on-tool-error \ - --zero-exit \ {toxinidir} {envbindir}/rst-lint README.rst {envbindir}/doc8 --ignore-path docs/_build --ignore D000 docs @@ -51,9 +50,6 @@ deps=sphinx sphinx_rtd_theme sphinx-paramlinks -recreate = {env:RECREATE:false} -chklinks= {env:-blinkcheck - # note: to verify URL links, # # $ tox -edocs -- -blinkcheck From 143f154624a8a1f00eeec1d8325bb4ed6df73cde Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 2 Oct 2015 15:53:52 -0700 Subject: [PATCH 270/459] static analysis: ignore PYR15 --- .landscape.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.landscape.yml b/.landscape.yml index 8d52777d..ce4e063f 100644 --- a/.landscape.yml +++ b/.landscape.yml @@ -75,6 +75,15 @@ pylint: pyroma: # checks setup.py run: true + disable: + # > Setuptools and Distribute support running tests. By specifying a test + # > suite, it's easy to find and run tests both for automated tools and humans. + # + # https://pytest.org/latest/goodpractises.html#integration-with-setuptools-test-commands + # > Most often it is better to use tox instead + # + # we agree. + - 'PYR15' vulture: # this tool does a good job of finding unused code. From 11d5c624f4271d260b0aa7df05116c20522ab54f Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 2 Oct 2015 15:54:05 -0700 Subject: [PATCH 271/459] note about --looponfail --- CONTRIBUTING.rst | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 2d3cfc1f..fbb56b0f 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -10,12 +10,14 @@ We welcome contributions via GitHub pull requests: Developing ---------- -Install git, Python 2 and 3, and pip. - -Then, from the blessed code folder:: +Prepare a developer environment. Then, from the blessed code folder:: pip install --editable . +Any changes made are automatically made available to the python interpreter +matching pip as the 'blessed' module path irregardless of the current working +directory. + Running Tests ~~~~~~~~~~~~~ @@ -26,17 +28,35 @@ Install and run tox pip install --upgrade tox tox +Py.test is used as the test runner, supporting positional arguments, you may +for example use `looponfailing +` +with python 3.5, stopping at the first failing test case, and looping +(retrying) after a filesystem save is detected:: + + tox -epy35 -- -fx + + Test Coverage ~~~~~~~~~~~~~ -Blessed has 99% code coverage, and we'd like to keep it that way, as -terminals are fiddly beasts. Thus, when you contribute a new feature, make -sure it is covered by tests. Likewise, a bug fix should include a test -demonstrating the bug. +When you contribute a new feature, make sure it is covered by tests. +Likewise, a bug fix should include a test demonstrating the bug. Blessed has +nearly 100% line coverage, with roughly 1/2 of the codebase in the form of +tests, which are further combined by a matrix of varying ``TERM`` types, +providing plenty of existing test cases to augment or duplicate in your +favor. Style and Static Analysis ~~~~~~~~~~~~~~~~~~~~~~~~~ The test runner (``tox``) ensures all code and documentation complies with standard python style guides, pep8 and pep257, as well as various -static analysis tools. +static analysis tools through the **sa** target, invoked using:: + + tox -esa + +All standards enforced by the underlying tools are adhered to by the blessed +project, with the declarative exception of those found in `landscape.yml +`_, or inline +using ``pylint: disable=`` directives. From 8b39cc6627a42e79d414621bb225f46a3d10b4b8 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 2 Oct 2015 15:54:39 -0700 Subject: [PATCH 272/459] use SVG versions of some badges --- docs/intro.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/intro.rst b/docs/intro.rst index 1cd10d0c..59f5eebf 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -6,7 +6,7 @@ :alt: TeamCity Build status :target: https://teamcity-master.pexpect.org/viewType.html?buildTypeId=Blessed_BuildHead&branch_Blessed=%3Cdefault%3E&tab=buildTypeStatusDiv -.. image:: https://coveralls.io/repos/jquast/blessed/badge.png?branch=master +.. image:: https://img.shields.io/coveralls/jekyll/jekyll/master.svg :alt: Coveralls Code Coverage :target: https://coveralls.io/r/jquast/blessed?branch=master @@ -14,7 +14,7 @@ :alt: Latest Version :target: https://pypi.python.org/pypi/blessed -.. image:: https://pypip.in/license/blessed/badge.svg +.. image:: https://img.shields.io/github/license/jquast/blessed.svg :alt: License :target: http://opensource.org/licenses/MIT From 938e1362592dcd356ed84f136b9a0afb96a22e2f Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 2 Oct 2015 15:55:26 -0700 Subject: [PATCH 273/459] workaround: provide ``sc`` and ``rc`` for 'ansi' closes #44, prepares for 1.9.10 release --- blessed/formatters.py | 7 ++- blessed/sequences.py | 8 ++-- blessed/tests/test_length_sequence.py | 10 +++++ blessed/tests/test_sequences.py | 64 +++++++++++++++++---------- docs/history.rst | 13 ++++-- version.json | 2 +- 6 files changed, 69 insertions(+), 35 deletions(-) diff --git a/blessed/formatters.py b/blessed/formatters.py index df7bf531..bfaa84bc 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -178,14 +178,15 @@ def get_proxy_string(term, attr): if term.kind.startswith(_kind)), term) return { 'screen': { - # proxy move_x/move_y for 'screen' terminal type. + # proxy move_x/move_y for 'screen' terminal type, used by tmux(1). 'hpa': ParameterizingProxyString( (u'\x1b[{0}G', lambda *arg: (arg[0] + 1,)), term.normal, attr), 'vpa': ParameterizingProxyString( (u'\x1b[{0}d', lambda *arg: (arg[0] + 1,)), term.normal, attr), }, 'ansi': { - # proxy show/hide cursor for 'ansi' terminal type. + # proxy show/hide cursor for 'ansi' terminal type. There is some + # demand for a richly working ANSI terminal type for some reason. 'civis': ParameterizingProxyString( (u'\x1b[?25l', lambda *arg: ()), term.normal, attr), 'cnorm': ParameterizingProxyString( @@ -194,6 +195,8 @@ def get_proxy_string(term, attr): (u'\x1b[{0}G', lambda *arg: (arg[0] + 1,)), term.normal, attr), 'vpa': ParameterizingProxyString( (u'\x1b[{0}d', lambda *arg: (arg[0] + 1,)), term.normal, attr), + 'sc': '\x1b[s', + 'rc': '\x1b[u', } }.get(term_kind, {}).get(attr, None) diff --git a/blessed/sequences.py b/blessed/sequences.py index cde3dd6e..393976a1 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -109,7 +109,7 @@ def get_movement_sequence_patterns(term): """ bnc = functools.partial(_build_numeric_capability, term) - return set([ + return set(filter(None, [ # carriage_return re.escape(term.cr), # column_address: Horizontal position, absolute @@ -141,7 +141,7 @@ def get_movement_sequence_patterns(term): term._cuf, # backward cursor term._cub, - ]) + ])) def get_wontmove_sequence_patterns(term): @@ -156,7 +156,7 @@ def get_wontmove_sequence_patterns(term): # pylint: disable=bad-builtin # Used builtin function 'map' - return list([ + return set(filter(None, [ # print_screen: Print contents of screen re.escape(term.mc0), # prtr_off: Turn off printer @@ -266,7 +266,7 @@ def get_wontmove_sequence_patterns(term): # ( not *exactly* legal, being extra forgiving. ) bna(cap='sgr', nparams=_num) for _num in range(1, 10) # reset_{1,2,3}string: Reset string - ] + list(map(re.escape, (term.r1, term.r2, term.r3,)))) + ] + list(map(re.escape, (term.r1, term.r2, term.r3,))))) def init_sequence_patterns(term): diff --git a/blessed/tests/test_length_sequence.py b/blessed/tests/test_length_sequence.py index b096aeb4..2c0c553b 100644 --- a/blessed/tests/test_length_sequence.py +++ b/blessed/tests/test_length_sequence.py @@ -340,3 +340,13 @@ def child_mnemonics_willmove(kind): measure_length(t.clear, t)) child_mnemonics_willmove(all_terms) + + +def test_foreign_sequences(): + """Test parsers about sequences received from foreign sources.""" + @as_subprocess + def child(kind): + from blessed.sequences import measure_length + t = TestTerminal(kind=kind) + assert measure_length(u'\x1b[m', t) == len('\x1b[m') + child(kind='ansi') diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index 3ed8ba9b..1f60a4de 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -157,9 +157,10 @@ def child_with_styling(kind): with t.location(3, 4): t.stream.write(u'hi') expected_output = u''.join( - (unicode_cap('sc'), + (unicode_cap('sc') or u'\x1b[s', unicode_parm('cup', 4, 3), - u'hi', unicode_cap('rc'))) + u'hi', + unicode_cap('rc') or u'\x1b[u')) assert (t.stream.getvalue() == expected_output) child_with_styling(all_terms) @@ -187,16 +188,18 @@ def child(kind): t = TestTerminal(kind=kind, stream=six.StringIO(), force_styling=True) with t.location(x=5): pass + _hpa = unicode_parm('hpa', 5) + if not _hpa and (kind.startswith('screen') or + kind.startswith('ansi')): + _hpa = u'\x1b[6G' expected_output = u''.join( - (unicode_cap('sc'), - unicode_parm('hpa', 5), - unicode_cap('rc'))) + (unicode_cap('sc') or u'\x1b[s', + _hpa, + unicode_cap('rc') or u'\x1b[u')) assert (t.stream.getvalue() == expected_output), ( repr(t.stream.getvalue()), repr(expected_output)) - # skip 'screen', 'ansi': hpa is proxied (see later tests) - if all_terms not in ('screen', 'ansi'): - child(all_terms) + child(all_terms) def test_vertical_location(all_terms): @@ -206,15 +209,18 @@ def child(kind): t = TestTerminal(kind=kind, stream=six.StringIO(), force_styling=True) with t.location(y=5): pass + _vpa = unicode_parm('vpa', 5) + if not _vpa and (kind.startswith('screen') or + kind.startswith('ansi')): + _vpa = u'\x1b[6d' + expected_output = u''.join( - (unicode_cap('sc'), - unicode_parm('vpa', 5), - unicode_cap('rc'))) + (unicode_cap('sc') or u'\x1b[s', + _vpa, + unicode_cap('rc') or u'\x1b[u')) assert (t.stream.getvalue() == expected_output) - # skip 'screen', vpa is proxied (see later tests) - if all_terms not in ('screen', 'ansi'): - child(all_terms) + child(all_terms) def test_inject_move_x(): @@ -226,9 +232,9 @@ def child(kind): with t.location(x=COL): pass expected_output = u''.join( - (unicode_cap('sc'), + (unicode_cap('sc') or u'\x1b[s', u'\x1b[{0}G'.format(COL + 1), - unicode_cap('rc'))) + unicode_cap('rc') or u'\x1b[u')) assert (t.stream.getvalue() == expected_output) assert (t.move_x(COL) == u'\x1b[{0}G'.format(COL + 1)) @@ -246,9 +252,9 @@ def child(kind): with t.location(y=ROW): pass expected_output = u''.join( - (unicode_cap('sc'), + (unicode_cap('sc') or u'\x1b[s', u'\x1b[{0}d'.format(ROW + 1), - unicode_cap('rc'))) + unicode_cap('rc') or u'\x1b[u')) assert (t.stream.getvalue() == expected_output) assert (t.move_y(ROW) == u'\x1b[{0}d'.format(ROW + 1)) @@ -264,10 +270,20 @@ def child(kind): t = TestTerminal(kind=kind, stream=six.StringIO(), force_styling=True) with t.hidden_cursor(): pass - expected_output = u''.join( - (unicode_cap('sc'), - u'\x1b[?25l\x1b[?25h', - unicode_cap('rc'))) + expected_output = u'\x1b[?25l\x1b[?25h' + assert (t.stream.getvalue() == expected_output) + + child('ansi') + + +def test_inject_sc_and_rc_for_ansi(): + """Test injection of sc and rc (save and restore cursor) for ansi.""" + @as_subprocess + def child(kind): + t = TestTerminal(kind=kind, stream=six.StringIO(), force_styling=True) + with t.location(): + pass + expected_output = u'\x1b[s\x1b[u' assert (t.stream.getvalue() == expected_output) child('ansi') @@ -281,9 +297,9 @@ def child(kind): with t.location(0, 0): pass expected_output = u''.join( - (unicode_cap('sc'), + (unicode_cap('sc') or u'\x1b[s', unicode_parm('cup', 0, 0), - unicode_cap('rc'))) + unicode_cap('rc') or u'\x1b[u')) assert (t.stream.getvalue() == expected_output) child(all_terms) diff --git a/docs/history.rst b/docs/history.rst index 91a3a587..c2ba17f2 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -1,5 +1,14 @@ Version History =============== +1.10 + * workaround: provide ``sc`` and ``rc`` for Terminals of ``kind='ansi'``, + repairing :meth:`~.Terminal.location` :ghissue:`44`. + * bugfix: length of simple SGR reset sequence ``\x1b[m`` was not correctly + determined on all terminal types, :ghissue:`45`. + * deprecated: ``_intr_continue`` arguments introduced in 1.8 are now marked + deprecated in 1.10: beginning with python 3.5, the default behavior is as + though this argument is always True, `PEP-475 + `_, blessed does the same. 1.9 * enhancement: :paramref:`~.Terminal.wrap.break_long_words` now supported by @@ -18,10 +27,6 @@ Version History * enhancement: new public attribute: :attr:`~.kind`: the very same as given :paramref:`Terminal.__init__.kind` keyword argument. Or, when not given, determined by and equivalent to the ``TERM`` Environment variable. - * deprecated: ``_intr_continue`` arguments introduced in 1.8 are now marked - deprecated in 1.9.6: beginning with python 3.5, the default behavior is as - though this argument is always True, `PEP-475 - `_. 1.8 * enhancement: export keyboard-read function as public method ``getch()``, diff --git a/version.json b/version.json index f828972f..4d232ac3 100644 --- a/version.json +++ b/version.json @@ -1 +1 @@ -{"version": "1.9.6"} +{"version": "1.10.0"} From d39cf3002b61abcba3f4fbc640dc780ee80d97d6 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 2 Oct 2015 16:00:14 -0700 Subject: [PATCH 274/459] spellfix alt, remove license badge, times out --- docs/intro.rst | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/docs/intro.rst b/docs/intro.rst index 59f5eebf..3290c8fa 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -1,5 +1,5 @@ .. image:: https://img.shields.io/travis/jquast/blessed.svg - :alt: Travis Continous Integration + :alt: Travis Continuous Integration :target: https://travis-ci.org/jquast/blessed/ .. image:: https://img.shields.io/teamcity/http/teamcity-master.pexpect.org/s/Blessed_BuildHead.png @@ -14,10 +14,6 @@ :alt: Latest Version :target: https://pypi.python.org/pypi/blessed -.. image:: https://img.shields.io/github/license/jquast/blessed.svg - :alt: License - :target: http://opensource.org/licenses/MIT - .. image:: https://img.shields.io/pypi/dm/blessed.svg :alt: Downloads :target: https://pypi.python.org/pypi/blessed From 8d248fcf337ce4949ff6d752ffe687b84791fc20 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 2 Oct 2015 16:36:17 -0700 Subject: [PATCH 275/459] about fork in intro.rst --- docs/intro.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/intro.rst b/docs/intro.rst index 3290c8fa..8f4159c2 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -147,6 +147,16 @@ License Blessed is under the MIT License. See the LICENSE file. +Forked +------ + +Blessed is a fork of `blessings `_. +Changes since 1.7 have all been proposed but unaccepted upstream. + +Furthermore, a project in the node.js language of the `same name +`_ is **not** related, or a fork +of each other in any way. + .. _`issue tracker`: https://github.com/jquast/blessed/issues/ .. _curses: https://docs.python.org/3/library/curses.html .. _tigetstr: http://www.openbsd.org/cgi-bin/man.cgi/OpenBSD-current/man3/tigetstr.3 From 3a216899ff0957582104cc98911869f3bfb4225e Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 2 Oct 2015 18:09:59 -0700 Subject: [PATCH 276/459] fix coverage, add TEST_QUICK and TEST_FULL. hopefully this new coverage.Coverage() API is the last time we need to accomidate using coverage with custom fork! --- .travis.yml | 10 +++--- blessed/tests/accessories.py | 47 +++++++++++++------------ blessed/tests/test_keyboard.py | 24 +++++++++---- blessed/tests/test_wrap.py | 64 +++++++++++++++++++--------------- tox.ini | 2 ++ 5 files changed, 85 insertions(+), 62 deletions(-) diff --git a/.travis.yml b/.travis.yml index 64302e35..588b179d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,15 +12,15 @@ matrix: - env: TOXENV=sa - env: TOXENV=docs - python: 2.6 - env: TOXENV=py26 + env: TOXENV=py26 TEST_QUICK=1 - python: 2.7 - env: TOXENV=py27 + env: TOXENV=py27 TEST_QUICK=1 - python: 3.3 - env: TOXENV=py33 + env: TOXENV=py33 TEST_QUICK=1 - python: 3.4 - env: TOXENV=py34 + env: TOXENV=py34 TEST_QUICK=1 - python: 3.5 - env: TOXENV=py35 + env: TOXENV=py35 TEST_QUICK=1 notifications: email: diff --git a/blessed/tests/accessories.py b/blessed/tests/accessories.py index bea4095a..b04824ce 100644 --- a/blessed/tests/accessories.py +++ b/blessed/tests/accessories.py @@ -21,29 +21,31 @@ import pytest import six - TestTerminal = functools.partial(Terminal, kind='xterm-256color') SEND_SEMAPHORE = SEMAPHORE = b'SEMAPHORE\n' RECV_SEMAPHORE = b'SEMAPHORE\r\n' -all_xterms_params = ['xterm', 'xterm-256color'] -many_lines_params = [30, 100] -many_columns_params = [1, 10] -default_all_terms = ['screen', 'vt220', 'rxvt', 'cons25', 'linux', 'ansi'] -if os.environ.get('TEST_ALLTERMS'): +many_lines_params = [40, 80] +many_columns_params = [5, 25] + +if os.environ.get('TEST_QUICK'): + many_lines_params = [80,] + many_columns_params = [25,] + +all_terms_params = 'xterm screen ansi vt220 rxvt cons25 linux'.split() + +if os.environ.get('TEST_FULL'): try: - available_terms = [ + all_terms_params = [ + # use all values of the first column of data in output of 'toe -a' _term.split(None, 1)[0] for _term in subprocess.Popen(('toe', '-a'), stdout=subprocess.PIPE, close_fds=True) .communicate()[0].splitlines()] except OSError: - all_terms_params = default_all_terms -else: - available_terms = default_all_terms -all_terms_params = list(set(available_terms) - ( - set(BINARY_TERMINALS) if not os.environ.get('TEST_BINTERMS') - else set())) or default_all_terms + pass +elif os.environ.get('TEST_QUICK'): + all_terms_params = 'xterm screen ansi linux'.split() class as_subprocess(object): @@ -65,11 +67,14 @@ def __call__(self, *args, **kwargs): # if failed, causing a non-zero exit code, using the # protected _exit() function of ``os``; to prevent the # 'SystemExit' exception from being thrown. + cov = None + try: + import coverage + cov = coverage.Coverage(data_suffix=True) + cov.start() + except ImportError: + pass try: - try: - cov = __import__('cov_core_init').init() - except ImportError: - cov = None self.func(*args, **kwargs) except Exception: e_type, e_value, e_tb = sys.exc_info() @@ -94,10 +99,12 @@ def __call__(self, *args, **kwargs): cov.save() os._exit(0) + # detect rare fork in test runner, when bad bugs happen if pid_testrunner != os.getpid(): print('TEST RUNNER HAS FORKED, {0}=>{1}: EXIT' .format(pid_testrunner, os.getpid()), file=sys.stderr) os._exit(1) + exc_output = six.text_type() decoder = codecs.getincrementaldecoder(self.encoding)() while True: @@ -217,12 +224,6 @@ def unsupported_sequence_terminals(request): return request.param -@pytest.fixture(params=all_xterms_params) -def xterms(request): - """Common kind values for xterm terminals.""" - return request.param - - @pytest.fixture(params=all_terms_params) def all_terms(request): """Common kind values for all kinds of terminals.""" diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index 448bcb6f..3395ebda 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -3,7 +3,6 @@ # std imports import functools import tempfile -import platform import signal import curses import time @@ -24,7 +23,6 @@ SEMAPHORE, all_terms, echo_off, - xterms, ) # 3rd-party @@ -36,6 +34,8 @@ unichr = chr +@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, + reason="TEST_QUICK specified") def test_kbhit_interrupted(): "kbhit() should not be interrupted with a signal handler." pid, master_fd = pty.fork() @@ -79,6 +79,8 @@ def on_resize(sig, action): assert math.floor(time.time() - stime) == 1.0 +@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, + reason="TEST_QUICK specified") def test_kbhit_interrupted_nonetype(): "kbhit() should also allow interruption with timeout of None." pid, master_fd = pty.fork() @@ -201,6 +203,8 @@ def child(): child() +@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, + reason="TEST_QUICK specified") def test_keystroke_1s_cbreak_noinput(): "1-second keystroke without input; '' should be returned after ~1 second." @as_subprocess @@ -214,6 +218,8 @@ def child(): child() +@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, + reason="TEST_QUICK specified") def test_keystroke_1s_cbreak_noinput_nokb(): "1-second keystroke without input or keyboard." @as_subprocess @@ -399,6 +405,8 @@ def test_keystroke_0s_cbreak_sequence(): assert math.floor(time.time() - stime) == 0.0 +@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, + reason="TEST_QUICK specified") def test_keystroke_1s_cbreak_with_input(): "1-second keystroke w/multibyte sequence; should return after ~1 second." pid, master_fd = pty.fork() @@ -431,6 +439,8 @@ def test_keystroke_1s_cbreak_with_input(): assert math.floor(time.time() - stime) == 1.0 +@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, + reason="TEST_QUICK specified") def test_esc_delay_cbreak_035(): "esc_delay will cause a single ESC (\\x1b) to delay for 0.35." pid, master_fd = pty.fork() @@ -466,6 +476,8 @@ def test_esc_delay_cbreak_035(): assert 34 <= int(duration_ms) <= 45, duration_ms +@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, + reason="TEST_QUICK specified") def test_esc_delay_cbreak_135(): "esc_delay=1.35 will cause a single ESC (\\x1b) to delay for 1.35." pid, master_fd = pty.fork() @@ -616,18 +628,18 @@ def child(kind): child(all_terms) -def test_get_keyboard_sequences_sort_order(xterms): +def test_get_keyboard_sequences_sort_order(): "ordereddict ensures sequences are ordered longest-first." @as_subprocess - def child(): - term = TestTerminal(force_styling=True) + def child(kind): + term = TestTerminal(kind=kind, force_styling=True) maxlen = None for sequence, code in term._keymap.items(): if maxlen is not None: assert len(sequence) <= maxlen assert sequence maxlen = len(sequence) - child() + child(kind='xterm-256color') def test_get_keyboard_sequence(monkeypatch): diff --git a/blessed/tests/test_wrap.py b/blessed/tests/test_wrap.py index e5f7a15b..6fe9f0d8 100644 --- a/blessed/tests/test_wrap.py +++ b/blessed/tests/test_wrap.py @@ -1,10 +1,8 @@ -import platform +# std +import os import textwrap -import termios -import struct -import fcntl -import sys +# local from .accessories import ( as_subprocess, TestTerminal, @@ -12,30 +10,10 @@ all_terms, ) +# 3rd party import pytest - -def test_SequenceWrapper_invalid_width(): - """Test exception thrown from invalid width""" - WIDTH = -3 - - @as_subprocess - def child(): - term = TestTerminal() - try: - my_wrapped = term.wrap(u'------- -------------', WIDTH) - except ValueError as err: - assert err.args[0] == ( - "invalid width %r(%s) (must be integer > 0)" % ( - WIDTH, type(WIDTH))) - else: - assert False, 'Previous stmt should have raised exception.' - del my_wrapped # assigned but never used - - child() - - -@pytest.mark.parametrize("kwargs", [ +TEXTWRAP_KEYWORD_COMBINATIONS = [ dict(break_long_words=False, drop_whitespace=False, subsequent_indent=''), @@ -60,7 +38,37 @@ def child(): dict(break_long_words=True, drop_whitespace=True, subsequent_indent=' '), -]) +] +if os.environ.get('TEST_QUICK', None) is not None: + # test only one feature: everything on + TEXTWRAP_KEYWORD_COMBINATIONS = [ + dict(break_long_words=True, + drop_whitespace=True, + subsequent_indent=' ') + ] + + +def test_SequenceWrapper_invalid_width(): + """Test exception thrown from invalid width""" + WIDTH = -3 + + @as_subprocess + def child(): + term = TestTerminal() + try: + my_wrapped = term.wrap(u'------- -------------', WIDTH) + except ValueError as err: + assert err.args[0] == ( + "invalid width %r(%s) (must be integer > 0)" % ( + WIDTH, type(WIDTH))) + else: + assert False, 'Previous stmt should have raised exception.' + del my_wrapped # assigned but never used + + child() + + +@pytest.mark.parametrize("kwargs", TEXTWRAP_KEYWORD_COMBINATIONS) def test_SequenceWrapper(all_terms, many_columns, kwargs): """Test that text wrapping matches internal extra options.""" @as_subprocess diff --git a/tox.ini b/tox.ini index 6d228970..1e123d5e 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,8 @@ skip_missing_interpreters = true [testenv] whitelist_externals = /bin/mv setenv = PYTHONIOENCODING=UTF8 +# use for brief or thorough testing, respectively +passenv = TEST_QUICK TEST_FULL deps = pytest-xdist pytest-cov pytest From c6433fdabc9815be6ef1b7e2712413740477eec9 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 2 Oct 2015 18:14:12 -0700 Subject: [PATCH 277/459] user servercentral for travis IRC notify --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 588b179d..20b1c4ab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,7 +30,7 @@ notifications: on_failure: change irc: channels: - - "irc.Prison.NET#1984" + - "irc.servercentral.net#1984" template: - "%{repository}(%{branch}): %{message} (%{duration}) %{build_url}" skip_join: true From fcb34a2d549063e33ec4f8ed45aaa43c1fe9eb5f Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 2 Oct 2015 18:45:27 -0700 Subject: [PATCH 278/459] don't append history to long_description the sphinx tags cause pypi not to render at all --- setup.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 496f1417..e51193a4 100755 --- a/setup.py +++ b/setup.py @@ -33,10 +33,8 @@ def _get_long_description(fname): fname=os.path.join(HERE, 'version.json')), install_requires=_get_install_requires( fname=os.path.join(HERE, 'requirements.txt')), - long_description='{0}\n\n{1}'.format( - _get_long_description(os.path.join(HERE, 'docs', 'intro.rst')), - _get_long_description(os.path.join(HERE, 'docs', 'history.rst')), - ), + long_description=_get_long_description( + fname=os.path.join(HERE, 'docs', 'intro.rst')), description=('A thin, practical wrapper around terminal styling, ' 'screen positioning, and keyboard input.'), author='Jeff Quast, Erik Rose', From b59b4ae9f9623edc41612ab2affc0eb2df98cf87 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 2 Oct 2015 19:14:50 -0700 Subject: [PATCH 279/459] track requirements as files, esp. for readthedocs.org --- CONTRIBUTING.rst | 5 ++++ requirements-analysis.txt | 3 +++ requirements-docs.txt | 3 +++ requirements-tests.txt | 4 ++++ tox.ini | 48 +++++---------------------------------- 5 files changed, 21 insertions(+), 42 deletions(-) create mode 100644 requirements-analysis.txt create mode 100644 requirements-docs.txt create mode 100644 requirements-tests.txt diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index fbb56b0f..c4d49cb8 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -56,6 +56,11 @@ static analysis tools through the **sa** target, invoked using:: tox -esa +Similarly, positional arguments can be used, for example to verify URL +links:: + + tox -esa -- -blinkcheck + All standards enforced by the underlying tools are adhered to by the blessed project, with the declarative exception of those found in `landscape.yml `_, or inline diff --git a/requirements-analysis.txt b/requirements-analysis.txt new file mode 100644 index 00000000..492c109e --- /dev/null +++ b/requirements-analysis.txt @@ -0,0 +1,3 @@ +prospector[with_frosted,with_pyroma] +restructuredtext_lint +doc8 diff --git a/requirements-docs.txt b/requirements-docs.txt new file mode 100644 index 00000000..247b04ff --- /dev/null +++ b/requirements-docs.txt @@ -0,0 +1,3 @@ +sphinx +sphinx_rtd_theme +sphinx-paramlinks diff --git a/requirements-tests.txt b/requirements-tests.txt new file mode 100644 index 00000000..c1d3f57f --- /dev/null +++ b/requirements-tests.txt @@ -0,0 +1,4 @@ +pytest-xdist +pytest-cov +pytest +mock diff --git a/tox.ini b/tox.ini index 1e123d5e..9cdb7522 100644 --- a/tox.ini +++ b/tox.ini @@ -5,13 +5,8 @@ skip_missing_interpreters = true [testenv] whitelist_externals = /bin/mv setenv = PYTHONIOENCODING=UTF8 -# use for brief or thorough testing, respectively passenv = TEST_QUICK TEST_FULL -deps = pytest-xdist - pytest-cov - pytest - mock - +deps = --requirement requirements-tests.txt commands = python {toxinidir}/bin/display-sighandlers.py python {toxinidir}/bin/display-terminalinfo.py {envbindir}/py.test \ @@ -23,21 +18,8 @@ commands = python {toxinidir}/bin/display-sighandlers.py /bin/mv {toxinidir}/.coverage {toxinidir}/.coverage.{envname} [testenv:sa] -# Instead of trusting whichever developer's environment python is used to -# invoke tox, explicitly define python2.7 because the 'doc8' tool does not -# appear to be python3-compatible: https://github.com/stackforge/doc8/commit/4d82c269ab46f0c5370c1f00be06e0c406164e85#commitcomment-10725927 -basepython=python2.7 -deps = prospector[with_frosted,with_pyroma] - restructuredtext_lint - doc8 - -# - ensure all python files compile ! -# - prospector is configured using .prospector.yaml, and wraps several -# static analysis/linting and style-checker tools. -# - rst-lint ensures that README.rst will present correctly on pypi. -# - doc8 is like pep8 for rst documents. Namely, enforcing styling. -# ignore docs/further.rst:21: D000 Bullet list ends without a blank line; -# unexpected unindent. This is a tool error +basepython = python2.7 +deps = --requirement requirements-analysis.txt commands = python -m compileall -fq {toxinidir}/blessed {envbindir}/prospector \ --die-on-tool-error \ @@ -46,32 +28,14 @@ commands = python -m compileall -fq {toxinidir}/blessed {envbindir}/doc8 --ignore-path docs/_build --ignore D000 docs [testenv:docs] -whitelist_externals=echo -basepython=python3.4 -deps=sphinx - sphinx_rtd_theme - sphinx-paramlinks - -# note: to verify URL links, -# -# $ tox -edocs -- -blinkcheck -# +whitelist_externals = echo +basepython = python3.4 +deps = --requirement requirements-docs.txt commands = {envbindir}/sphinx-build -v -W -b html \ -d {toxinidir}/docs/_build/doctrees \ {posargs} docs \ {toxinidir}/docs/_build/html echo "open {toxinidir}/docs/_build/html/index.html for review" -[testenv:linkcheck] -deps=sphinx - sphinx_rtd_theme - sphinx-paramlinks - -commands = {envbindir}/sphinx-build -v -W -b html \ - -d {toxinidir}/docs/_build/doctrees \ - docs \ - {toxinidir}/docs/_build/html - echo "open {toxinidir}/docs/_build/html/index.html for review" - [pytest] looponfailroots = blessed From 5e5f88fcf0411d123e6fad8c4458b99db4d69504 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 2 Oct 2015 19:17:13 -0700 Subject: [PATCH 280/459] travis-ci's pip has no --requirement, only -r --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 9cdb7522..36b16948 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,7 @@ skip_missing_interpreters = true whitelist_externals = /bin/mv setenv = PYTHONIOENCODING=UTF8 passenv = TEST_QUICK TEST_FULL -deps = --requirement requirements-tests.txt +deps = -rrequirements-tests.txt commands = python {toxinidir}/bin/display-sighandlers.py python {toxinidir}/bin/display-terminalinfo.py {envbindir}/py.test \ @@ -19,7 +19,7 @@ commands = python {toxinidir}/bin/display-sighandlers.py [testenv:sa] basepython = python2.7 -deps = --requirement requirements-analysis.txt +deps = -rrequirements-analysis.txt commands = python -m compileall -fq {toxinidir}/blessed {envbindir}/prospector \ --die-on-tool-error \ @@ -30,7 +30,7 @@ commands = python -m compileall -fq {toxinidir}/blessed [testenv:docs] whitelist_externals = echo basepython = python3.4 -deps = --requirement requirements-docs.txt +deps = -rrequirements-docs.txt commands = {envbindir}/sphinx-build -v -W -b html \ -d {toxinidir}/docs/_build/doctrees \ {posargs} docs \ From d878b59154eed35dd39294e3691b913b81ea9e39 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 3 Oct 2015 01:11:28 -0700 Subject: [PATCH 281/459] Resolve code coverage across build chains --- .coveragerc | 8 ++++ .gitignore | 1 + .landscape.yml | 1 + .travis.yml | 9 ++-- blessed/formatters.py | 5 ++- blessed/terminal.py | 10 +---- blessed/tests/accessories.py | 11 ++++- blessed/tests/test_keyboard.py | 12 ++++++ blessed/tests/test_length_sequence.py | 27 +++++++++--- blessed/tests/test_sequences.py | 2 + blessed/tests/test_wrap.py | 10 ++--- docs/intro.rst | 4 +- tools/custom-combine.py | 61 +++++++++++++++++++++++++++ tools/teamcity-coverage-report.sh | 27 ------------ tools/teamcity-runtests.sh | 35 --------------- tox.ini | 43 +++++++++++++------ 16 files changed, 161 insertions(+), 105 deletions(-) create mode 100755 tools/custom-combine.py delete mode 100755 tools/teamcity-coverage-report.sh delete mode 100755 tools/teamcity-runtests.sh diff --git a/.coveragerc b/.coveragerc index eb472140..de699ea7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,6 +1,14 @@ [run] branch = True source = blessed +parallel = True [report] omit = blessed/tests/* +exclude_lines = pragma: no cover +precision = 1 + +[paths] +source = + blessed/ + /opt/TeamCity/*/blessed/*.py diff --git a/.gitignore b/.gitignore index f71f24b1..ffa0246f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .coverage +._coverage.* .coverage.* .cache .tox diff --git a/.landscape.yml b/.landscape.yml index ce4e063f..1a1a1b75 100644 --- a/.landscape.yml +++ b/.landscape.yml @@ -7,6 +7,7 @@ ignore-patterns: - ^build/ # ignore these, their quality does not so much matter. - ^blessed/tests/ + - ^tools/ test-warnings: true diff --git a/.travis.yml b/.travis.yml index 20b1c4ab..41c84d29 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,18 +9,15 @@ script: matrix: fast_finish: true include: + - env: TOXENV=about - env: TOXENV=sa - - env: TOXENV=docs - - python: 2.6 - env: TOXENV=py26 TEST_QUICK=1 + - env: TOXENV=sphinx - python: 2.7 env: TOXENV=py27 TEST_QUICK=1 - - python: 3.3 - env: TOXENV=py33 TEST_QUICK=1 - python: 3.4 env: TOXENV=py34 TEST_QUICK=1 - python: 3.5 - env: TOXENV=py35 TEST_QUICK=1 + env: TOXENV=py35 notifications: email: diff --git a/blessed/formatters.py b/blessed/formatters.py index bfaa84bc..4be99300 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -176,7 +176,7 @@ def get_proxy_string(term, attr): # normalize 'screen-256color', or 'ansi.sys' to its basic names term_kind = next(iter(_kind for _kind in ('screen', 'ansi',) if term.kind.startswith(_kind)), term) - return { + _proxy_table = { # pragma: no cover 'screen': { # proxy move_x/move_y for 'screen' terminal type, used by tmux(1). 'hpa': ParameterizingProxyString( @@ -198,7 +198,8 @@ def get_proxy_string(term, attr): 'sc': '\x1b[s', 'rc': '\x1b[u', } - }.get(term_kind, {}).get(attr, None) + } + return _proxy_table.get(term_kind, {}).get(attr, None) class FormattingString(six.text_type): diff --git a/blessed/terminal.py b/blessed/terminal.py index 2c4fbce2..5b475c28 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -10,7 +10,6 @@ import io import locale import os -import platform import select import struct import sys @@ -175,14 +174,7 @@ def __init__(self, kind=None, stream=None, force_styling=False): # send them to stdout as a fallback, since they have to go # somewhere. try: - if (platform.python_implementation() == 'PyPy' and - isinstance(self._kind, unicode)): - # pypy/2.4.0_2/libexec/lib_pypy/_curses.py, line 1131 - # TypeError: initializer for ctype 'char *' must be a str - curses.setupterm(self._kind.encode('ascii'), - self._init_descriptor) - else: - curses.setupterm(self._kind, self._init_descriptor) + curses.setupterm(self._kind, self._init_descriptor) except curses.error as err: warnings.warn('Failed to setupterm(kind={0!r}): {1}' .format(self._kind, err)) diff --git a/blessed/tests/accessories.py b/blessed/tests/accessories.py index b04824ce..8253ae7a 100644 --- a/blessed/tests/accessories.py +++ b/blessed/tests/accessories.py @@ -25,7 +25,8 @@ SEND_SEMAPHORE = SEMAPHORE = b'SEMAPHORE\n' RECV_SEMAPHORE = b'SEMAPHORE\r\n' many_lines_params = [40, 80] -many_columns_params = [5, 25] +# we must test a '1' column for conditional in _handle_long_word +many_columns_params = [1, 10] if os.environ.get('TEST_QUICK'): many_lines_params = [80,] @@ -70,7 +71,13 @@ def __call__(self, *args, **kwargs): cov = None try: import coverage - cov = coverage.Coverage(data_suffix=True) + _coveragerc = os.path.join(os.path.dirname(__file__), + os.pardir, os.pardir, + '.coveragerc') + cov = coverage.Coverage(config_file=_coveragerc) + cov.set_option("run:note", + "@as_subprocess-{0};{1}(*{2}, **{3})".format( + os.getpid(), self.func, args, kwargs)) cov.start() except ImportError: pass diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index 3395ebda..be75a806 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -151,6 +151,18 @@ def child(): child() +def test_raw_input_with_kb(): + "raw should call tty.setraw() when with keyboard." + @as_subprocess + def child(): + term = TestTerminal() + assert term._keyboard_fd is not None + with mock.patch("tty.setraw") as mock_setraw: + with term.raw(): + assert mock_setraw.called + child() + + def test_notty_kb_is_None(): "term._keyboard_fd should be None when os.isatty returns False." # in this scenerio, stream is sys.__stdout__, diff --git a/blessed/tests/test_length_sequence.py b/blessed/tests/test_length_sequence.py index 2c0c553b..0c3efffb 100644 --- a/blessed/tests/test_length_sequence.py +++ b/blessed/tests/test_length_sequence.py @@ -1,7 +1,6 @@ # encoding: utf-8 # std imports import itertools -import platform import termios import struct import fcntl @@ -222,8 +221,6 @@ def child(): child() -@pytest.mark.skipif(platform.python_implementation() == 'PyPy', - reason='PyPy fails TIOCSWINSZ') def test_winsize(many_lines, many_columns): """Test height and width is appropriately queried in a pty.""" @as_subprocess @@ -241,8 +238,28 @@ def child(lines=25, cols=80): child(lines=many_lines, cols=many_columns) -@pytest.mark.skipif(platform.python_implementation() == 'PyPy', - reason='PyPy fails TIOCSWINSZ') +def test_Sequence_alignment_fixed_width(): + @as_subprocess + def child(kind): + t = TestTerminal(kind=kind) + pony_msg = 'pony express, all aboard, choo, choo!' + pony_len = len(pony_msg) + pony_colored = u''.join( + ['%s%s' % (t.color(n % 7), ch,) + for n, ch in enumerate(pony_msg)]) + pony_colored += t.normal + ladjusted = t.ljust(pony_colored, 88) + radjusted = t.rjust(pony_colored, 88) + centered = t.center(pony_colored, 88) + assert (t.length(pony_colored) == pony_len) + assert (t.length(centered.strip()) == pony_len) + assert (t.length(centered) == len(pony_msg.center(88))) + assert (t.length(ladjusted.strip()) == pony_len) + assert (t.length(ladjusted) == len(pony_msg.ljust(88))) + assert (t.length(radjusted.strip()) == pony_len) + assert (t.length(radjusted) == len(pony_msg.rjust(88))) + + def test_Sequence_alignment(all_terms): """Tests methods related to Sequence class, namely ljust, rjust, center.""" @as_subprocess diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index 1f60a4de..24c5c3d8 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -562,6 +562,8 @@ def child(): from blessed import Terminal term = Terminal('xterm-256color') assert Sequence('xyz\b', term).padd() == u'xy' + assert Sequence('xyz\b-', term).padd() == u'xy-' assert Sequence('xxxx\x1b[3Dzz', term).padd() == u'xzz' + assert Sequence('\x1b[3D', term).padd() == u'' # "Trim left" child() diff --git a/blessed/tests/test_wrap.py b/blessed/tests/test_wrap.py index 6fe9f0d8..1026b68d 100644 --- a/blessed/tests/test_wrap.py +++ b/blessed/tests/test_wrap.py @@ -7,7 +7,6 @@ as_subprocess, TestTerminal, many_columns, - all_terms, ) # 3rd party @@ -69,13 +68,12 @@ def child(): @pytest.mark.parametrize("kwargs", TEXTWRAP_KEYWORD_COMBINATIONS) -def test_SequenceWrapper(all_terms, many_columns, kwargs): +def test_SequenceWrapper(many_columns, kwargs): """Test that text wrapping matches internal extra options.""" @as_subprocess - def child(term, width, kwargs): + def child(width, pgraph, kwargs): # build a test paragraph, along with a very colorful version term = TestTerminal() - pgraph = u' Z! a bc defghij klmnopqrstuvw<<>>xyz012345678900 ' attributes = ('bright_red', 'on_bright_blue', 'underline', 'reverse', 'red_reverse', 'red_on_white', 'superscript', 'subscript', 'on_bright_white') @@ -110,4 +108,6 @@ def child(term, width, kwargs): # ensure our colored textwrap is the same paragraph length assert (len(internal_wrapped) == len(my_wrapped_colored)) - child(all_terms, many_columns, kwargs) + child(width=many_columns, kwargs=kwargs, + pgraph=u' Z! a bc defghij klmnopqrstuvw<<>>xyz012345678900 '*2) + child(width=many_columns, kwargs=kwargs, pgraph=u'a bb ccc') diff --git a/docs/intro.rst b/docs/intro.rst index 8f4159c2..a106b537 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -6,9 +6,9 @@ :alt: TeamCity Build status :target: https://teamcity-master.pexpect.org/viewType.html?buildTypeId=Blessed_BuildHead&branch_Blessed=%3Cdefault%3E&tab=buildTypeStatusDiv -.. image:: https://img.shields.io/coveralls/jekyll/jekyll/master.svg +.. image:: https://coveralls.io/repos/jquast/blessed/badge.svg?branch=master&service=github :alt: Coveralls Code Coverage - :target: https://coveralls.io/r/jquast/blessed?branch=master + :target: https://coveralls.io/github/jquast/blessed?branch=master .. image:: https://img.shields.io/pypi/v/blessed.svg :alt: Latest Version diff --git a/tools/custom-combine.py b/tools/custom-combine.py new file mode 100755 index 00000000..7331d301 --- /dev/null +++ b/tools/custom-combine.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +"""Simple script provides coverage combining across build chains.""" +# pylint: disable=invalid-name +from __future__ import print_function + +# local +import subprocess +import tempfile +import shutil +import glob +import os + +# 3rd-party +import coverage +import six + +PROJ_ROOT = os.path.join(os.path.dirname(__file__), os.pardir) +COVERAGERC = os.path.join(PROJ_ROOT, '.coveragerc') + +def main(): + """Program entry point.""" + cov = coverage.Coverage(config_file=COVERAGERC) + cov.combine() + + # we must duplicate these files, coverage.py unconditionally + # deletes them on .combine(). + _data_paths = glob.glob(os.path.join(PROJ_ROOT, '._coverage.*')) + dst_folder = tempfile.mkdtemp() + data_paths = [] + for src in _data_paths: + dst = os.path.join(dst_folder, os.path.basename(src)) + shutil.copy(src, dst) + data_paths.append(dst) + + print("combining coverage: {0}".format(data_paths)) + cov.combine(data_paths=data_paths) + cov.load() + cov.html_report() + print("--> open {0}/htmlcov/index.html for review." + .format(os.path.relpath(PROJ_ROOT))) + + fout = six.StringIO() + cov.report(file=fout) + for line in fout.getvalue().splitlines(): + if u'TOTAL' in line: + total_line = line + break + else: + raise ValueError("'TOTAL' summary not found in summary output") + + _, no_stmts, no_miss, _ = total_line.split(None, 3) + no_covered = int(no_stmts) - int(no_miss) + print("##teamcity[buildStatisticValue " + "key='CodeCoverageAbsLTotal' " + "value='{0}']".format(no_stmts)) + print("##teamcity[buildStatisticValue " + "key='CodeCoverageAbsLCovered' " + "value='{0}']".format(no_covered)) + +if __name__ == '__main__': + main() diff --git a/tools/teamcity-coverage-report.sh b/tools/teamcity-coverage-report.sh deleted file mode 100755 index 2e32241b..00000000 --- a/tools/teamcity-coverage-report.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash -# This is to be executed by each individual OS test. It only -# combines coverage files and reports locally to the given -# TeamCity build configuration. -set -e -set -o pipefail -[ -z ${TEMP} ] && TEMP=/tmp - -# combine all .coverage* files, -coverage combine - -# create ascii report, -report_file=$(mktemp $TEMP/coverage.XXXXX) -coverage report --rcfile=`dirname $0`/../.coveragerc > "${report_file}" 2>/dev/null - -# Report Code Coverage for TeamCity, using 'Service Messages', -# https://confluence.jetbrains.com/display/TCD8/How+To...#HowTo...-ImportcoverageresultsinTeamCity -# https://confluence.jetbrains.com/display/TCD8/Custom+Chart#CustomChart-DefaultStatisticsValuesProvidedbyTeamCity -total_no_lines=$(awk '/TOTAL/{printf("%s",$2)}' < "${report_file}") -total_no_misses=$(awk '/TOTAL/{printf("%s",$3)}' < "${report_file}") -total_no_covered=$((${total_no_lines} - ${total_no_misses})) -echo "##teamcity[buildStatisticValue key='CodeCoverageAbsLTotal' value='""${total_no_lines}""']" -echo "##teamcity[buildStatisticValue key='CodeCoverageAbsLCovered' value='""${total_no_covered}""']" - -# Display for human consumption and remove ascii file. -cat "${report_file}" -rm "${report_file}" diff --git a/tools/teamcity-runtests.sh b/tools/teamcity-runtests.sh deleted file mode 100755 index 15b8e697..00000000 --- a/tools/teamcity-runtests.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash -set -e -set -o pipefail - -here=$(cd `dirname $0`; pwd) -osrel=$(uname -s) - -# run tests -cd $here/.. - -_cmd=tox -if [ X"$osrel" == X"Linux" ]; then - # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=754248 - # cannot create a virtualenv for python2.6 due to use of - # "{}".format in virtualenv, throws exception - # ValueError: zero length field name in format. - _cmd='tox -epy27,py33,py34,docs,sa' -fi - -ret=0 -echo ${_cmd} -${_cmd} || ret=$? - -if [ $ret -ne 0 ]; then - # we always exit 0, preferring instead the jUnit XML - # results to be the dominate cause of a failed build. - echo "py.test returned exit code ${ret}." >&2 - echo "the build should detect and report these failing tests." >&2 -fi - -# combine all coverage to single file, publish as build -# artifact in {pexpect_projdir}/build-output -mkdir -p build-output -coverage combine -mv .coverage build-output/.coverage.${osrel}.$RANDOM.$$ diff --git a/tox.ini b/tox.ini index 36b16948..5d242d26 100644 --- a/tox.ini +++ b/tox.ini @@ -1,21 +1,36 @@ [tox] -envlist = about, sa, docs, py{26,27,33,34,35} +envlist = about, sa, sphinx, py{27,34,35} skip_missing_interpreters = true [testenv] -whitelist_externals = /bin/mv +whitelist_externals = cp + echo setenv = PYTHONIOENCODING=UTF8 passenv = TEST_QUICK TEST_FULL deps = -rrequirements-tests.txt -commands = python {toxinidir}/bin/display-sighandlers.py - python {toxinidir}/bin/display-terminalinfo.py - {envbindir}/py.test \ +commands = {envbindir}/py.test \ --strict --verbose --verbose --color=yes \ --junit-xml=results.{envname}.xml \ - --cov blessed --cov-report=term-missing \ - blessed/tests \ + --cov blessed blessed/tests \ {posargs} - /bin/mv {toxinidir}/.coverage {toxinidir}/.coverage.{envname} + coverage combine + cp {toxinidir}/.coverage \ + {toxinidir}/._coverage.{envname}.{env:TEAMCITY_BUILDCONFNAME:xxx} + {toxinidir}/tools/custom-combine.py + +# CI buildchain target +[testenv:coverage] +deps = coverage +commands = {toxinidir}/tools/custom-combine.py + +# CI buildhcain target +[testenv:coveralls] +deps = coveralls +commands = coveralls + +[testenv:about] +commands = python {toxinidir}/bin/display-sighandlers.py + python {toxinidir}/bin/display-terminalinfo.py [testenv:sa] basepython = python2.7 @@ -27,15 +42,19 @@ commands = python -m compileall -fq {toxinidir}/blessed {envbindir}/rst-lint README.rst {envbindir}/doc8 --ignore-path docs/_build --ignore D000 docs -[testenv:docs] +[testenv:sphinx] whitelist_externals = echo basepython = python3.4 deps = -rrequirements-docs.txt -commands = {envbindir}/sphinx-build -v -W -b html \ +commands = {envbindir}/sphinx-build -v -W \ -d {toxinidir}/docs/_build/doctrees \ - {posargs} docs \ + {posargs:-b html} docs \ {toxinidir}/docs/_build/html - echo "open {toxinidir}/docs/_build/html/index.html for review" + echo "--> open docs/_build/html/index.html for review." [pytest] looponfailroots = blessed + +[coverage] +rcfile = {toxinidir}/.coveragerc +rc = --rcfile={[coverage]rcfile} From fd3875ecf235d320446f1bdc7c849c881debee2e Mon Sep 17 00:00:00 2001 From: Volodymyr Vitvitskyi Date: Fri, 9 Oct 2015 21:12:07 +0100 Subject: [PATCH 282/459] Fix prospector complains When running `tox` prospector complains about severael D211 violations:: pep257: D211 / No blank lines allowed before class docstring (found 1) --- blessed/formatters.py | 4 ---- blessed/keyboard.py | 1 - blessed/sequences.py | 2 -- 3 files changed, 7 deletions(-) diff --git a/blessed/formatters.py b/blessed/formatters.py index 4be99300..088a115b 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -40,7 +40,6 @@ def _make_compoundables(colors): class ParameterizingString(six.text_type): - r""" A Unicode string which can be called as a parameterizing termcap. @@ -104,7 +103,6 @@ def __call__(self, *args): class ParameterizingProxyString(six.text_type): - r""" A Unicode string which can be called to proxy missing termcap entries. @@ -203,7 +201,6 @@ def get_proxy_string(term, attr): class FormattingString(six.text_type): - r""" A Unicode string which doubles as a callable. @@ -240,7 +237,6 @@ def __call__(self, text): class NullCallableString(six.text_type): - """ A dummy callable Unicode alternative to :class:`FormattingString`. diff --git a/blessed/keyboard.py b/blessed/keyboard.py index 79352bb7..39b2450d 100644 --- a/blessed/keyboard.py +++ b/blessed/keyboard.py @@ -18,7 +18,6 @@ class Keystroke(six.text_type): - """ A unicode-derived class for describing a single keystroke. diff --git a/blessed/sequences.py b/blessed/sequences.py index 393976a1..555eb2fe 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -374,7 +374,6 @@ def init_sequence_patterns(term): class SequenceTextWrapper(textwrap.TextWrapper): - """This docstring overridden.""" def __init__(self, width, term, **kwargs): @@ -488,7 +487,6 @@ def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width): class Sequence(six.text_type): - """ A "sequence-aware" version of the base :class:`str` class. From e6414a51ff2bbaf8d6bb521eb89c6e96fa038bd8 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 9 Oct 2015 16:28:06 -0700 Subject: [PATCH 283/459] pytest: +norecursedirs = .git for faster launches --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 5d242d26..5f9e1f0b 100644 --- a/tox.ini +++ b/tox.ini @@ -54,6 +54,7 @@ commands = {envbindir}/sphinx-build -v -W \ [pytest] looponfailroots = blessed +norecursedirs = .git [coverage] rcfile = {toxinidir}/.coveragerc From 975bcc43b07d59ca21d9eab36caa6ab5492d0724 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 9 Oct 2015 16:57:35 -0700 Subject: [PATCH 284/459] add display-{fpathconf,maxcanon}.py from pexpect so that the 'tox -eabout' describes the operating system's terminal paramters/environment values more thoroughly. --- bin/display-fpathconf.py | 55 +++++++++++++++++++++++ bin/display-maxcanon.py | 87 +++++++++++++++++++++++++++++++++++++ bin/display-terminalinfo.py | 2 +- requirements-about.txt | 1 + tox.ini | 5 +++ 5 files changed, 149 insertions(+), 1 deletion(-) create mode 100755 bin/display-fpathconf.py create mode 100755 bin/display-maxcanon.py create mode 100644 requirements-about.txt diff --git a/bin/display-fpathconf.py b/bin/display-fpathconf.py new file mode 100755 index 00000000..81229482 --- /dev/null +++ b/bin/display-fpathconf.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +"""Displays os.fpathconf values related to terminals.""" +# pylint: disable=invalid-name +# Invalid module name "display-sighandlers" +from __future__ import print_function +import sys +import os + + +def display_fpathconf(): + """Program entry point.""" + disp_values = ( + ('PC_MAX_CANON', ('Max no. of bytes in a ' + 'terminal canonical input line.')), + ('PC_MAX_INPUT', ('Max no. of bytes for which ' + 'space is available in a terminal input queue.')), + ('PC_PIPE_BUF', ('Max no. of bytes which will ' + 'be written atomically to a pipe.')), + + # to explain in more detail: PC_VDISABLE is the reference character in + # the pairing output for bin/display-terminalinfo.py: if the value + # matches (\xff), then that special control character is disabled, fe: + # + # Index Name Special Character Default Value + # VEOF EOF ^D + # VEOL EOL _POSIX_VDISABLE + # + # irregardless, this value is almost always \xff. + ('PC_VDISABLE', 'Terminal character disabling value.') + ) + fmt = '{name:<13} {value:<10} {description:<11}' + + # column header + print(fmt.format(name='name', value='value', description='description')) + print(fmt.replace('<', '-<').format(name='-', value='-', description='-')) + + fd = sys.stdin.fileno() + for name, description in disp_values: + key = os.pathconf_names.get(name, None) + if key is None: + value = 'UNDEF' + else: + try: + value = os.fpathconf(fd, name) + if name == 'PC_VDISABLE': + value = r'\x{0:2x}'.format(value) + except OSError as err: + value = 'OSErrno {0.errno}'.format(err) + + print(fmt.format(name=name, value=value, description=description)) + print() + + +if __name__ == '__main__': + display_fpathconf() diff --git a/bin/display-maxcanon.py b/bin/display-maxcanon.py new file mode 100755 index 00000000..1df8a7e4 --- /dev/null +++ b/bin/display-maxcanon.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python +""" +This tool uses pexpect to test expected Canonical mode length. + +All systems use the value of MAX_CANON which can be found using +fpathconf(3) value PC_MAX_CANON -- with the exception of Linux +and FreeBSD. + +Linux, though defining a value of 255, actually honors the value +of 4096 from linux kernel include file tty.h definition +N_TTY_BUF_SIZE. + +Linux also does not honor IMAXBEL. termios(3) states, "Linux does not +implement this bit, and acts as if it is always set." Although these +tests ensure it is enabled, this is a non-op for Linux. + +FreeBSD supports neither, and instead uses a fraction (1/5) of the tty +speed which is always 9600. Therefor, the maximum limited input line +length is 9600 / 5 = 1920. + +In other words, the only way to determine the true MAX_CANON in a +cross-platform manner is through this systems integrated test: the given +system definitions are misleading on some operating systems. +""" +# pylint: disable=invalid-name +# Invalid module name "display-sighandlers" +# std import +from __future__ import print_function +import sys +import os + + +def detect_maxcanon(): + """Program entry point.""" + import pexpect + bashrc = os.path.join( + # re-use pexpect/replwrap.py's bashrc file, + os.path.dirname(__file__), os.path.pardir, 'pexpect', 'bashrc.sh') + + child = pexpect.spawn('bash', ['--rcfile', bashrc], + echo=True, encoding='utf8', + timeout=3) + + child.sendline(u'echo -n READY_; echo GO') + child.expect_exact(u'READY_GO') + + child.sendline(u'stty icanon imaxbel erase ^H; echo -n retval: $?') + child.expect_exact(u'retval: 0') + + child.sendline(u'echo -n GO_; echo AGAIN') + child.expect_exact(u'GO_AGAIN') + child.sendline(u'cat') + + child.delaybeforesend = 0 + + column, blocksize = 0, 64 + ch_marker = u'_' + + print('auto-detecting MAX_CANON: ', end='') + sys.stdout.flush() + + while True: + child.send(ch_marker * blocksize) + result = child.expect([ch_marker * blocksize, u'\a', pexpect.TIMEOUT]) + if result == 0: + # entire block fit without emitting bel + column += blocksize + elif result == 1: + # an '\a' was emitted, count the number of ch_markers + # found since last blocksize, determining our MAX_CANON + column += child.before.count(ch_marker) + break + elif result == 3: + print('Undetermined (Timeout) !') + print(('child.before: ', child.before)) + print(column) + +if __name__ == '__main__': + try: + detect_maxcanon() + except ImportError: + # we'd like to use this with CI -- but until we integrate + # with tox, we can't determine a period in testing when + # the pexpect module has been installed + print('warning: pexpect not in module path, MAX_CANON ' + 'could not be determined by systems test.', + file=sys.stderr) diff --git a/bin/display-terminalinfo.py b/bin/display-terminalinfo.py index f8da0896..861ee6c0 100755 --- a/bin/display-terminalinfo.py +++ b/bin/display-terminalinfo.py @@ -166,7 +166,7 @@ def display_pathconf(names, getter): try: value = getter(name) except OSError as err: - value = err + value = 'OSErrno {err.errno}'.format(err=err) print(fmt.format(name=name, value=value, col1_width=col1_width)) print() diff --git a/requirements-about.txt b/requirements-about.txt new file mode 100644 index 00000000..808fb07a --- /dev/null +++ b/requirements-about.txt @@ -0,0 +1 @@ +pexpect diff --git a/tox.ini b/tox.ini index 5f9e1f0b..5fb7241c 100644 --- a/tox.ini +++ b/tox.ini @@ -29,12 +29,17 @@ deps = coveralls commands = coveralls [testenv:about] +deps = -rrequirements-about.txt +basepython = python3.5 commands = python {toxinidir}/bin/display-sighandlers.py python {toxinidir}/bin/display-terminalinfo.py + python {toxinidir}/bin/display-fpathconf.py + python {toxinidir}/bin/display-maxcanon.py [testenv:sa] basepython = python2.7 deps = -rrequirements-analysis.txt + -rrequirements-about.txt commands = python -m compileall -fq {toxinidir}/blessed {envbindir}/prospector \ --die-on-tool-error \ From f2ae1ba4728a2ec839868ae4f62d97ddabee433c Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 9 Oct 2015 18:18:48 -0700 Subject: [PATCH 285/459] Document OS and python version requirements simply, specify our test environment as the requirements. Hopefully, this prevents people from diving too far from the Windows environment at the moment! --- docs/intro.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/intro.rst b/docs/intro.rst index a106b537..454d8cd4 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -123,6 +123,12 @@ The same program with *Blessed* is simply:: with term.location(0, term.height - 1): print('This is' + term.underline('underlined') + '!') +Requirements +------------ + +Blessed is compatible with Python 2.7, 3.4, and 3.5 on Debian Linux, Mac OSX, +and FreeBSD. + Further Documentation --------------------- From 4de9ef78b22aecb4caaf55ccbf8b1101999818d5 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 9 Oct 2015 18:28:08 -0700 Subject: [PATCH 286/459] bugfix: ensure COVERALLS_REPO_TOKEN is passthru --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 5fb7241c..561bd77d 100644 --- a/tox.ini +++ b/tox.ini @@ -25,6 +25,7 @@ commands = {toxinidir}/tools/custom-combine.py # CI buildhcain target [testenv:coveralls] +passenv = COVERALLS_REPO_TOKEN deps = coveralls commands = coveralls From ab61b580e6011f31c97b507d167e8a74019f7760 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 9 Oct 2015 19:37:36 -0700 Subject: [PATCH 287/459] rename 'keyboard.prefixes' => get_leading_prefixes And fully docstring what this function is for and used by, as this becomes a permanent API member, ah! --- blessed/keyboard.py | 18 +++++++++++++++--- blessed/terminal.py | 4 ++-- blessed/tests/test_keyboard.py | 12 ++++++------ 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/blessed/keyboard.py b/blessed/keyboard.py index 10c2e0b0..fb1f598d 100644 --- a/blessed/keyboard.py +++ b/blessed/keyboard.py @@ -189,13 +189,25 @@ def get_keyboard_sequences(term): (seq, sequence_map[seq]) for seq in sorted( sequence_map.keys(), key=len, reverse=True))) -def prefixes(sequences): - """prefixes(iterable of strings) -> (set) - Returns a set of proper prefixes of an iterable of strings +def get_leading_prefixes(sequences): + """ + Return a set of proper prefixes for given sequence of strings. + + :param iterable sequences + :rtype: set + + Given an iterable of strings, all textparts leading up to the final + string is returned as a unique set. This function supports the + :meth:`~.Terminal.inkey` method by determining whether the given + input is a sequence that **may** lead to a final matching pattern. + + >>> prefixes(['abc', 'abdf', 'e', 'jkl']) + set([u'a', u'ab', u'abd', u'j', u'jk']) """ return set(seq[:i] for seq in sequences for i in range(1, len(seq))) + def resolve_sequence(text, mapper, codes): r""" Return :class:`Keystroke` instance for given sequence ``text``. diff --git a/blessed/terminal.py b/blessed/terminal.py index fb49dafb..19af2d1e 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -50,9 +50,9 @@ ) from .keyboard import (get_keyboard_sequences, + get_leading_prefixes, get_keyboard_codes, resolve_sequence, - prefixes, ) @@ -209,7 +209,7 @@ def __init__(self, kind=None, stream=None, force_styling=False): # Build database of sequence <=> KEY_NAME. self._keymap = get_keyboard_sequences(self) # build set of prefixes of sequences - self._keymap_prefixes = prefixes(self._keymap) + self._keymap_prefixes = get_leading_prefixes(self._keymap) self._keyboard_buf = collections.deque() if self._keyboard_fd is not None: diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index 568d0f4d..b9883b1a 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -832,12 +832,12 @@ def test_resolve_sequence(): assert repr(ks) in (u"KEY_L", "KEY_L") -def test_prefixes(): - "Test prefixes" - from blessed.keyboard import prefixes - keys = {u'abc': '1', u'abdf': '2', u'e': '3'} - pfs = prefixes(keys) - assert pfs == set([u'a', u'ab', u'abd']) +def test_keyboard_prefixes(): + "Test keyboard.prefixes" + from blessed.keyboard import get_leading_prefixes + keys = ['abc', 'abdf', 'e', 'jkl'] + pfs = get_leading_prefixes(keys) + assert pfs == set([u'a', u'ab', u'abd', u'j', u'jk']) def test_keypad_mixins_and_aliases(): From c03c1a2f9029707c591ca43d675aba0e0508c268 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 9 Oct 2015 19:45:33 -0700 Subject: [PATCH 288/459] advance version to 1.11.0 and set changelog --- docs/history.rst | 5 +++++ version.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/history.rst b/docs/history.rst index c2ba17f2..279c768d 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -1,5 +1,10 @@ Version History =============== +1.11 + * enhancement: :meth:`~.Terminal.inkey` can return more quickly for + combinations such as ``Alt + Z`` when ``MetaSendsEscape`` is enabled, + :ghissue:`30`. + 1.10 * workaround: provide ``sc`` and ``rc`` for Terminals of ``kind='ansi'``, repairing :meth:`~.Terminal.location` :ghissue:`44`. diff --git a/version.json b/version.json index 4d232ac3..c16d5563 100644 --- a/version.json +++ b/version.json @@ -1 +1 @@ -{"version": "1.10.0"} +{"version": "1.11.0"} From 6512d9e85ae877b1c406a80538d71ea9f50260a6 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 9 Oct 2015 20:13:03 -0700 Subject: [PATCH 289/459] introduce init_subproc_coverage() function --- blessed/tests/accessories.py | 32 ++++++++++------- blessed/tests/test_keyboard.py | 66 ++++++++-------------------------- 2 files changed, 33 insertions(+), 65 deletions(-) diff --git a/blessed/tests/accessories.py b/blessed/tests/accessories.py index 8253ae7a..a84522e4 100644 --- a/blessed/tests/accessories.py +++ b/blessed/tests/accessories.py @@ -49,6 +49,21 @@ all_terms_params = 'xterm screen ansi linux'.split() +def init_subproc_coverage(run_note): + try: + import coverage + except ImportError: + return None + _coveragerc = os.path.join( + os.path.dirname(__file__), + os.pardir, os.pardir, + '.coveragerc') + cov = coverage.Coverage(config_file=_coveragerc) + cov.set_option("run:note", run_note) + cov.start() + return cov + + class as_subprocess(object): """This helper executes test cases in a child process, avoiding a python-internal bug of _curses: setupterm() @@ -68,19 +83,10 @@ def __call__(self, *args, **kwargs): # if failed, causing a non-zero exit code, using the # protected _exit() function of ``os``; to prevent the # 'SystemExit' exception from being thrown. - cov = None - try: - import coverage - _coveragerc = os.path.join(os.path.dirname(__file__), - os.pardir, os.pardir, - '.coveragerc') - cov = coverage.Coverage(config_file=_coveragerc) - cov.set_option("run:note", - "@as_subprocess-{0};{1}(*{2}, **{3})".format( - os.getpid(), self.func, args, kwargs)) - cov.start() - except ImportError: - pass + cov = init_subproc_coverage( + "@as_subprocess-{pid};{func_name}(*{args}, **{kwargs})" + .format(pid=os.getpid(), func_name=self.func, + args=args, kwargs=kwargs)) try: self.func(*args, **kwargs) except Exception: diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index b9883b1a..8b11668e 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -14,6 +14,7 @@ # local from .accessories import ( + init_subproc_coverage, read_until_eof, read_until_semaphore, SEND_SEMAPHORE, @@ -40,10 +41,7 @@ def test_kbhit_interrupted(): "kbhit() should not be interrupted with a signal handler." pid, master_fd = pty.fork() if pid == 0: - try: - cov = __import__('cov_core_init').init() - except ImportError: - cov = None + cov = init_subproc_coverage('test_kbhit_interrupted') # child pauses, writes semaphore and begins awaiting input global got_sigwinch @@ -85,10 +83,7 @@ def test_kbhit_interrupted_nonetype(): "kbhit() should also allow interruption with timeout of None." pid, master_fd = pty.fork() if pid == 0: - try: - cov = __import__('cov_core_init').init() - except ImportError: - cov = None + cov = init_subproc_coverage('test_kbhit_interrupted_nonetype') # child pauses, writes semaphore and begins awaiting input global got_sigwinch @@ -249,10 +244,7 @@ def test_keystroke_0s_cbreak_with_input(): "0-second keystroke with input; Keypress should be immediately returned." pid, master_fd = pty.fork() if pid == 0: - try: - cov = __import__('cov_core_init').init() - except ImportError: - cov = None + cov = init_subproc_coverage('test_keystroke_0s_cbreak_with_input') # child pauses, writes semaphore and begins awaiting input term = TestTerminal() read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) @@ -282,10 +274,7 @@ def test_keystroke_cbreak_with_input_slowly(): "0-second keystroke with input; Keypress should be immediately returned." pid, master_fd = pty.fork() if pid == 0: - try: - cov = __import__('cov_core_init').init() - except ImportError: - cov = None + cov = init_subproc_coverage('test_keystroke_cbreak_with_input_slowly') # child pauses, writes semaphore and begins awaiting input term = TestTerminal() read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) @@ -325,10 +314,7 @@ def test_keystroke_0s_cbreak_multibyte_utf8(): # utf-8 bytes represent "latin capital letter upsilon". pid, master_fd = pty.fork() if pid == 0: # child - try: - cov = __import__('cov_core_init').init() - except ImportError: - cov = None + cov = init_subproc_coverage('test_keystroke_0s_cbreak_multibyte_utf8') term = TestTerminal() read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) os.write(sys.__stdout__.fileno(), SEMAPHORE) @@ -358,10 +344,7 @@ def test_keystroke_0s_raw_input_ctrl_c(): "0-second keystroke with raw allows receiving ^C." pid, master_fd = pty.fork() if pid == 0: # child - try: - cov = __import__('cov_core_init').init() - except ImportError: - cov = None + cov = init_subproc_coverage('test_keystroke_0s_raw_input_ctrl_c') term = TestTerminal() read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) with term.raw(): @@ -391,10 +374,7 @@ def test_keystroke_0s_cbreak_sequence(): "0-second keystroke with multibyte sequence; should decode immediately." pid, master_fd = pty.fork() if pid == 0: # child - try: - cov = __import__('cov_core_init').init() - except ImportError: - cov = None + cov = init_subproc_coverage('test_keystroke_0s_cbreak_sequence') term = TestTerminal() os.write(sys.__stdout__.fileno(), SEMAPHORE) with term.cbreak(): @@ -423,10 +403,7 @@ def test_keystroke_1s_cbreak_with_input(): "1-second keystroke w/multibyte sequence; should return after ~1 second." pid, master_fd = pty.fork() if pid == 0: # child - try: - cov = __import__('cov_core_init').init() - except ImportError: - cov = None + cov = init_subproc_coverage('test_keystroke_1s_cbreak_with_input') term = TestTerminal() os.write(sys.__stdout__.fileno(), SEMAPHORE) with term.cbreak(): @@ -457,10 +434,7 @@ def test_esc_delay_cbreak_035(): "esc_delay will cause a single ESC (\\x1b) to delay for 0.35." pid, master_fd = pty.fork() if pid == 0: # child - try: - cov = __import__('cov_core_init').init() - except ImportError: - cov = None + cov = init_subproc_coverage('test_esc_delay_cbreak_035') term = TestTerminal() os.write(sys.__stdout__.fileno(), SEMAPHORE) with term.cbreak(): @@ -494,10 +468,7 @@ def test_esc_delay_cbreak_135(): "esc_delay=1.35 will cause a single ESC (\\x1b) to delay for 1.35." pid, master_fd = pty.fork() if pid == 0: # child - try: - cov = __import__('cov_core_init').init() - except ImportError: - cov = None + cov = init_subproc_coverage('test_esc_delay_cbreak_135') term = TestTerminal() os.write(sys.__stdout__.fileno(), SEMAPHORE) with term.cbreak(): @@ -529,10 +500,7 @@ def test_esc_delay_cbreak_timout_0(): """esc_delay still in effect with timeout of 0 ("nonblocking").""" pid, master_fd = pty.fork() if pid == 0: # child - try: - cov = __import__('cov_core_init').init() - except ImportError: - cov = None + cov = init_subproc_coverage('test_esc_delay_cbreak_timout_0') term = TestTerminal() os.write(sys.__stdout__.fileno(), SEMAPHORE) with term.cbreak(): @@ -564,10 +532,7 @@ def test_esc_delay_cbreak_nonprefix_sequence(): "ESC a (\\x1ba) will return an ESC immediately" pid, master_fd = pty.fork() if pid is 0: # child - try: - cov = __import__('cov_core_init').init() - except ImportError: - cov = None + cov = init_subproc_coverage('test_esc_delay_cbreak_nonprefix_sequence') term = TestTerminal() os.write(sys.__stdout__.fileno(), SEMAPHORE) with term.cbreak(): @@ -601,10 +566,7 @@ def test_esc_delay_cbreak_prefix_sequence(): "An unfinished multibyte sequence (\\x1b[) will delay an ESC by .35 " pid, master_fd = pty.fork() if pid is 0: # child - try: - cov = __import__('cov_core_init').init() - except ImportError: - cov = None + cov = init_subproc_coverage('test_esc_delay_cbreak_prefix_sequence') term = TestTerminal() os.write(sys.__stdout__.fileno(), SEMAPHORE) with term.cbreak(): From 26fdc2c29bf859ad4c85a47c5acba8b93fe73e22 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 9 Oct 2015 20:18:41 -0700 Subject: [PATCH 290/459] do not notify irc from travis-ci any longer --- .travis.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 41c84d29..dbb9eb78 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,11 +25,11 @@ notifications: - contact@jeffquast.com on_success: change on_failure: change - irc: - channels: - - "irc.servercentral.net#1984" - template: - - "%{repository}(%{branch}): %{message} (%{duration}) %{build_url}" - skip_join: true - on_success: change - on_failure: change +# irc: +# channels: +# - "irc.servercentral.net#1984" +# template: +# - "%{repository}(%{branch}): %{message} (%{duration}) %{build_url}" +# skip_join: true +# on_success: change +# on_failure: change From ed420d5f1dd5cc78779cf6de48fbb88a29daf3f2 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 9 Oct 2015 20:44:42 -0700 Subject: [PATCH 291/459] no coverage for (rare!) timeout-after-interrupt --- blessed/terminal.py | 4 ++-- tools/custom-combine.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/blessed/terminal.py b/blessed/terminal.py index 19af2d1e..8b3e02a2 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -764,8 +764,8 @@ def kbhit(self, timeout=None, **_kwargs): if timeout > 0: continue # no time remains after handling exception (rare) - ready_r = [] - break + ready_r = [] # pragma: no cover + break # pragma: no cover else: break diff --git a/tools/custom-combine.py b/tools/custom-combine.py index 7331d301..fdaaddb1 100755 --- a/tools/custom-combine.py +++ b/tools/custom-combine.py @@ -50,6 +50,7 @@ def main(): _, no_stmts, no_miss, _ = total_line.split(None, 3) no_covered = int(no_stmts) - int(no_miss) + print(fout.getvalue()) print("##teamcity[buildStatisticValue " "key='CodeCoverageAbsLTotal' " "value='{0}']".format(no_stmts)) From d6f9159def5571ecc2d3efe2addbb5af74e01b35 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 9 Oct 2015 22:45:08 -0700 Subject: [PATCH 292/459] Migrate Jim Allman's nested styling enhancement @jimallman brings us this convenience of allowing existing strings to be joined as a call parameter argument to a FormattingString, allowing nestation: This was rejected upstream as https://github.com/erikrose/blessings/pull/45 ```python t.red('This is ', t.bold('extremely important'), ' information!') t.green('foo', t.bold('bar', t.underline('baz'), 'herp'), 'derp') ``` --- blessed/formatters.py | 67 ++++++++++++++++++++------------ blessed/tests/test_formatters.py | 45 ++++++++++++++++++--- blessed/tests/test_sequences.py | 45 +++++++++++++++++++-- docs/history.rst | 2 + 4 files changed, 126 insertions(+), 33 deletions(-) diff --git a/blessed/formatters.py b/blessed/formatters.py index 088a115b..c581ea87 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -112,18 +112,18 @@ class ParameterizingProxyString(six.text_type): given positional ``*args`` of :meth:`ParameterizingProxyString.__call__` into a terminal sequence. - For example: - - >>> from blessed import Terminal - >>> term = Terminal('screen') - >>> hpa = ParameterizingString(term.hpa, term.normal, 'hpa') - >>> hpa(9) - u'' - >>> fmt = u'\x1b[{0}G' - >>> fmt_arg = lambda *arg: (arg[0] + 1,) - >>> hpa = ParameterizingProxyString((fmt, fmt_arg), term.normal, 'hpa') - >>> hpa(9) - u'\x1b[10G' + For example:: + + >>> from blessed import Terminal + >>> term = Terminal('screen') + >>> hpa = ParameterizingString(term.hpa, term.normal, 'hpa') + >>> hpa(9) + u'' + >>> fmt = u'\x1b[{0}G' + >>> fmt_arg = lambda *arg: (arg[0] + 1,) + >>> hpa = ParameterizingProxyString((fmt, fmt_arg), term.normal, 'hpa') + >>> hpa(9) + u'\x1b[10G' """ def __new__(cls, *args): @@ -208,13 +208,13 @@ class FormattingString(six.text_type): directly, or as a callable. When used directly, it simply emits the given terminal sequence. When used as a callable, it wraps the given (string) argument with the 2nd argument used by the class - constructor. + constructor:: - >>> style = FormattingString(term.bright_blue, term.normal) - >>> print(repr(style)) - u'\x1b[94m' - >>> style('Big Blue') - u'\x1b[94mBig Blue\x1b(B\x1b[m' + >>> style = FormattingString(term.bright_blue, term.normal) + >>> print(repr(style)) + u'\x1b[94m' + >>> style('Big Blue') + u'\x1b[94mBig Blue\x1b(B\x1b[m' """ def __new__(cls, *args): @@ -229,11 +229,30 @@ def __new__(cls, *args): new._normal = len(args) > 1 and args[1] or u'' return new - def __call__(self, text): + def __call__(self, *args): """Return ``text`` joined by ``sequence`` and ``normal``.""" - if len(self): - return u''.join((self, text, self._normal)) - return text + # Jim Allman brings us this convenience of allowing existing + # unicode strings to be joined as a call parameter to a formatting + # string result, allowing nestation: + # + # >>> t.red('This is ', t.bold('extremely'), ' dangerous!') + for idx, ucs_part in enumerate(args): + if not isinstance(ucs_part, six.string_types): + raise TypeError("Positional argument #{idx} is {is_type} " + "expected any of {expected_types}: " + "{ucs_part!r}".format( + idx=idx, ucs_part=ucs_part, + is_type=type(ucs_part), + expected_types=six.string_types, + )) + postfix = u'' + if len(self) and self._normal: + postfix = self._normal + _refresh = self._normal + self + args = [_refresh.join(ucs_part.split(self._normal)) + for ucs_part in args] + + return self + u''.join(args) + postfix class NullCallableString(six.text_type): @@ -262,7 +281,7 @@ def __call__(self, *args): the first arg, acting in place of :class:`FormattingString` without any attributes. """ - if len(args) != 1 or isinstance(args[0], int): + if len(args) == 0 or isinstance(args[0], int): # I am acting as a ParameterizingString. # tparm can take not only ints but also (at least) strings as its @@ -283,7 +302,7 @@ def __call__(self, *args): # without color support, so turtles all the way down: we return # another instance. return NullCallableString() - return args[0] + return u''.join(args) def split_compound(compound): diff --git a/blessed/tests/test_formatters.py b/blessed/tests/test_formatters.py index 295d7290..88e08008 100644 --- a/blessed/tests/test_formatters.py +++ b/blessed/tests/test_formatters.py @@ -1,7 +1,11 @@ # -*- coding: utf-8 -*- """Tests string formatting functions.""" +# std import curses + +# 3rd-party import mock +import pytest def test_parameterizing_string_args_unspecified(monkeypatch): @@ -70,7 +74,7 @@ def test_parameterizing_string_args(monkeypatch): def test_parameterizing_string_type_error(monkeypatch): - """Test formatters.ParameterizingString raising TypeError""" + """Test formatters.ParameterizingString raising TypeError.""" from blessed.formatters import ParameterizingString def tparm_raises_TypeError(*args): @@ -106,7 +110,7 @@ def tparm_raises_TypeError(*args): def test_formattingstring(monkeypatch): - """Test formatters.FormattingString""" + """Test simple __call__ behavior of formatters.FormattingString.""" from blessed.formatters import FormattingString # given, with arg @@ -117,11 +121,41 @@ def test_formattingstring(monkeypatch): assert str(pstr) == u'attr' assert pstr('text') == u'attrtextnorm' - # given, without arg + # given, with empty attribute pstr = FormattingString(u'', u'norm') assert pstr('text') == u'text' +def test_nested_formattingstring(monkeypatch): + """Test nested __call__ behavior of formatters.FormattingString.""" + from blessed.formatters import FormattingString + + # given, with arg + pstr = FormattingString(u'a1-', u'n-') + zstr = FormattingString(u'a2-', u'n-') + + # exercise __call__ + assert pstr('x-', zstr('f-'), 'q-') == 'a1-x-a2-f-n-a1-q-n-' + + +def test_nested_formattingstring_type_error(monkeypatch): + """Test formatters.FormattingString raising TypeError.""" + from blessed.formatters import FormattingString + + # given, + pstr = FormattingString(u'a-', u'n-') + expected_msg = ( + "Positional argument #1 is {0} expected any of " + .format(type(1))) + + # exercise, + with pytest.raises(TypeError) as err: + pstr('text', 1, '...') + + # verify, + assert expected_msg in '{0}'.format(err) + + def test_nullcallablestring(monkeypatch): """Test formatters.NullCallableString""" from blessed.formatters import (NullCallableString) @@ -129,11 +163,10 @@ def test_nullcallablestring(monkeypatch): # given, with arg pstr = NullCallableString() - # excersize __call__, + # exercise __call__, assert str(pstr) == u'' assert pstr('text') == u'text' - assert pstr('text', 1) == u'' - assert pstr('text', 'moretext') == u'' + assert pstr('text', 'moretext') == u'textmoretext' assert pstr(99, 1) == u'' assert pstr() == u'' assert pstr(0) == u'' diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index 24c5c3d8..17aab44d 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -441,6 +441,33 @@ def child(kind): child(all_terms) +def test_nested_formatting(all_terms): + """Test complex nested compound formatting, wow!""" + @as_subprocess + def child(kind): + t = TestTerminal(kind=kind) + + # Test deeply nested styles + given = t.green('-a-', t.bold('-b-', t.underline('-c-'), + '-d-'), + '-e-') + expected = u''.join(( + t.green, '-a-', t.bold, '-b-', t.underline, '-c-', t.normal, + t.green, t.bold, '-d-', + t.normal, t.green, '-e-', t.normal)) + assert given == expected + + # Test off-and-on nested styles + given = t.green('off ', t.underline('ON'), + ' off ', t.underline('ON'), + ' off') + expected = u''.join(( + t.green, 'off ', t.underline, 'ON', + t.normal, t.green , ' off ', t.underline, 'ON', + t.normal, t.green, ' off', t.normal)) + assert given == expected + + def test_formatting_functions_without_tty(all_terms): """Test crazy-ass formatting wrappers when there's no tty.""" @as_subprocess @@ -451,6 +478,20 @@ def child(kind): # Test non-ASCII chars, no longer really necessary: assert (t.bold_green(u'boö') == u'boö') assert (t.bold_underline_green_on_red('loo') == u'loo') + + # Test deeply nested styles + given = t.green('-a-', t.bold('-b-', t.underline('-c-'), + '-d-'), + '-e-') + expected = u'-a--b--c--d--e-' + assert given == expected + + # Test off-and-on nested styles + given = t.green('off ', t.underline('ON'), + ' off ', t.underline('ON'), + ' off') + expected = u'off ON off ON off' + assert given == expected assert (t.on_bright_red_bold_bright_green_underline('meh') == u'meh') child(all_terms) @@ -502,9 +543,7 @@ def child(kind): assert (t.move(1 == 2) == '') assert (t.move_x(1) == '') assert (t.bold() == '') - assert (t.bold('', 'x', 'huh?') == '') - assert (t.bold('', 9876) == '') - assert (t.uhh(9876) == '') + assert (t.bold('', 'x', 'huh?') == 'xhuh?') assert (t.clear('x') == 'x') child(all_terms) diff --git a/docs/history.rst b/docs/history.rst index 279c768d..831736b2 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -4,6 +4,8 @@ Version History * enhancement: :meth:`~.Terminal.inkey` can return more quickly for combinations such as ``Alt + Z`` when ``MetaSendsEscape`` is enabled, :ghissue:`30`. + * enhancement: :class:`~.FormattingString` may now be nested, such as + ``t.red('red', t.underline('rum'))``, :ghissue:`61` 1.10 * workaround: provide ``sc`` and ``rc`` for Terminals of ``kind='ansi'``, From 25a9b089c5556893ff252af12f4f946c4d81649a Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 9 Oct 2015 23:05:33 -0700 Subject: [PATCH 293/459] bugfix url for blessed's setup() call --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e51193a4..7c10f629 100755 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ def _get_long_description(fname): author_email='contact@jeffquast.com', license='MIT', packages=['blessed', 'blessed.tests'], - url='https://github.com/erikrose/blessed', + url='https://github.com/jquast/blessed', include_package_data=True, zip_safe=True, classifiers=[ From 35faa42c2788d2ccfaae631b164d062e5df0a1da Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 9 Oct 2015 23:58:04 -0700 Subject: [PATCH 294/459] tox.ini: Use pytest -- -x unless -- is specified fork() is dangerous, the test runner forks quite often under dangerous circumstances -- our CI agents need to stop immediately by default with basic arguments. For developers who wish to see all errors, they may simply run:: tox -epy27,py34 -- --- tox.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 561bd77d..161c01c0 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,6 @@ skip_missing_interpreters = true [testenv] whitelist_externals = cp - echo setenv = PYTHONIOENCODING=UTF8 passenv = TEST_QUICK TEST_FULL deps = -rrequirements-tests.txt @@ -12,7 +11,7 @@ commands = {envbindir}/py.test \ --strict --verbose --verbose --color=yes \ --junit-xml=results.{envname}.xml \ --cov blessed blessed/tests \ - {posargs} + {posargs:-x} coverage combine cp {toxinidir}/.coverage \ {toxinidir}/._coverage.{envname}.{env:TEAMCITY_BUILDCONFNAME:xxx} From d96ed0b681a88cab0e659bb16bb9c773e9776ee5 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 10 Oct 2015 00:04:56 -0700 Subject: [PATCH 295/459] include all requirements files --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index c2a28ff6..5a2ca9ef 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,6 @@ include docs/*.rst include LICENSE include version.json -include requirements.txt +include *.txt include tox.ini include blessed/tests/wall.ans From 314b3958c6d5b80605d82e109b45a1d033c693a5 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 11 Oct 2015 00:33:44 -0700 Subject: [PATCH 296/459] sphinx nit: prefer ':arg' over ':param' creates briefer phrases and joins a few phrases reducing LOC --- blessed/formatters.py | 22 +++++++-------- blessed/keyboard.py | 12 ++++---- blessed/sequences.py | 64 +++++++++++++++++++++---------------------- blessed/terminal.py | 24 ++++++++-------- 4 files changed, 61 insertions(+), 61 deletions(-) diff --git a/blessed/formatters.py b/blessed/formatters.py index c581ea87..80ed56cf 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -23,7 +23,7 @@ def _make_compoundables(colors): """ Return given set ``colors`` along with all "compoundable" attributes. - :param set colors: set of color names as string. + :arg set colors: set of color names as string. :rtype: set """ _compoundables = set('bold underline reverse blink dim italic shadow ' @@ -163,8 +163,8 @@ def get_proxy_string(term, attr): """ Proxy and return callable string for proxied attributes. - :param Terminal term: :class:`~.Terminal` instance. - :param str attr: terminal capability name that may be proxied. + :arg Terminal term: :class:`~.Terminal` instance. + :arg str attr: terminal capability name that may be proxied. :rtype: None or :class:`ParameterizingProxyString`. :returns: :class:`ParameterizingProxyString` for some attributes of some terminal types that support it, where the terminfo(5) @@ -312,8 +312,8 @@ def split_compound(compound): >>> split_compound('bold_underline_bright_blue_on_red') ['bold', 'underline', 'bright_blue', 'on_red'] - :param str compound: a string that may contain compounds, - separated by underline (``_``). + :arg str compound: a string that may contain compounds, separated by + underline (``_``). :rtype: list """ merged_segs = [] @@ -331,8 +331,8 @@ def resolve_capability(term, attr): """ Resolve a raw terminal capability using :func:`tigetstr`. - :param Terminal term: :class:`~.Terminal` instance. - :param str attr: terminal capability name. + :arg Terminal term: :class:`~.Terminal` instance. + :arg str attr: terminal capability name. :returns: string of the given terminal capability named by ``attr``, which may be empty (u'') if not found or not supported by the given :attr:`~.Terminal.kind`. @@ -352,8 +352,8 @@ def resolve_color(term, color): This function supports :func:`resolve_attribute`. - :param Terminal term: :class:`~.Terminal` instance. - :param str color: any string found in set :const:`COLORS`. + :arg Terminal term: :class:`~.Terminal` instance. + :arg str color: any string found in set :const:`COLORS`. :returns: a string class instance which emits the terminal sequence for the given color, and may be used as a callable to wrap the given string with such sequence. @@ -387,8 +387,8 @@ def resolve_attribute(term, attr): """ Resolve a terminal attribute name into a capability class. - :param Terminal term: :class:`~.Terminal` instance. - :param str attr: Sugary, ordinary, or compound formatted terminal + :arg Terminal term: :class:`~.Terminal` instance. + :arg str attr: Sugary, ordinary, or compound formatted terminal capability, such as "red_on_white", "normal", "red", or "bold_on_black", respectively. :returns: a string class instance which emits the terminal sequence diff --git a/blessed/keyboard.py b/blessed/keyboard.py index fb1f598d..cb8ae8d5 100644 --- a/blessed/keyboard.py +++ b/blessed/keyboard.py @@ -120,7 +120,7 @@ def _alternative_left_right(term): r""" Determine and return mapping of left and right arrow keys sequences. - :param blessed.Terminal term: :class:`~.Terminal` instance. + :arg blessed.Terminal term: :class:`~.Terminal` instance. :rtype: dict This function supports :func:`get_terminal_sequences` to discover @@ -144,7 +144,7 @@ def get_keyboard_sequences(term): r""" Return mapping of keyboard sequences paired by keycodes. - :param blessed.Terminal term: :class:`~.Terminal` instance. + :arg blessed.Terminal term: :class:`~.Terminal` instance. :returns: mapping of keyboard unicode sequences paired by keycodes as integer. This is used as the argument ``mapper`` to the supporting function :func:`resolve_sequence`. @@ -194,7 +194,7 @@ def get_leading_prefixes(sequences): """ Return a set of proper prefixes for given sequence of strings. - :param iterable sequences + :arg iterable sequences :rtype: set Given an iterable of strings, all textparts leading up to the final @@ -217,10 +217,10 @@ def resolve_sequence(text, mapper, codes): :attr:`Keystroke.sequence` valued only ``u\x1b[D``. It is up to determine that ``xxx`` remains unresolved. - :param text: string of characters received from terminal input stream. - :param OrderedDict mapper: an OrderedDict of unicode multibyte sequences, + :arg text: string of characters received from terminal input stream. + :arg OrderedDict mapper: an OrderedDict of unicode multibyte sequences, such as u'\x1b[D' paired by their integer value (260) - :param dict codes: a :type:`dict` of integer values (such as 260) paired + :arg dict codes: a :type:`dict` of integer values (such as 260) paired by their mnemonic name, such as ``'KEY_LEFT'``. :rtype: Keystroke """ diff --git a/blessed/sequences.py b/blessed/sequences.py index 555eb2fe..28a62974 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -22,7 +22,7 @@ def _sort_sequences(regex_seqlist): """ Sort, filter, and return ``regex_seqlist`` in ascending order of length. - :param list regex_seqlist: list of strings. + :arg list regex_seqlist: list of strings. :rtype: list :returns: given list filtered and sorted. @@ -54,10 +54,10 @@ def _build_numeric_capability(term, cap, optional=False, by regular expression pattern ``\d``. Any other digits found are *not* replaced. - :param blessed.Terminal term: :class:`~.Terminal` instance. - :param str cap: terminal capability name. - :param int num: the numeric to use for parameterized capability. - :param int nparams: the number of parameters to use for capability. + :arg blessed.Terminal term: :class:`~.Terminal` instance. + :arg str cap: terminal capability name. + :arg int num: the numeric to use for parameterized capability. + :arg int nparams: the number of parameters to use for capability. :rtype: str :returns: regular expression for the given capability. """ @@ -80,10 +80,10 @@ def _build_any_numeric_capability(term, cap, num=99, nparams=1): r""" Return regular expression for capabilities containing any numerics. - :param blessed.Terminal term: :class:`~.Terminal` instance. - :param str cap: terminal capability name. - :param int num: the numeric to use for parameterized capability. - :param int nparams: the number of parameters to use for capability. + :arg blessed.Terminal term: :class:`~.Terminal` instance. + :arg str cap: terminal capability name. + :arg int num: the numeric to use for parameterized capability. + :arg int nparams: the number of parameters to use for capability. :rtype: str :returns: regular expression for the given capability. @@ -104,7 +104,7 @@ def get_movement_sequence_patterns(term): """ Get list of regular expressions for sequences that cause movement. - :param blessed.Terminal term: :class:`~.Terminal` instance. + :arg blessed.Terminal term: :class:`~.Terminal` instance. :rtype: list """ bnc = functools.partial(_build_numeric_capability, term) @@ -148,7 +148,7 @@ def get_wontmove_sequence_patterns(term): """ Get list of regular expressions for sequences not causing movement. - :param blessed.Terminal term: :class:`~.Terminal` instance. + :arg blessed.Terminal term: :class:`~.Terminal` instance. :rtype: list """ bnc = functools.partial(_build_numeric_capability, term) @@ -278,7 +278,7 @@ def init_sequence_patterns(term): returns a dictionary database of regular expressions, which is re-attached to the terminal by attributes of the same key-name. - :param blessed.Terminal term: :class:`~.Terminal` instance. + :arg blessed.Terminal term: :class:`~.Terminal` instance. :rtype: dict :returns: dictionary containing mappings of sequence "groups", containing a compiled regular expression which it matches: @@ -499,8 +499,8 @@ def __new__(cls, sequence_text, term): """ Class constructor. - :param sequence_text: A string that may contain sequences. - :param blessed.Terminal term: :class:`~.Terminal` instance. + :arg sequence_text: A string that may contain sequences. + :arg blessed.Terminal term: :class:`~.Terminal` instance. """ new = six.text_type.__new__(cls, sequence_text) new._term = term @@ -510,9 +510,9 @@ def ljust(self, width, fillchar=u' '): """ Return string containing sequences, left-adjusted. - :param int width: Total width given to right-adjust ``text``. If + :arg int width: Total width given to right-adjust ``text``. If unspecified, the width of the attached terminal is used (default). - :param str fillchar: String for padding right-of ``text``. + :arg str fillchar: String for padding right-of ``text``. :returns: String of ``text``, right-aligned by ``width``. :rtype: str """ @@ -524,9 +524,9 @@ def rjust(self, width, fillchar=u' '): """ Return string containing sequences, right-adjusted. - :param int width: Total width given to right-adjust ``text``. If + :arg int width: Total width given to right-adjust ``text``. If unspecified, the width of the attached terminal is used (default). - :param str fillchar: String for padding left-of ``text``. + :arg str fillchar: String for padding left-of ``text``. :returns: String of ``text``, right-aligned by ``width``. :rtype: str """ @@ -538,9 +538,9 @@ def center(self, width, fillchar=u' '): """ Return string containing sequences, centered. - :param int width: Total width given to center ``text``. If + :arg int width: Total width given to center ``text``. If unspecified, the width of the attached terminal is used (default). - :param str fillchar: String for padding left and right-of ``text``. + :arg str fillchar: String for padding left and right-of ``text``. :returns: String of ``text``, centered by ``width``. :rtype: str """ @@ -589,7 +589,7 @@ def strip(self, chars=None): """ Return string of sequences, leading, and trailing whitespace removed. - :param str chars: Remove characters in chars instead of whitespace. + :arg str chars: Remove characters in chars instead of whitespace. :rtype: str """ return self.strip_seqs().strip(chars) @@ -598,7 +598,7 @@ def lstrip(self, chars=None): """ Return string of all sequences and leading whitespace removed. - :param str chars: Remove characters in chars instead of whitespace. + :arg str chars: Remove characters in chars instead of whitespace. :rtype: str """ return self.strip_seqs().lstrip(chars) @@ -607,7 +607,7 @@ def rstrip(self, chars=None): """ Return string of all sequences and trailing whitespace removed. - :param str chars: Remove characters in chars instead of whitespace. + :arg str chars: Remove characters in chars instead of whitespace. :rtype: str """ return self.strip_seqs().rstrip(chars) @@ -689,8 +689,8 @@ def measure_length(ucs, term): r""" Return non-zero for string ``ucs`` that begins with a terminal sequence. - :param str ucs: String that may begin with a terminal sequence. - :param blessed.Terminal term: :class:`~.Terminal` instance. + :arg str ucs: String that may begin with a terminal sequence. + :arg blessed.Terminal term: :class:`~.Terminal` instance. :rtype: int :returns: length of the sequence beginning at ``ucs``, if any. Otherwise 0 if ``ucs`` does not begin with a terminal @@ -737,11 +737,11 @@ def termcap_distance(ucs, cap, unit, term): r""" Return distance of capabilities ``cub``, ``cub1``, ``cuf``, and ``cuf1``. - :param str ucs: Terminal sequence created using any of ``cub(n)``, - ``cub1``, ``cuf(n)``, or ``cuf1``. - :param str cap: ``cub`` or ``cuf`` only. - :param int unit: Unit multiplier, should always be ``1`` or ``-1``. - :param blessed.Terminal term: :class:`~.Terminal` instance. + :arg str ucs: Terminal sequence created using any of ``cub(n)``, ``cub1``, + ``cuf(n)``, or ``cuf1``. + :arg str cap: ``cub`` or ``cuf`` only. + :arg int unit: Unit multiplier, should always be ``1`` or ``-1``. + :arg blessed.Terminal term: :class:`~.Terminal` instance. :rtype: int :returns: the printable distance determined by the given sequence. If the given sequence does not match any of the ``cub`` or ``cuf`` @@ -780,14 +780,14 @@ def horizontal_distance(ucs, term): r""" Determine the horizontal distance of single terminal sequence, ``ucs``. - :param ucs: terminal sequence, which may be any of the following: + :arg ucs: terminal sequence, which may be any of the following: - move_right (fe. ``[C``): returns value ``(n)``. - move left (fe. ``[D``): returns value ``-(n)``. - backspace (``\b``) returns value -1. - tab (``\t``) returns value 8. - :param blessed.Terminal term: :class:`~.Terminal` instance. + :arg blessed.Terminal term: :class:`~.Terminal` instance. :rtype: int .. note:: Tabstop (``\t``) cannot be correctly calculated, as the relative diff --git a/blessed/terminal.py b/blessed/terminal.py index 8b3e02a2..1b0d13eb 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -109,25 +109,24 @@ def __init__(self, kind=None, stream=None, force_styling=False): """ Initialize the terminal. - :param str kind: A terminal string as taken by - :func:`curses.setupterm`. Defaults to the value of the ``TERM`` - environment variable. + :arg str kind: A terminal string as taken by :func:`curses.setupterm`. + Defaults to the value of the ``TERM`` environment variable. .. note:: Terminals withing a single process must share a common ``kind``. See :obj:`_CUR_TERM`. - :param file stream: A file-like object representing the Terminal - output. Defaults to the original value of :obj:`sys.__stdout__`, - like :func:`curses.initscr` does. + :arg file stream: A file-like object representing the Terminal output. + Defaults to the original value of :obj:`sys.__stdout__`, like + :func:`curses.initscr` does. If ``stream`` is not a tty, empty Unicode strings are returned for all capability values, so things like piping your program output to a pipe or file does not emit terminal sequences. - :param bool force_styling: Whether to force the emission of - capabilities even if :obj:`sys.__stdout__` does not seem to be - connected to a terminal. If you want to force styling to not - happen, use ``force_styling=None``. + :arg bool force_styling: Whether to force the emission of capabilities + even if :obj:`sys.__stdout__` does not seem to be connected to a + terminal. If you want to force styling to not happen, use + ``force_styling=None``. This comes in handy if users are trying to pipe your output through something like ``less -r`` or build systems which support decoding @@ -253,7 +252,8 @@ def __getattr__(self, attr): if not self.does_styling: return NullCallableString() val = resolve_attribute(self, attr) - # Cache capability codes. + # Cache capability resolution: note this will prevent this + # __getattr__ method for being called again. That's the idea! setattr(self, attr, val) return val @@ -311,7 +311,7 @@ def _winsize(fd): :mod:`fcntl`, or :mod:`tty`, window size of 80 columns by 25 rows is always returned. - :param int fd: file descriptor queries for its window size. + :arg int fd: file descriptor queries for its window size. :raises IOError: the file descriptor ``fd`` is not a terminal. :rtype: WINSZ From d1f86392787ae1620bade5f6faf8f9f758c0cb35 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 11 Oct 2015 00:34:14 -0700 Subject: [PATCH 297/459] spellfix comment excersize -> exercise --- blessed/tests/test_formatters.py | 34 ++++++++++++++++---------------- blessed/tests/test_sequences.py | 9 ++++++--- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/blessed/tests/test_formatters.py b/blessed/tests/test_formatters.py index 88e08008..01762d33 100644 --- a/blessed/tests/test_formatters.py +++ b/blessed/tests/test_formatters.py @@ -22,18 +22,18 @@ def test_parameterizing_string_args_unspecified(monkeypatch): # given, pstr = ParameterizingString(u'') - # excersize __new__ + # exercise __new__ assert str(pstr) == u'' assert pstr._normal == u'' assert pstr._name == u'' - # excersize __call__ + # exercise __call__ zero = pstr(0) assert type(zero) is FormattingString assert zero == u'~0' assert zero('text') == u'~0text' - # excersize __call__ with multiple args + # exercise __call__ with multiple args onetwo = pstr(1, 2) assert type(onetwo) is FormattingString assert onetwo == u'~1~2' @@ -55,18 +55,18 @@ def test_parameterizing_string_args(monkeypatch): # given, pstr = ParameterizingString(u'cap', u'norm', u'seq-name') - # excersize __new__ + # exercise __new__ assert str(pstr) == u'cap' assert pstr._normal == u'norm' assert pstr._name == u'seq-name' - # excersize __call__ + # exercise __call__ zero = pstr(0) assert type(zero) is FormattingString assert zero == u'cap~0' assert zero('text') == u'cap~0textnorm' - # excersize __call__ with multiple args + # exercise __call__ with multiple args onetwo = pstr(1, 2) assert type(onetwo) is FormattingString assert onetwo == u'cap~1~2' @@ -116,7 +116,7 @@ def test_formattingstring(monkeypatch): # given, with arg pstr = FormattingString(u'attr', u'norm') - # excersize __call__, + # exercise __call__, assert pstr._normal == u'norm' assert str(pstr) == u'attr' assert pstr('text') == u'attrtextnorm' @@ -193,7 +193,7 @@ def test_resolve_capability(monkeypatch): term = mock.Mock() term._sugar = dict(mnemonic='xyz') - # excersize + # exercise assert resolve_capability(term, 'mnemonic') == u'seq-xyz' assert resolve_capability(term, 'natural') == u'seq-natural' @@ -201,7 +201,7 @@ def test_resolve_capability(monkeypatch): tigetstr_none = lambda attr: None monkeypatch.setattr(curses, 'tigetstr', tigetstr_none) - # excersize, + # exercise, assert resolve_capability(term, 'natural') == u'' # given, where does_styling is False @@ -210,7 +210,7 @@ def raises_exception(*args): term.does_styling = False monkeypatch.setattr(curses, 'tigetstr', raises_exception) - # excersize, + # exercise, assert resolve_capability(term, 'natural') == u'' @@ -230,13 +230,13 @@ def test_resolve_color(monkeypatch): term.number_of_colors = -1 term.normal = 'seq-normal' - # excersize, + # exercise, red = resolve_color(term, 'red') assert type(red) == FormattingString assert red == u'seq-1984' assert red('text') == u'seq-1984textseq-normal' - # excersize bold, +8 + # exercise bold, +8 bright_red = resolve_color(term, 'bright_red') assert type(bright_red) == FormattingString assert bright_red == u'seq-1992' @@ -245,13 +245,13 @@ def test_resolve_color(monkeypatch): # given, terminal without color term.number_of_colors = 0 - # excersize, + # exercise, red = resolve_color(term, 'red') assert type(red) == NullCallableString assert red == u'' assert red('text') == u'text' - # excesize bold, + # exercise bold, bright_red = resolve_color(term, 'bright_red') assert type(bright_red) == NullCallableString assert bright_red == u'' @@ -349,7 +349,7 @@ def test_resolve_attribute_recursive_compoundables(monkeypatch): # given, pstr = resolve_attribute(term, 'bright_blue_on_red') - # excersize, + # exercise, assert type(pstr) == FormattingString assert str(pstr) == 'seq-6808seq-6502' assert pstr('text') == 'seq-6808seq-6502textseq-normal' @@ -378,13 +378,13 @@ def test_pickled_parameterizing_string(monkeypatch): # multiprocessing Pipe implicitly pickles. r, w = Pipe() - # excersize picklability of ParameterizingString + # exercise picklability of ParameterizingString for proto_num in range(pickle.HIGHEST_PROTOCOL): assert pstr == pickle.loads(pickle.dumps(pstr, protocol=proto_num)) w.send(pstr) r.recv() == pstr - # excersize picklability of FormattingString + # exercise picklability of FormattingString # -- the return value of calling ParameterizingString zero = pstr(0) for proto_num in range(pickle.HIGHEST_PROTOCOL): diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index 17aab44d..ae71ed29 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -115,10 +115,13 @@ def child(kind): # that cause false negatives, because their underlying curses library emits # some kind of "warning" to stderr, which our @as_subprocess decorator # determines to be noteworthy enough to fail the test: + # # https://gist.github.com/jquast/7b90af251fe4000baa09 # - # so we chose only one of beautiful lineage: + # so we chose only one, known good value, of beautiful lineage: + # # http://terminals.classiccmp.org/wiki/index.php/Tektronix_4207 + child(kind='tek4207-s') @@ -560,7 +563,7 @@ def test_bnc_parameter_emits_warning(): fake_cap = lambda *args: u'NO-DIGIT' term.fake_cap = fake_cap - # excersize, + # exercise, try: _build_numeric_capability(term, 'fake_cap', base_num=1984) except UserWarning: @@ -582,7 +585,7 @@ def test_bna_parameter_emits_warning(): fake_cap = lambda *args: 'NO-DIGIT' term.fake_cap = fake_cap - # excersize, + # exercise, try: _build_any_numeric_capability(term, 'fake_cap') except UserWarning: From a31ca312f26236379c66fe74f0f30d6f0a5a6dee Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 11 Oct 2015 00:54:08 -0700 Subject: [PATCH 298/459] use newstyle formatting (drop %s, etc.) --- blessed/sequences.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/blessed/sequences.py b/blessed/sequences.py index 28a62974..68c0251f 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -70,9 +70,10 @@ def _build_numeric_capability(term, cap, optional=False, # search for matching ascii, n-1 through n+1 if str(num) in cap_re: # modify & return n to matching digit expression - cap_re = cap_re.replace(str(num), r'(\d+)%s' % (opt,)) + cap_re = cap_re.replace(str(num), r'(\d+){0}'.format(opt)) return cap_re - warnings.warn('Unknown parameter in %r (%r, %r)' % (cap, _cap, cap_re)) + warnings.warn('Unknown parameter in {0:!r}, {1!r}: {2!r})' + .format(cap, _cap, cap_re), RuntimeWarning) return None # no such capability @@ -96,7 +97,8 @@ def _build_any_numeric_capability(term, cap, num=99, nparams=1): cap_re = re.sub(r'(\d+)', r'(\d+)', cap_re) if r'(\d+)' in cap_re: return cap_re - warnings.warn('Missing numerics in %r, %r' % (cap, cap_re)) + warnings.warn('Missing numerics in {0:!r}: {1:!r}' + .format(cap, cap_re), RuntimeWarning) return None # no such capability @@ -345,8 +347,8 @@ def init_sequence_patterns(term): ] # compile as regular expressions, OR'd. - _re_will_move = re.compile('(%s)' % ('|'.join(_will_move))) - _re_wont_move = re.compile('(%s)' % ('|'.join(_wont_move))) + _re_will_move = re.compile(u'({0})'.format(u'|'.join(_will_move))) + _re_wont_move = re.compile(u'({0})'.format(u'|'.join(_wont_move))) # static pattern matching for horizontal_distance(ucs, term) bnc = functools.partial(_build_numeric_capability, term) @@ -397,8 +399,10 @@ def _wrap_chunks(self, chunks): """ lines = [] if self.width <= 0 or not isinstance(self.width, int): - raise ValueError("invalid width %r(%s) (must be integer > 0)" % ( - self.width, type(self.width))) + raise ValueError( + "invalid width {self.width} (width_type) (must be integer > 0)" + .format(self=self, width_type=type(self.width))) + term = self.term drop_whitespace = not hasattr(self, 'drop_whitespace' ) or self.drop_whitespace @@ -763,12 +767,12 @@ def termcap_distance(ucs, cap, unit, term): assert cap in ('cuf', 'cub'), cap assert unit in (1, -1), unit # match cub1(left), cuf1(right) - one = getattr(term, '_%s1' % (cap,)) + one = getattr(term, '_{0}1'.format(cap)) if one and ucs.startswith(one): return unit # match cub(n), cuf(n) using regular expressions - re_pattern = getattr(term, '_re_%s' % (cap,)) + re_pattern = getattr(term, '_re_{0}'.format(cap)) _dist = re_pattern and re_pattern.match(ucs) if _dist: return unit * int(_dist.group(1)) From 9da73f80032bca98e5912cd60c31c7dbd79fa2d6 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 11 Oct 2015 00:58:45 -0700 Subject: [PATCH 299/459] fix format specifier --- blessed/sequences.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blessed/sequences.py b/blessed/sequences.py index 68c0251f..256121ef 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -72,7 +72,7 @@ def _build_numeric_capability(term, cap, optional=False, # modify & return n to matching digit expression cap_re = cap_re.replace(str(num), r'(\d+){0}'.format(opt)) return cap_re - warnings.warn('Unknown parameter in {0:!r}, {1!r}: {2!r})' + warnings.warn('Unknown parameter in {0:!r}, {1:!r}: {2:!r})' .format(cap, _cap, cap_re), RuntimeWarning) return None # no such capability @@ -400,7 +400,7 @@ def _wrap_chunks(self, chunks): lines = [] if self.width <= 0 or not isinstance(self.width, int): raise ValueError( - "invalid width {self.width} (width_type) (must be integer > 0)" + "invalid width {self.width} ({width_type}) (must be int > 0)" .format(self=self, width_type=type(self.width))) term = self.term From 98632e38b7299d47c56f9b07edb306e3d1b88e70 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 11 Oct 2015 01:01:23 -0700 Subject: [PATCH 300/459] amend a 3rd time --- blessed/sequences.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/blessed/sequences.py b/blessed/sequences.py index 256121ef..454bc477 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -72,7 +72,7 @@ def _build_numeric_capability(term, cap, optional=False, # modify & return n to matching digit expression cap_re = cap_re.replace(str(num), r'(\d+){0}'.format(opt)) return cap_re - warnings.warn('Unknown parameter in {0:!r}, {1:!r}: {2:!r})' + warnings.warn('Unknown parameter in {0!r}, {1!r}: {2!r})' .format(cap, _cap, cap_re), RuntimeWarning) return None # no such capability @@ -97,7 +97,7 @@ def _build_any_numeric_capability(term, cap, num=99, nparams=1): cap_re = re.sub(r'(\d+)', r'(\d+)', cap_re) if r'(\d+)' in cap_re: return cap_re - warnings.warn('Missing numerics in {0:!r}: {1:!r}' + warnings.warn('Missing numerics in {0!r}: {1!r}' .format(cap, cap_re), RuntimeWarning) return None # no such capability @@ -400,8 +400,8 @@ def _wrap_chunks(self, chunks): lines = [] if self.width <= 0 or not isinstance(self.width, int): raise ValueError( - "invalid width {self.width} ({width_type}) (must be int > 0)" - .format(self=self, width_type=type(self.width))) + "invalid width {0!r} ({1!r}) (must be int > 0)" + .format(self.width, type(self.width))) term = self.term drop_whitespace = not hasattr(self, 'drop_whitespace' From 4230af39321f87986a2490c74db5a2f739bb1e51 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 11 Oct 2015 01:07:21 -0700 Subject: [PATCH 301/459] RuntimeWarning -> UserWarning --- blessed/sequences.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blessed/sequences.py b/blessed/sequences.py index 454bc477..e6be19ac 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -73,7 +73,7 @@ def _build_numeric_capability(term, cap, optional=False, cap_re = cap_re.replace(str(num), r'(\d+){0}'.format(opt)) return cap_re warnings.warn('Unknown parameter in {0!r}, {1!r}: {2!r})' - .format(cap, _cap, cap_re), RuntimeWarning) + .format(cap, _cap, cap_re), UserWarning) return None # no such capability @@ -98,7 +98,7 @@ def _build_any_numeric_capability(term, cap, num=99, nparams=1): if r'(\d+)' in cap_re: return cap_re warnings.warn('Missing numerics in {0!r}: {1!r}' - .format(cap, cap_re), RuntimeWarning) + .format(cap, cap_re), UserWarning) return None # no such capability From ad27861ad1e84a5954752b4de3a3487e2502baa5 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 11 Oct 2015 01:14:16 -0700 Subject: [PATCH 302/459] revert to previous exact spacing (test enforced) --- blessed/sequences.py | 2 +- tox.ini | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/blessed/sequences.py b/blessed/sequences.py index e6be19ac..0c6d0a6a 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -400,7 +400,7 @@ def _wrap_chunks(self, chunks): lines = [] if self.width <= 0 or not isinstance(self.width, int): raise ValueError( - "invalid width {0!r} ({1!r}) (must be int > 0)" + "invalid width {0!r}({1!r}) (must be integer > 0)" .format(self.width, type(self.width))) term = self.term diff --git a/tox.ini b/tox.ini index 161c01c0..14762ecb 100644 --- a/tox.ini +++ b/tox.ini @@ -57,6 +57,12 @@ commands = {envbindir}/sphinx-build -v -W \ {toxinidir}/docs/_build/html echo "--> open docs/_build/html/index.html for review." +[testenv:py34] +# there is not much difference of py34 vs. 35 in blessed +# library; prefer testing integration against py35, and +# just do a 'quick' on py34, if exists. +setenv = TEST_QUICK=1 + [pytest] looponfailroots = blessed norecursedirs = .git From bd351f16e888de5d744bb8a5842e0c835a06c832 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 11 Oct 2015 01:33:47 -0700 Subject: [PATCH 303/459] use 'COVERAGE_ID' env variable for combining --- .travis.yml | 18 ++++++++---------- tox.ini | 2 +- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index dbb9eb78..ecb95fbf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,4 @@ language: python -sudo: false - -before_script: - - pip install -q tox -script: - - tox - matrix: fast_finish: true include: @@ -13,11 +6,16 @@ matrix: - env: TOXENV=sa - env: TOXENV=sphinx - python: 2.7 - env: TOXENV=py27 TEST_QUICK=1 + env: TOXENV=py27 TEST_QUICK=1 COVERAGE_ID=travis-ci - python: 3.4 - env: TOXENV=py34 TEST_QUICK=1 + env: TOXENV=py34 TEST_QUICK=1 COVERAGE_ID=travis-ci - python: 3.5 - env: TOXENV=py35 + env: TOXENV=py35 COVERAGE_ID=travis-ci +install: + - pip install tox +script: + - tox +sudo: false notifications: email: diff --git a/tox.ini b/tox.ini index 14762ecb..0110faf9 100644 --- a/tox.ini +++ b/tox.ini @@ -14,7 +14,7 @@ commands = {envbindir}/py.test \ {posargs:-x} coverage combine cp {toxinidir}/.coverage \ - {toxinidir}/._coverage.{envname}.{env:TEAMCITY_BUILDCONFNAME:xxx} + {toxinidir}/._coverage.{envname}.{env:COVERAGE_ID:local} {toxinidir}/tools/custom-combine.py # CI buildchain target From 05a53c6ea66f0e0d440bd0d74aee1e4424be02dd Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 11 Oct 2015 02:31:02 -0700 Subject: [PATCH 304/459] prevent coverage.misc.NoSource exception thrown --- tools/custom-combine.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/custom-combine.py b/tools/custom-combine.py index fdaaddb1..f5805be5 100755 --- a/tools/custom-combine.py +++ b/tools/custom-combine.py @@ -35,12 +35,12 @@ def main(): print("combining coverage: {0}".format(data_paths)) cov.combine(data_paths=data_paths) cov.load() - cov.html_report() + cov.html_report(ignore_errors=True) print("--> open {0}/htmlcov/index.html for review." .format(os.path.relpath(PROJ_ROOT))) fout = six.StringIO() - cov.report(file=fout) + cov.report(file=fout, ignore_errors=True) for line in fout.getvalue().splitlines(): if u'TOTAL' in line: total_line = line From e9ad7b85dfcbbba49010ab8c13e3a5920d81b010 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Tue, 13 Oct 2015 13:54:54 -0700 Subject: [PATCH 305/459] add Terminal.get_location() method This PR succeeds #32 by @thomasballinger. major -------- - **new method**, ``.get_location(timeout=None)``. - **new method**, ``.ungetch()`` which pushes data back for next ``inkey()``. - **new example** program, ``bin/resize.py``, which required such condition. - **new example** program, ``bin/detect-multibyte.py`` which uses this method. - enhancement: now possible to use stdin when stream=stderr minor -------- - method-local function, ``time_left()``, moved to ``keyboard.py`` as ``_time_left()``. - new function in keyboard.py, ``_read_until``, something like a mini-pexpect. - new private state value ``_line_buffered``, used to determine whether the terminal has already entered cbreak or raw mode. This may become public later if we also use termios routines, instead as a read-only \@property. --- .gitignore | 1 + bin/detect-multibyte.py | 92 ++++++++++ bin/editor.py | 2 +- bin/resize.py | 85 +++++++++ bin/tprint.py | 30 +++- blessed/keyboard.py | 89 +++++++++- blessed/terminal.py | 161 +++++++++++++++--- blessed/tests/test_core.py | 23 +++ blessed/tests/test_keyboard.py | 56 ++++++ .../soulburner-ru-family-encodings.jpg | Bin 0 -> 230516 bytes docs/examples.rst | 33 +++- docs/history.rst | 6 + docs/intro.rst | 8 +- docs/overview.rst | 48 +++--- docs/pains.rst | 38 +---- version.json | 2 +- 16 files changed, 565 insertions(+), 109 deletions(-) create mode 100644 bin/detect-multibyte.py create mode 100755 bin/resize.py create mode 100644 docs/_static/soulburner-ru-family-encodings.jpg diff --git a/.gitignore b/.gitignore index ffa0246f..943eebde 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ docs/_build htmlcov .coveralls.yml .DS_Store +.*.sw? diff --git a/bin/detect-multibyte.py b/bin/detect-multibyte.py new file mode 100644 index 00000000..18930c1c --- /dev/null +++ b/bin/detect-multibyte.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python +# coding: utf-8 +""" +Determines whether the attached terminal supports multibyte encodings. + +Problem: A screen drawing application wants to detect whether the terminal +client is capable of rendering utf-8. Some transports, such as a serial link, +often cannot forward their ``LANG`` environment preference, or protocols such +as telnet and rlogin often assume mutual agreement by manual configuration. + +We can interactively determine whether the connecting terminal emulator is +rendering in utf8 by making an inquiry of their cursor position: + + - request cursor position (p0). + + - display multibyte character. + + - request cursor position (p1). + +If the horizontal distance of (p0, p1) is 1 cell, we know the connecting +client is certainly matching our intended encoding. + +As an exercise, it may be possible to use this technique to accurately +determine to the remote encoding without protocol negotiation using cursor +positioning alone, as demonstrated by the following diagram, + +.. image:: _static/soulburner-ru-family-encodings.jpg + :alt: Cyrillic encodings flowchart +""" +# pylint: disable=invalid-name +# Invalid module name "detect-multibyte" +# std imports +from __future__ import print_function +import collections +import sys + +# local, +from blessed import Terminal + + +def get_pos(term): + """Get cursor position, calling os.exit(2) if not determined.""" + # pylint: disable=invalid-name + # Invalid variable name "Position" + Position = collections.namedtuple('Position', ('row', 'column')) + + pos = Position(*term.get_location(timeout=5.0)) + + if -1 in pos: + print('stdin: not a human', file=sys.stderr) + exit(2) + + return pos + + +def main(): + """Program entry point.""" + term = Terminal() + + # move to bottom of screen, temporarily, where we're likely to do + # the least damage, as we are performing something of a "destructive + # write and erase" onto this screen location. + with term.cbreak(), term.location(y=term.height - 1, x=0): + + # store first position + pos0 = get_pos(term) + + # display multibyte character + print(u'⦰', end='') + + # store second position + pos1 = get_pos(term) + + # determine distance + horizontal_distance = pos1.column - pos0.column + multibyte_capable = bool(horizontal_distance == 1) + + # rubout character(s) + print('\b \b' * horizontal_distance, end='') + + # returned to our original starting position, + if not multibyte_capable: + print('multibyte encoding failed, horizontal distance is {0}, ' + 'expected 1 for unicode point https://codepoints.net/U+29B0' + .format(horizontal_distance), file=sys.stderr) + exit(1) + + print('{checkbox} multibyte encoding supported!' + .format(checkbox=term.bold_green(u'✓'))) + +if __name__ == '__main__': + exit(main()) diff --git a/bin/editor.py b/bin/editor.py index b3060f0b..979b4e41 100755 --- a/bin/editor.py +++ b/bin/editor.py @@ -62,7 +62,7 @@ def echo_yx(cursor, text): """Move to ``cursor`` and display ``text``.""" echo(cursor.term.move(cursor.y, cursor.x) + text) -Cursor = collections.namedtuple('Point', ('y', 'x', 'term')) +Cursor = collections.namedtuple('Cursor', ('y', 'x', 'term')) def readline(term, width=20): """A rudimentary readline implementation.""" diff --git a/bin/resize.py b/bin/resize.py new file mode 100755 index 00000000..4584e7ed --- /dev/null +++ b/bin/resize.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python +""" +Determines and prints COLUMNS and LINES of the attached window width. + +A strange problem: programs that perform screen addressing incorrectly +determine the screen margins. Calls to reset(1) do not resolve the +issue. + +This may often happen because the transport is incapable of communicating +the terminal size, such as over a serial line. This demonstration program +determines true screen dimensions and produces output suitable for evaluation +by a bourne-like shell:: + + $ eval `./resize.py` + +The following remote login protocols communicate window size: + + - ssh: notifies on dedicated session channel, see for example, + ``paramiko.ServerInterface.check_channel_window_change_request``. + + - telnet: sends window size through NAWS (negotiate about window + size, RFC 1073), see for example, + ``telnetlib3.TelnetServer.naws_receive``. + + - rlogin: protocol sends only initial window size, and does not notify + about size changes. + +This is a simplified version of `resize.c +`_ provided by the +xterm package. +""" +# std imports +from __future__ import print_function +import collections +import sys + +# local +from blessed import Terminal + + +def main(): + """Program entry point.""" + # pylint: disable=invalid-name + # Invalid variable name "Position" + Position = collections.namedtuple('Position', ('row', 'column')) + + # particularly strange, we use sys.stderr as our output stream device, + # this 'stream' file descriptor is only used for side effects, of which + # this application uses two: the term.location() has an implied write, + # as does get_position(). + # + # the reason we chose stderr is to ensure that the terminal emulator + # receives our bytes even when this program is wrapped by shell eval + # `resize.py`; backticks gather stdout but not stderr in this case. + term = Terminal(stream=sys.stderr) + + # Move the cursor to the farthest lower-right hand corner that is + # reasonable. Due to word size limitations in older protocols, 999,999 + # is our most reasonable and portable edge boundary. Telnet NAWS is just + # two unsigned shorts: ('!HH' in python struct module format). + with term.location(999, 999): + + # We're not likely at (999, 999), but a well behaved terminal emulator + # will do its best to accommodate our request, positioning the cursor + # to the farthest lower-right corner. By requesting the current + # position, we may negotiate about the window size directly with the + # terminal emulator connected at the distant end. + pos = Position(*term.get_location(timeout=5.0)) + + if -1 not in pos: + # true size was determined + lines, columns = pos.row, pos.column + + else: + # size could not be determined. Oh well, the built-in blessed + # properties will use termios if available, falling back to + # existing environment values if it has to. + lines, columns = term.height, term.width + + print("COLUMNS={columns};\nLINES={lines};\nexport COLUMNS LINES;" + .format(columns=columns, lines=lines)) + + +if __name__ == '__main__': + exit(main()) diff --git a/bin/tprint.py b/bin/tprint.py index 27ea046e..8bc1488a 100755 --- a/bin/tprint.py +++ b/bin/tprint.py @@ -1,25 +1,37 @@ #!/usr/bin/env python -"""A simple cmd-line tool for displaying FormattingString capabilities.""" +""" +A simple cmd-line tool for displaying FormattingString capabilities. + +For example:: + + $ python tprint.py bold A rather bold statement. + +""" +# std from __future__ import print_function import argparse -def main(): - """Program entry point.""" - from blessed import Terminal +# local +from blessed import Terminal + +def parse_args(): + """Parse sys.argv, returning dict suitable for main().""" parser = argparse.ArgumentParser( description='displays argument as specified style') parser.add_argument('style', type=str, help='style formatter') parser.add_argument('text', type=str, nargs='+') + return dict(parser.parse_args()._get_kwargs()) - term = Terminal() - args = parser.parse_args() - style = getattr(term, args.style) +def main(style, text): + """Program entry point.""" + term = Terminal() + style = getattr(term, style) + print(style(' '.join(text))) - print(style(' '.join(args.text))) if __name__ == '__main__': - main() + exit(main(**parse_args())) diff --git a/blessed/keyboard.py b/blessed/keyboard.py index cb8ae8d5..81b82573 100644 --- a/blessed/keyboard.py +++ b/blessed/keyboard.py @@ -3,6 +3,8 @@ # std imports import curses.has_key import curses +import time +import re # 3rd party import six @@ -218,8 +220,8 @@ def resolve_sequence(text, mapper, codes): determine that ``xxx`` remains unresolved. :arg text: string of characters received from terminal input stream. - :arg OrderedDict mapper: an OrderedDict of unicode multibyte sequences, - such as u'\x1b[D' paired by their integer value (260) + :arg OrderedDict mapper: unicode multibyte sequences, such as ``u'\x1b[D'`` + paired by their integer value (260) :arg dict codes: a :type:`dict` of integer values (such as 260) paired by their mnemonic name, such as ``'KEY_LEFT'``. :rtype: Keystroke @@ -230,6 +232,87 @@ def resolve_sequence(text, mapper, codes): return Keystroke(ucs=text and text[0] or u'') +def _time_left(stime, timeout): + """ + Return time remaining since ``stime`` before given ``timeout``. + + This function assists determining the value of ``timeout`` for + class method :meth:`~.Terminal.kbhit` and similar functions. + + :arg float stime: starting time for measurement + :arg float timeout: timeout period, may be set to None to + indicate no timeout (where None is always returned). + :rtype: float or int + :returns: time remaining as float. If no time is remaining, + then the integer ``0`` is returned. + """ + if timeout is not None: + if timeout == 0: + return 0 + return max(0, timeout - (time.time() - stime)) + + +def _read_until(term, pattern, timeout): + """ + Convenience read-until-pattern function, supporting :meth:`~.get_location`. + + :arg blessed.Terminal term: :class:`~.Terminal` instance. + :arg float timeout: timeout period, may be set to None to indicate no + timeout (where 0 is always returned). + :arg str pattern: target regular expression pattern to seek. + :rtype: tuple + :returns: tuple in form of ``(match, str)``, *match* + may be :class:`re.MatchObject` if pattern is discovered + in input stream before timeout has elapsed, otherwise + None. ``str`` is any remaining text received exclusive + of the matching pattern). + + The reason a tuple containing non-matching data is returned, is that the + consumer should push such data back into the input buffer by + :meth:`~.Terminal.ungetch` if any was received. + + For example, when a user is performing rapid input keystrokes while its + terminal emulator surreptitiously responds to this in-band sequence, we + must ensure any such keyboard data is well-received by the next call to + term.inkey() without delay. + """ + stime = time.time() + match, buf = None, u'' + + # first, buffer all pending data. pexpect library provides a + # 'searchwindowsize' attribute that limits this memory region. We're not + # concerned about OOM conditions: only (human) keyboard input and terminal + # response sequences are expected. + + while True: + # block as long as necessary to ensure at least one character is + # received on input or remaining timeout has elapsed. + ucs = term.inkey(timeout=_time_left(stime, timeout)) + if ucs: + buf += ucs + # while the keyboard buffer is "hot" (has input), we continue to + # aggregate all awaiting data. We do this to ensure slow I/O + # calls do not unnecessarily give up within the first 'while' loop + # for short timeout periods. + while True: + ucs = term.inkey(timeout=0) + if not ucs: + break + buf += ucs + + match = re.search(pattern=pattern, string=buf) + if match is not None: + # match + break + + if timeout is not None: + if not _time_left(stime, timeout): + # timeout + break + + return match, buf + + def _inject_curses_keynames(): r""" Inject KEY_NAMES that we think would be useful into the curses module. @@ -240,7 +323,7 @@ def _inject_curses_keynames(): curses module, and is called from the global namespace at time of import. - Though we may determine keynames and codes for keyboard input that + Though we may determine *keynames* and codes for keyboard input that generate multibyte sequences, it is also especially useful to aliases a few basic ASCII characters such as ``KEY_TAB`` instead of ``u'\t'`` for uniformity. diff --git a/blessed/terminal.py b/blessed/terminal.py index 1b0d13eb..51ae9cc1 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -15,6 +15,7 @@ import sys import time import warnings +import re try: import termios @@ -45,6 +46,7 @@ ) from .sequences import (init_sequence_patterns, + _build_any_numeric_capability, SequenceTextWrapper, Sequence, ) @@ -53,6 +55,8 @@ get_leading_prefixes, get_keyboard_codes, resolve_sequence, + _read_until, + _time_left, ) @@ -137,11 +141,17 @@ def __init__(self, kind=None, stream=None, force_styling=False): self._keyboard_fd = None # Default stream is stdout, keyboard valid as stdin only when - # output stream is stdout is a tty. - if stream is None or stream == sys.__stdout__: + # output stream is stdout or stderr and is a tty. + if stream is None: stream = sys.__stdout__ + if stream in (sys.__stdout__, sys.__stderr__): self._keyboard_fd = sys.__stdin__.fileno() + # we assume our input stream to be line-buffered until either the + # cbreak of raw context manager methods are entered with an + # attached tty. + self._line_buffered = True + try: stream_fd = (stream.fileno() if hasattr(stream, 'fileno') and callable(stream.fileno) else None) @@ -401,10 +411,109 @@ def location(self, x=None, y=None): elif y is not None: self.stream.write(self.move_y(y)) try: + self.stream.flush() yield finally: # Restore original cursor position: self.stream.write(self.restore) + self.stream.flush() + + def get_location(self, timeout=None): + r""" + Return tuple (row, column) of cursor position. + + :arg float timeout: Return after time elapsed in seconds with value + ``(-1, -1)`` indicating that the remote end did not respond. + :rtype: tuple + :returns: cursor position as tuple in form of (row, column). + + The location of the cursor is determined by emitting the ``u7`` + terminal capability, or VT100 `Query Cursor Position + `_ when such + capability is undefined, which elicits a response from a reply string + described by capability ``u6``, or again VT100's definition of + ``\x1b[%i%d;%dR`` when undefined. + + The ``(row, col)`` return value matches the parameter order of the + ``move`` capability, so that the following sequence should cause the + cursor to not move at all:: + + >>> term = Terminal() + >>> term.move(*term.get_location())) + + .. warning:: You might first test that a terminal is capable of + informing you of its location, while using a timeout, before + later calling. When a timeout is specified, always ensure the + return value is conditionally checked for ``(-1, -1)``. + """ + # Local lines attached by termios and remote login protocols such as + # ssh and telnet both provide a means to determine the window + # dimensions of a connected client, but **no means to determine the + # location of the cursor**. + # + # from http://invisible-island.net/ncurses/terminfo.src.html, + # + # > The System V Release 4 and XPG4 terminfo format defines ten string + # > capabilities for use by applications, .... In this file, + # > we use certain of these capabilities to describe functions which + # > are not covered by terminfo. The mapping is as follows: + # > + # > u9 terminal enquire string (equiv. to ANSI/ECMA-48 DA) + # > u8 terminal answerback description + # > u7 cursor position request (equiv. to VT100/ANSI/ECMA-48 DSR 6) + # > u6 cursor position report (equiv. to ANSI/ECMA-48 CPR) + query_str = self.u7 or u'\x1b[6n' + + # determine response format as a regular expression + response_re = re.escape(u'\x1b') + r'\[(\d+)\;(\d+)R' + + if self.u6: + with warnings.catch_warnings(): + response_re = _build_any_numeric_capability( + term=self, cap='u6', nparams=2 + ) or response_re + + # Avoid changing user's desired raw or cbreak mode if already entered, + # by entering cbreak mode ourselves. This is necessary to receive user + # input without awaiting a human to press the return key. This mode + # also disables echo, which we should also hide, as our input is an + # sequence that is not meaningful for display as an output sequence. + + ctx = None + try: + if self._line_buffered: + ctx = self.cbreak() + ctx.__enter__() + + # emit the 'query cursor position' sequence, + self.stream.write(query_str) + self.stream.flush() + + # expect a response, + match, data = _read_until(term=self, + pattern=response_re, + timeout=timeout) + + # ensure response sequence is excluded from subsequent input, + if match: + data = (data[:match.start()] + data[match.end():]) + + # re-buffer keyboard data, if any + self.ungetch(data) + + if match: + # return matching sequence response, the cursor location. + row, col = match.groups() + return int(row), int(col) + + finally: + if ctx is not None: + ctx.__exit__(None, None, None) + + # We chose to return an illegal value rather than an exception, + # favoring that users author function filters, such as max(0, y), + # rather than crowbarring such logic into an exception handler. + return -1, -1 @contextlib.contextmanager def fullscreen(self): @@ -711,6 +820,14 @@ def getch(self): byte = os.read(self._keyboard_fd, 1) return self._keyboard_decoder.decode(byte, final=False) + def ungetch(self, text): + """ + Buffer input data to be discovered by next call to :meth:`~.inkey`. + + :param str ucs: String to be buffered as keyboard input. + """ + self._keyboard_buf.extendleft(text) + def kbhit(self, timeout=None, **_kwargs): """ Return whether a keypress has been detected on the keyboard. @@ -804,14 +921,17 @@ def cbreak(self): if HAS_TTY and self._keyboard_fd is not None: # Save current terminal mode: save_mode = termios.tcgetattr(self._keyboard_fd) + save_line_buffered = self._line_buffered tty.setcbreak(self._keyboard_fd, termios.TCSANOW) try: + self._line_buffered = False yield finally: # Restore prior mode: termios.tcsetattr(self._keyboard_fd, termios.TCSAFLUSH, save_mode) + self._line_buffered = save_line_buffered else: yield @@ -839,14 +959,17 @@ def raw(self): if HAS_TTY and self._keyboard_fd is not None: # Save current terminal mode: save_mode = termios.tcgetattr(self._keyboard_fd) + save_line_buffered = self._line_buffered tty.setraw(self._keyboard_fd, termios.TCSANOW) try: + self._line_buffered = False yield finally: # Restore prior mode: termios.tcsetattr(self._keyboard_fd, termios.TCSAFLUSH, save_mode) + self._line_buffered = save_line_buffered else: yield @@ -912,25 +1035,6 @@ def inkey(self, timeout=None, esc_delay=0.35, **_kwargs): 'Terminal.inkey() called, but no terminal with keyboard ' 'attached to process. This call would hang forever.') - def time_left(stime, timeout): - """ - Return time remaining since ``stime`` before given ``timeout``. - - This function assists determining the value of ``timeout`` for - class method :meth:`kbhit`. - - :arg float stime: starting time for measurement - :arg float timeout: timeout period, may be set to None to - indicate no timeout (where 0 is always returned). - :rtype: float or int - :returns: time remaining as float. If no time is remaining, - then the integer ``0`` is returned. - """ - if timeout is not None: - if timeout == 0: - return 0 - return max(0, timeout - (time.time() - stime)) - resolve = functools.partial(resolve_sequence, mapper=self._keymap, codes=self._keycodes) @@ -952,25 +1056,30 @@ class method :meth:`kbhit`. # so long as the most immediately received or buffered keystroke is # incomplete, (which may be a multibyte encoding), block until until # one is received. - while not ks and self.kbhit(timeout=time_left(stime, timeout)): + while not ks and self.kbhit(timeout=_time_left(stime, timeout)): ucs += self.getch() ks = resolve(text=ucs) # handle escape key (KEY_ESCAPE) vs. escape sequence (like those # that begin with \x1b[ or \x1bO) up to esc_delay when # received. This is not optimal, but causes least delay when - # "meta sends escape" is used, - # or when an unsupported sequence is sent. + # "meta sends escape" is used, or when an unsupported sequence is + # sent. + # + # The statement, "ucs in self._keymap_prefixes" has an effect on + # keystrokes such as Alt + Z ("\x1b[z" with metaSendsEscape): because + # no known input sequences begin with such phrasing to allow it to be + # returned more quickly than esc_delay otherwise blocks for. if ks.code == self.KEY_ESCAPE: esctime = time.time() while (ks.code == self.KEY_ESCAPE and ucs in self._keymap_prefixes and - self.kbhit(timeout=time_left(esctime, esc_delay))): + self.kbhit(timeout=_time_left(esctime, esc_delay))): ucs += self.getch() ks = resolve(text=ucs) # buffer any remaining text received - self._keyboard_buf.extendleft(ucs[len(ks):]) + self.ungetch(ucs[len(ks):]) return ks diff --git a/blessed/tests/test_core.py b/blessed/tests/test_core.py index bc1a3bb6..e03d06c9 100644 --- a/blessed/tests/test_core.py +++ b/blessed/tests/test_core.py @@ -6,6 +6,8 @@ import warnings import platform import locale +import time +import math import sys import imp import os @@ -449,3 +451,24 @@ def __import__(name, *args, **kwargs): imp.reload(blessed.terminal) child() + + +def test_time_left(): + """test '_time_left' routine returns correct positive delta difference.""" + from blessed.keyboard import _time_left + + # given stime =~ "10 seconds ago" + stime = (time.time() - 10) + + # timeleft(now, 15s) = 5s remaining + timeout = 15 + result = _time_left(stime=stime, timeout=timeout) + + # we expect roughly 4.999s remain + assert math.ceil(result) == 5.0 + + +def test_time_left_infinite_None(): + """keyboard '_time_left' routine returns None when given None.""" + from blessed.keyboard import _time_left + assert _time_left(stime=time.time(), timeout=None) is None diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index 8b11668e..9df528d6 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -596,6 +596,62 @@ def test_esc_delay_cbreak_prefix_sequence(): assert 34 <= int(duration_ms) <= 45, duration_ms +def test_get_location_0s(): + "0-second get_location call without response." + @as_subprocess + def child(): + term = TestTerminal(stream=six.StringIO()) + stime = time.time() + y, x = term.get_location(timeout=0) + assert (math.floor(time.time() - stime) == 0.0) + assert (y, x) == (-1, -1) + child() + + +def test_get_location_0s_under_raw(): + "0-second get_location call without response under raw mode." + @as_subprocess + def child(): + term = TestTerminal(stream=six.StringIO()) + with term.raw(): + stime = time.time() + y, x = term.get_location(timeout=0) + assert (math.floor(time.time() - stime) == 0.0) + assert (y, x) == (-1, -1) + child() + + +def test_get_location_0s_reply_via_ungetch(): + "0-second get_location call with response." + @as_subprocess + def child(): + term = TestTerminal(stream=six.StringIO()) + stime = time.time() + # monkey patch in an invalid response ! + term.ungetch(u'\x1b[10;10R') + + y, x = term.get_location(timeout=0.01) + assert (math.floor(time.time() - stime) == 0.0) + assert (y, x) == (10, 10) + child() + + +def test_get_location_0s_reply_via_ungetch_under_raw(): + "0-second get_location call with response under raw mode." + @as_subprocess + def child(): + term = TestTerminal(stream=six.StringIO()) + with term.raw(): + stime = time.time() + # monkey patch in an invalid response ! + term.ungetch(u'\x1b[10;10R') + + y, x = term.get_location(timeout=0.01) + assert (math.floor(time.time() - stime) == 0.0) + assert (y, x) == (10, 10) + child() + + def test_keystroke_default_args(): "Test keyboard.Keystroke constructor with default arguments." from blessed.keyboard import Keystroke diff --git a/docs/_static/soulburner-ru-family-encodings.jpg b/docs/_static/soulburner-ru-family-encodings.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ecb964f86ef832279d0566de09ee446c1f12fc72 GIT binary patch literal 230516 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%0A!ki zmv*fX3j-#osNI+LM3~ZMNl7_RK2|*_o#xe3$Eboe8^efe z6bjn3Ye2?*{lr11DgvSCU#u86X8(dsvY&>(^xaDAAN@A^M}b;Z}8gcAw{JxbFZ(OZH>t?L^c8btLz+=JmS)?S$-J z@i|}SQQRNjx*>%t?bHkSxI86Ik&rFFVep$4HbdX0+4H?4y61U$6%R}9$hu@0`^;G0 zk9VqXR-8&(8g+e3*5B#3cr*bIdFcDfwLP>37emWtLA;}5yLg|XA&pkTUSeU+|Fe

F=Lcu6rAu2dTXGcgmb;2I;?SSMJA$Tqm^J#kD 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

)1wbsZ5}1zz1n>a`_{s8eWMWns>n8^WkVOBP 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�t+fhlpfOoT{E72P5Ywk#dOi}-JwJ++q{U6}tg*>&UIRgI?m2PGOcYql&p9@E~~vHBq8;qux(W zj;18c`tlc#+#9W80gV(5E`T(7^mn4P`=M3#Wxm-$7c(vVKOK(ud9$!Vv zXQ??ck0$YVee+2NNWu#zvF;i6%Dw(m>%vCz!xsW6yfedFDP+;vOR8v?&i?OUDpdWe z?nK7_>_q>9UG(p+7bpJ@IRDW_+g%Keaok1zG}hm+{~v5D`3Ub-v{2WdkxXve;M(Ti zBCT}xX_G12zn|ARs7miBPEV9`_Q+UjYlhaTq%n65PcRElMCbFf!zVP&hK>vcXno}b zCsSb;a$jfX!N&T=x@NT#+KB%+mgRP=+jNQ#7q03+snp91NlvrK{YRNOrJ}9N&4cj`mx~1_zV>M7O4N%ha2(#_HdIjxK0|xc@m9g-RO`0mHhYoNqiKm z`1f4<7f^_X>e^H-iDjAA^SUY4>ne$gy`P$~xIcqAZ9wvyX-X;p;)Hi6xEQJEEyyyh zY(2}&mMI+hjC!>YdWzKlW#WS>!J01PH@|KZ?@FT+tG@K0i2-f##h(Lj)oId&#mpAE z$|sCY4t-``{;b}*7H#nStYh!KdD$l{?lNb_mfhNBQr;+1>${dT_vndRosPX0*=OQ& zQU3)Uciy2J8xnRWO6bKF!DTFwjrnC?zYM(gJtDLefP8gd!6q81%Ev{qr9vxRGNNW7 zsCVq2)~;u38a(n@Vsn!ERp%GQCrlt4z!pdi8h0OnnFHuuIegd=ySRS~WbjN9cI+Q$ z<5PCh(3q;MK;6w|VQZ3$;HvVv?=XwsbD4LTJJ1e=@X4qMZb|P=zfZZRbUWZttL7ae z@iS|6hrExvJG*$iOGZydAL;|d#lbkkS0BrB+`E}hC@*UeI4~L$v2V!`_p=r>g61}e zVD$(nCHfOjmkGR|ejAZ$0D(AM3N|=SCgp`RVx(8>hHl+&`l6ke>K<2A zrcl_J6KaMfCc2HiqljV`RgpAs)SA||3*7P_6PY>|Wth%d#1gf`*I205w0q)6R`wXH zLnGcm-$Je#1nk}$Bt}?4>-y6^HXvr+>dx~Y&ucGBz>No8<@IgLc)PQHU4T@Ykru4( zHTiToz9jeJR4R*?UGRk#?ObS2p;ACedPc@G>E8G==rb7_`%_l5%Br}xn8=;W!?zH- zd8ZHQ_{givUt)33kD!*?cS-e|SL6eT)@zT4F%p(qHB1yGC^osSNT@fnh53U4Rm2Cb zLY{AzjuXVU_{}6;OrB24wzfvyDkU14gYT^AtM>Zo*`S|Mv-v@F%P*9`G6C($HTo<7SwZ5$!=&MhEIHRPm0hSV6 zk7$#97PY{S&moA^TzkPLq@Dw39u}4bQ&IF51K+r6YWBd`)AO?$)rIHc={D1XZy*bb zN#6-P31DddT( z4*g~wl(GGeqH^ zAp^B*ytJPC`L-C}=AzE0vl9&_8|0rQsS&hraB<_fq-(V|aED@a6D}luE?^%qs+mff zqejm&wlN8BeMgs#-p?%3=!rWI97MctSuAg)Y%?oosiF{fe&6z}b<~zD_4c%SUX<+M zS$E;7oW`{RWim1sCUDDQx@z?u@j6(n-Sf+}|7h0pyS885jz1Rg+Ug<=JD{hzh9MQh zgBH3RgB`q0v}*8R;zbC2pi9#m|1G7BQM0a_r`g=of7h>1$L zW_zwLF=Tg(GVP4vA*v?I)MUb(ac`7$kmxWB=4g3FwglYyiHfn z)_Wv~Jb=~jJ~g6!A2mF)uX3GDb8eX+vOj2)VlfGC>fBafEV5-3iW9KYxolvp*|)x= ziK}O`v!325BXdyJ1a7UcOC)n|;McBhARUAdmDrd>C`P(qA-MvvyJP3VIiw>^!Hgq? zQg*TJlq>$gh~easfH3a@P)2}*1IS9wNa>72{%|AhL$-DZ$Vi4=hVgPJMsqJ^6Eu0Z z4EGR8T&V;(Na9)LS;cRg8HPJPe^6putMcT!a)9*S4`?0b4P*9KU&4z>6{wN3%33S6kGfTJdd-b z{5#Qs8T2{m53fJ}23Y;fP5XZ>F?6xR{w;T2kANRx)nC!Cee=HoEZdToFMIx3&zqqC zb2UKcUC5$yl4g5%PH>ld7GU-pp!HKHA*Sq~h{iU|a8f!NyARHMQ6;Spi{jk4X;+k_ z@3!2(JI7P|o_aA*vhsC0iUrjpnfOxH3yK<=*-Xz$G;TDapyrm|!*nO~HsvdLVk5jX zv%gs&#B1oVS&|2`KKq~;QSaMq8(RrvY3e^aC7A)ovRS2t(9^I8{n5lk3-uDB>FoY` z&Hba=`{yQd$Kf|%|EBUTF|aqSrl4UlvHtL^Iu=_7y-}ymGZA*@?NyD637kxIyCL_x z|0`%9Ks0xQ$95lJ#Ym)M52O_IhpF8jt)bHZv$oQHb@aJ?IDqhk9y?Y)cOM*t84|$b z(<#D8)!`&x{1UL*8H1(!cXD`ya-)a@7B4NVZS$YbUx`Ol(ru_+e%22lyzEOjXo{Vy zew1^WIX@$@;UReEvTwu_K{05cxs~$Jl{qs_e7v?6f>ukzg}d)i@E&wVcyig3g;Hb} z8XNNWbVR3&Qcovh>shi!rEG=+Ao&~6E%$Yu>k2mxBIFwSI$M2v!CA74 z+3C%76j}iFRocb-ms4m#6EYXmoaVsIsNmK26wOXi+B*T(OllS-X~dDQIJmB%1~6#E znfvigtlI?PkE39o=L(fdkhx-1gK!jTs32J_B}Gb0#2yKPx)aO31F*8vh$I#Wxkbfb z4-kz8zE()JqnV>?%tpFRp=Pw5{04@~T>VAsg)+?G%0rA03|U1mP(fQ08tcscKTm!zS)h&cc=Aa(43@Q#qB=xu|EIPus-%d9U2?w?j-9;2_)o!5 z!2D=GZgflda+%1bV8hpwSr4Bx!bJw%Oyyku=&tmf(*CQ_3suL!$SelP^CF9%19vfi3ta>!4{? zZFDTiJY}TvREGIPLg{17fg=4|iBdF{&!;@f+(RxxLM|@a+mBu-DI?lq0h^y>JXq<) zW!E&h3ufl3vgHy@h|pi4SkYR~-0!L_b~MqLvvyI_wYg$m6w z%>^wu+%sQU-ykQ&VzmS~UF}OIg8S)Go|tRMXpm&fr!6e4j43J7*7mh>iR)lj5D5O` zJ}$=DyR(m|=}L%x=%r(0NpZDEhiA|uGqtZR+;VTjsXxN0DUJ@KGb1B=l50n5^=nm; zVIa~nzDkjLTO8;VORD0geDQSQ+}pIO4DI=ujbAbD>ch!bAMkQDrpZRm?N{EMTQ89e z?xxa-FY!U#n{xCp-VM7?c}7^z+Qq7&o{Q7^)=#JzMm;Gt_*n84kM6V&#yDC(kP#EG z--%V?I@S0_Gqd(ts%guM;^G&?s#v8>?6OdfN;@zZTclUy9>uY5gVjlj$t1~2iUb!IULi6PkZGrsuay^jSIwoGor&`K7k)Y%m*wEwb);RVM?JsWH&z$mJ9 z6FI(;ChdZN4I>NIr9aWpn(-`ac2u?k@%3ADtI@1KQxF;CjwN$qSoR|d2sEy`d358>qGQVdkSRHWg!*<%iC|2I7RA%yqrD=>1PlQ3tuC*larDfl9dv8nb(u>%(V z;!)#~XR?ZN60Ze8V!8#e)<8eGw1PSrPaK1YyBwtQqFTF!uegxvX3=cCdYBJ9xRT-1 zNFgFrV%42=C@r zHKV@G{&bIkst& z`f7fwS<`O-;gSuoV+;?fDj>>LZ!pqQY|Ayh=q${F7!ixXZ~Lhrz!I+WDz2kpa+@{t zev5gDoxsD}ekCYh1M$3%PrCi;@g!o^m7KbeltLyd%D5tix)4;HTi)n+1?Tf}uxEOl zS>cM-%AKHyQf#b`Mm4;dh0AG>8CJix6}3+!wx537^KnB)>l!iSw4Z2htr0;1Rb|NU zwq(wHPp=)WoJt(Q47hC$|2{Vyca!Ii5euN>~go-nFJbagK=$b@^0jHpr5u^C` zoKJ^34b-}2n#CJAJ^?xh0!Zy51NG2fp8Mzz9!?tO;0BSRq72GXtw-`6r`f8GQ~4AF zeVE_Y-u*~vLe+W`K}<<4F|eKTn`{0nT?nU-_Owh$|( zrQ^>fzK_nyp9=4GGu6xSsMVx`$>OEJ!-4H7{g9Sc~P#zh$azHnc+5{Ix*&ocAn&-p9$5g`;rZ{Bvfba2+UFlN*2fF`i` zuT$@AS&gzRK6sli_MT=7Hn|vTf;hhC=5d6;o>JjTb3L!5B+X?T*!gJ6pR}kIpJL=t z=8wNwtvpd?Q#{H8ExuNIGsBxI6D-rKj+C-p>kAVxdy(D|=FsC{5#Ao_(W7F0G??x? zj0xxP8$n5J9EnN_yjX_TZqTI?H$4xBV2QZIy_Sj{5_NEW{`sQwT>m**Q#h3j?4~I` zos&gOX0$@hv_bJ{<6R>(qQN0lbM=aMdPUZUk}6{tsdoX|U+xoRvPtAPdFBHFPx1op zF)}480ss&uoGAS^xOy=y}m0hHuk%ODMOL^9p_<_ZgeJ}SSt+?X<36&GJp~*}+uO`@h8CD}Woh+e}EY5vf z)@2|g4~_UipA}s^hXzpFb9TPeO8}nB5NR}^=vZ2ip?Rboy;)KJNmK(;aD33&9w>UTQm1hhL5G8E< z$%6uFP>s@KO~}B+mWebAO*=z5fAS>zl9~Cu)d^$vrnbOxojU8x`~rtWva8|w^iQvD zH56NxTml_osy5C&IqE&ichn!(vMV?02RTw#W=j&(QJVWj*PW@#iJHSp9|5EwG}RXf zO>P$??6&sAW`CyMSmg4Mg{pr?Z~xs3&t>d~DQePaw|Aqw&jbf%Jhdf>;=ZNjz6i9< zS{87-lMn1N{TOF;@2>SEP@}JRzw+#*G>(kuDA7OH&anajmnl5iDgb&Bod5XkAYhwt zm=2Skn)DvP4JU5wtKbaS1b#_*DX4XEyS(j> z(0rno6rO==0xSpCwvd#svNFMV`=$^}gDFr21Z{dxiq zXpC!QyQ^85T{iAT4Z#oU_gcbpoj_%v7p#!0pYT5o(C^AD9@viz<^>W!6#ex zS1AM<^(mzLe|)QJ^h!mTNjD#n=$JVpVnfj3#e+!XO{}U zOz+tuU2{R)LGO#R)1aDJ%>}#!=DNnbXmXk^Th-Z#tItnhzCQIildLci^5UBWnr2TT zYeO&0OG^s@8IjV`SLa`5kbtw*D->m{eK0F~?O+BA< z8z8nKlj}X=W{o^z7UHq;Jc!>o|SM*b+T1&;xb5U~(;*2aQ+ z_oV!$g7FEYiDb6r7-M9sgxD~O0XHEor5>Nv4uXXL`q3xDg3L;H)gx#`p*MAf#&y9m zTG>pouc)a#RzFe@#^PdRxm0+3e5MvFfZuE`g)_C3N{y_61Qe=+mzzTJ>+zI4%-2{I z_teDzF`ps_x4b@3$s_w)?{A@qm2xM>U+OnGl>Y`G>?GM8-*9rvtBwRgQ2(MH{nc`z18xUZM?YC|qNaj~n|Qbm~3EqB9ktXkXDADu_=aBIJF z)IQ^9tHx2G$v^RTdX8##i|$NL9GTF$q`fv6KqvJZ@UQf}I~v7Fax27-y5FL;dhq;u z3Y+7;{~h-EKSs7&;h^ab{8tz)mqXkOmIDGm7do`0(r4pS>tn`We}_5$3&V&9Xc;HU z|A<^%vi;d2<1w6bs65|(R4kNWsESGdui`0HSj4XLm#|%lC~t{)I<6Nq^_t2!7!0Ju z3!_qnD`I^~6MY}+PL~x`T4r-ds`&5`*RkUBuls%zl6JFi0M)9&GHFo;-|SQP1V zj!G=GnVn?mD&C=8qN@fVP3B%*{5j;~*uTCf-k4B(n@L@Cr%vGvsVYUd@sH1}y^h&r zR9K6_m?D0uO#k<_1lipZgJ=A-e?^8rfP7q>2YAs%`_C(-uc1=f|4$;Xwz|x{4D9L?g-nwm@>`&?%eL-vjWjGM$PHuiA)tyzBCykCeo_84+(0& z0bk*P_0j%D311t212*|puZMGw7zxzl@;{c3|EL?R`VH6$+BOB zJ(3q1wyAoQlVCFyBfo(rY_tKK}kcUz^WZg<|Uir zM1;oE?6Xw_*h{T8I8|O!DaRQ67>n!9L?vtHu|$nV6ABKk<*_s7nU3)tq@6qH`j$LL%5zLYoio-J%|e^-x3>s1=BXSIVa{l zKaW;Y)}&>J3a)=ES;7UQXIK$wp?#vsqXDR;ujh+>L;(wv1?J`=*2MqC*?WdHxxD?N zAgF+XN>dP!4haI%I||YgdT0`wbOMCl5k#a(Cn5B10)$=?Is($WlF&kxqEriYq2k(E z>;0d7&UN;?&votdAs;f&WafUJYi9DyJ-<>L$KbOutRBLplJWQo?~&{%7*YsOS68Lc zp_=7jT{5x9NXN=b!~iHN;>E7Vz)*o7jd^s7&CK;B&0N{ti!K7(@~mEqVw%qfpEv~$ z(TBUW6xJKN6#7}?67_zq1KLBe1G4xlJ`A_G!=`w-G;yfh0|7d9<`g=0@S|&pbS+f_ z<9>L{SO9M3zJVVkhB`V@?M`*Jn{0PZyQxhpy<|c=8tX|r8nx)l9R&{S9obysFh7fT zU-FZ__!YfTZo*$BJkS4oqE}szDXOOAB>DDB;e>9Px_u+C!6>gOtJ+ls>VVg;IoySc zbU0$P%w8_hwBGS;_nGMLbFj8YTG5r2t_dQqksH|uIilU-yUFejvqbt^SS#AK2J zb-BXEZW0yp=cy%n)4?RT{SDlRzcviB2KiPkb*0sUDNVqcMz85yzlB}Mf-&Ir@bxhc zKEXY_Nr1tUlt`XwW2yOlY)oAA`JiYm`>i|Z2!g`qw@|HUn^@Iyc6T%Exe&YFI3q!S z*QIJf@}e+b=~bDt{Fspsh_gs9G0CzbQHlZBw6zIyHmGtzqZ@wauL2a8!O?VDY6jpZ z>k3~|cu|N5I|nQGgLI6OqX8tWZ@|B%z*UH&ldr;Z`<@weB85S3%G~u{dghI%&R^y+ zJi6pJ-lC>5ibDxJ!opc>v5Dwds3qgiGWG&v0M3?C5t{^uiiw$D@!?{n>?G>v< z31g|);;pZdfGA*xSk|lw#q;3so*FHpyy7NE&z2KQ7RL|%n?h$Ho^PXhv8R~$OXddV z=qH<1-~5$z?jOlEyKzTsA4bg1n6QrUC9BL(H70+Ry*y`xPb(Ya_Ei`sfbJ?knf0;Q zINIVaXD|zt^b)vWOTzJFe^5{VDkrp*(oeS;@jNqXyLe`*uI&w+w5DcF)T?sNa%#O=IXRFmo{%%>TQCx&ylh zzVexFf#N#VD*)%m-a>AeL1p_MagqH{);%AJba-o+r$)BfhCJ9T#Nfyx{LO z1(hYniuul+gL6CqUT%N!ct8@hpHaI~DGWgGFZ{>#kC1X?_lyu(Yea9qzSN7b zSV>sL3h*gpY7pFm`Op-UD!DsJ$`7yTP`JMNG^J{*>;JHAqTekvo6)FcE*)LV|2+EE z6`D^81hlraO$+k}rury(qzsXt!q4sV$;c5W*wy>zsdN07t!!#G(p!d(Q~h8(CwUN~Ix z-iG%NjK7@WpGEWl^WR{(q=H0 z_I(s%>kaKv8njlf5XheEfhK}y^WO>ISM8~DXW7<7m%s5b7pwp@2L!yc4HajxKPRq3 z=m;3VOmUb=sss;%&D}`Sw%|@+u9a%3NrRJ19+PRG>Jg+han-qe4p+egW*;5xH5_M+ zuydM|3ER-`x5Q(S_;j48i5Lz9MScSl3UBb$f|TQplJ|RD&+xwon&ndH3>tFHSah@B zN9XZB--YTJU60f9@sYggG4m~A26lER2F+E^$Sx;fkfpgWm+0HC7Ow9q zPfHC+oo5U~YMey|4lBUMDY@C?9}Io>-MrUHpx<&SPuh~KrOGd={foTJ#tgn;nauI` zolS$X{Y^oe#IA~5Bz+(Ma;ob8p!)WofA;t)Ow+ehr8vk&#=Pr4V_xjBagZ%zo~;`^ zeyAfX+6pgfeZH3I5s9*H7R^-{Q#$@?XhT0Cdc*e?4`une0pA-s;AW9E_tc`;ud<}( z_WSBcEktJ>$zxV~9^yNUMsMfNu&(Pmiz(F9QqmF;GNIOuj45TFb?pJR0sB=Sx=cJw zodI41bB?lRBi&k2Vw(Yn(M%w>Uy9uK2(JupDC3#tyo&d-Y)@dYj1443?>bVtsXbFk zETE)RR}qmB6@5)XL-f_5^slE&HiYKS@bIv@yqowhrVcLdUcPyO>ZRO-hi$i+bAuS| z1SUc2rdQ;*!JqPPI4r3blT>ooz{^n?#n}a6`ZLST2U3z&e7A5*ySiBQnUW{ZMii(E zoObq>9@LpS*H+bZleq;Vk;eCPT;DUCHQ@B6e?}L=Mr~}WN@Hu&n_c&1>I0s;Opj@@x08Zx7@`WV!skM!e;Jn zA?NQ&{W5NbnQH9^-Y5UXX#2tZ;8UHFj5BL$Jn-0nO6G#i%0aw0?@G*Y&~&cH&hR3~ z&VO~0kX$qa2gbHtj`H};e0F2prwNO|rk7h88|i>E6*#bb*VFFY??5?#Qw;JdXUj>5 z^h}tco}O~8FbtQB5L)Z>#h-L;sC8VS?b@ur$%e0UHo-gBqwKo^Zb= zs$Ek(@BgIPBq`R)#(wWuG~(VbK1*}5(9aO_ETmBEFV$141{C@f<9JmFcW+`fD92m% zSwV{D1BT6OLpA!NQ^dn6;yAL7kpAWksNz=E@2chs<6VdjK&awXVl=V4YOcQhy@#8r zpc7~NSMXv{z;vmHQ>TovUPoXIb7hQ7Dfg8}x33E~!}h0$9~9|nsQPohzJAXelr`pB zbzcyBFxP;sQew7>dBe8-h=WNoWva{<^_Kq%oetdYD#$|F_qg#6xIW@t$5bA}cU^x~ zOi)GmVzAAiK548LI~D<)3H!wGfxq0+lD@;pD7da(fnt*itvxFO3)g;YxY+zl6SJhg z(piz0VV?*t{@UR5ea61Q>BRDhzsM+%Ao$zJx*B%wJbRUi~zmDqdjG8Z0U7c zexbBXHHqb|Qt+4K3{gu)Ind^o@ z)tSCEQ7UWSxeo~M@w`FWJ7_KUG}v%2nziv3%vb7?Ov*D+YS8_QAu2lXPK8jWuD9qL z=((nC+Ov#uf-tI`{h-$7XEI*2Xg7bkrV`gmLH9mYvdmCSOKx(Jz5-Ui`aTrwOJWL0 z-J+-G=C$|smieNfc(TdKHeqE!EYws83P)E3olT2D(S<-rMc}yEJ_I*y%MR{&+}S3b2fMKDs0F46LZ&#bm?(|B%ykB#Lfc}P*I?J zN;=fa8zy~XX6uK2p`h{(tJn)gFXs4qU4v@XxPc4Egr2!F(C|&KFYu07E6*V0er6!E z5;#e6&y_V<;9@kL4SZ+LXodsQaqWi$ZFTieqCnl5s_ZXZNBKl;-or}W+=*uF}nN^xLaFfwwHuz{i^t(Ew^BCtww6oFkDV-4X>_QdTAec_D*B4jI%{upl``ax;ji;E) z>6?>Q^ZebT0R}{|u#=No%|NWBEIJuCQF+A@{wTQwHY4MN=qkwQzun?-+I;lD{zoue z_F_>bLjgikGM`nimNCw%__+MGQ9es9&57|?C}fxB114eu(YO_b9FlC|#P6+s0OI#F zhs8xCtHSjrbZ22Fp?d+%J3f_wGUECT2JQ+4YIH1v#=Y_Ug~OmNybw0{>;sUcs=Y9~ z2s0}HsL{#uIN;Q;Z9!HFqZrL93!iMp%cB;8@B}r}{rT>L%Qfl0)Y&-?Tr{(87ZeM$ z9D&~$k0rKKC*O3MI^r78Hx#Z~HvUm+N%oUlK3Z$>6EztFx=xW@dxuN6|EBQV3VMDz zdHx@pUQpm!j>nVgaSdl+Ke(H_gR5VNFUjLHJloTX5$tW68&i+f!txlvW$&H{7-mM-rc4EYp zOc*HN*Lpjupgg+A*_fjD?y!nrnhj(5>3M7xtC;AQlXHU!=;;j8Qpk(~aSVvxUr3Uv zL3$iMnkPBxB_R-7m*9fLf~pE^a_Plqs**ZOd^7};)=kY{q(UX}T0GLqB&*}=8|bCN zk}nSHfyI1(28PrE1tJnZ9et18csQ#1jzAw#&TH0iZY z>{J7$Rp74}cik;^vl^E#>0OWOw}Yh?YCnFSEIo@Z5G~EU(5#(jCD@CTRWkY3_$flL z@*5cgj;XwXcSxT2f5(L$Y2NwLf7Rl$Bd@Rr2oEj4=lth~YWJVh!gUeDKzjL+>(r-1 zX9Fb1SrTT{%Vq{a)XgIaoBo8psIvUbTBwr?^Pq?uxz1y4GJ>3vO5PL+%JTK?v3J`4 z_jyz1Wup5Bv9K1-0NHHt?LE^HQup3>hJXSx#Iy{Ue{@dbn$9ig0M4}FCi4Nx1Vhjx z>?_q==Z>t`rQ5eZ+GR0Y5oWXuq*(oQVp+$t{~fBF>_;ISsjum68p*kV%YTPR0C^`71lrx7Tdxj9DcTZiD|`3E+7I8g6s3jgf3 zw+=CUMadd7ZYm?7GI4OM6D*Ye!K_+{k3-SX(WDYQ10$HsD#T&FNRA7bIUTIF=y$ ztqr43MT))`w(UwUUcl&5474CY9#?E_cG0q%?egqKI>62TsFWOqFI@EJS~fVk6+<@| zvf>$p=?$jgpx_c(@O#C!1~q=QRf2nihQ5@KQiPoh#6h8{ioKV-#T?eqT}R`o3jAZ4 zO@5}jF#gXEv#Qfi*<=87yG`Dcsy<7isLC_S+hK5Jvj8)XQ6>}b6nM**%r^mCJOIOo zo4KDsDRUppP27pTGh;CUx|P@1snVZZ8gDvsxsORMpr-9;IdBDU@tvHXvLKJLtPDq9 zaeU1?hX3y442z0u8~U;AjVjD9?;@!_kK3)6U7NTwU&sFG`&Ty0#SV|jx1VXe{!{r= z-2;YC|5ly(Z>#eCcUAunTK(^4&!bd7j$sGk+`IXKZScU<>cb;UNb{z81J2 z*Y-jw&OC@5rT*_e4%TOUSF=Wbt36=E3Elwv>5yc-W6-T`^PEMlO!|%55I8%hZeZhx!WofLwOnqVP z4FVk+tnr$WH%G5WMN}FJOm-^%rbr(BGb2Ol)bk7){Jo|{77YcFzbke+?y3~kKm9z- z$9QOV&ev)OJN;*t*Zu>g=0c@{qtFQdW<^QRx@J=?aHxdkbw)@Qc_iMCkAcNPZcT}Gi<5oO1EH-?Oz+dJ$L^Kzn!h0eS2l9I0mwZjZ=H&7}J*o=a27Yfs= zhao7qO6i4fKza@EHv!Ms9s@o*IL7npyhZQorwU^l5KFFjB*;^x)HQ;cJT}a9p!J0Fa(T2#9&Hy?UDGT zb{0hueBJ|p!c{h`-;kBS<6v-NHu|P7M`%ta;j5K8;iWrnvOHKyX0O&9zA~_R)!U4J z?gQhfdS!#klf)B1gqJSGo`HAc)oWsL8`8oZMFO)Tic zY-=j`Pq&#boE{ZURB7`^nk&>mi@%ZC2QFK-WTnqR zz8@MPT!dMO%9{VjzRn@CS*>Hs?U>N(A>_3E2FGs`&c>sSK$^IT+Iaq6W~6P4S+Zj6Rv$!Xcqc7-;C+aDos>;jr}4Qw ztp5%O{6KZc`@H-+#mB-4&=(EZSR=;kyOz>h>tNPuls4nzj)B>QsLckXaCzPCqqg9=^*|8 zYE}BRmUnRZ{e_PqnudRkoIYsc`cJEI>28x>&;EB8PWr?D*QnEH=-IkgbG-H3y8aXE zt^avrI@14Nx3l|SK~MkTe?d6YHfd!NIpSx;W*bsm(P29j6lH5y(zfC?0@*?{BpS`? zdd!TrsGh2H=vf+HT|8ym1y~5W{7sRTV+@_Pd_#(moJ`)OQc$C4+DW0>%jkl+QI&nE8;&Rdr`r`MuazeQAM?SHjBE@oIb190%t zeI0|l9GqIwA;Y`6f5`C82h^MYP0(~l{v4lJ+j9BEq?vzP)ZO~` z1cv9P=)ja>>r6L88|o9K4&}4!MpOyA^+HGZgV57zk)^yKxy)R2V;g0h;jkS~>!sBGKdKp4W4~ zDV`Es0H=Ulz#FLwE%s3KRc_`e1ZCfqTZ@0i6WePG&M#P=k?#L~kfGGh-jUmWpr0f4 z!VF`q1Td;JT&8u7F%^W`>(D__vkmZnYlw6K1xk^5i((} zr=|LHW^E05IraMN04Ana8wpRn71Y0_`18Ki=2Rt*n5aVUJ;R$!>=j?xIVR9GnRAm9 z1Id_(VCt^w=HnaA?c44^BhS{T-q7f$^v?kP*?)d@^Pnt0f)`%V2u2$mRt8fuH zb+=y77)F$%9tu1{3s^n_7#Z~$a@~1EN5JJHFV}?r$d#d*za~Z`8M{ZuI?rbP?xf2` z>0S1z$S+rehm6L(qOtQCZ%;mDQ9$|Fqk;IQ}FY zl)fgjJRs28?$8|50TZnA-Ma1EuFGSNyHfp%;r`<-62a-K+NRi*XH}@&E#Ql7JYR$R zpGgbe5t7yMG2__Va?^7U!jKy_H=|#GlAW1z>Wayfpp|V=wddE*1!gSbN-DT8CTZDa zHz(ePMO~ecq7=Q3X5@5`;@gk>73Q^o`Ehgk&FzHA$dCH{fza7p>^YODxBTyM2xST6 zq(9~B%pR&BItI1w_{L6tJ0u8Uv)m$OBi4C-ff87BnsYu&bBA`yw3)a+Hf#{w+Aa@& z!=#v=ogP&noI+h7@@$mz4qM%_^z1>WqaozW_F3c#*Os5U??pLMHqmKZl0e_Vp<8el zwY9CC`kl*nF9kFr&mJ`t=KFwOR_!VccfmTEOXa1wCl}k2clC0iWXI{O+roKo_F~a!N%vbN3@gQ32LFq-tQqeuZqCK+OfCrFbyP zkRnudY4^NZ6xwaWzDVm#tZ4_x3YEj^9)q26`eOi|&$KXADBUZA(g~+Ki+99X0@KI} zQsL^4|KyU-B!7N<9#3)a9#z@>k=+1Cp&(~ATul&2maHrs#6cEkg_B89=LuqpPlhnh zDJzfVDT1^kk0j*MS0gWyocJW_F(T%~rN_T-zC6Ds8YS_O%eTWjG+**pR$+Si`NzST z*I*9_67BtF8ga{CiQ~$hA~^pir`mIO0AjYsZ!avHN9=~dJbWSr6ktNHS=~MK+s9yBsjn5 z<~fu#dyW$cO^+$n34B;U&mHx4itm?cPelt>w`^wbEYHL+qh{RHh0Ua%YX@G^0Hx+; zMpmy;^|K|9i@4o<#(8AZ<2czaV*%VWwPCzM#v@%7rT<*EINQgUY`P@Z#^m66R_Z!s zDLD&cuNfem@{5FJ6|Yrpvx8}8GGR|frx^JJUs=DknlV#g^;C=n8@%NOOgCE8k!9Wm zv?4d6_H@+cj|>zTO0OUMHiTHu=R6~cycD#YN73ys#*NmKHm$Vkm!)d0>bk~JtlP(; z(lN+xXZ@B!fWmpag*MlnS5HXJK_``KDEe*ld6yNux_}#sm2qGbc?0H^d)hic9m+%h zNyAxWgrlsj!qg2%l5t7T=WSHrv#1Dea+aEBKAFi78UCsd7(mQ~{1ZrJ1~Djg5>f+f zHs`|@eS3;?mM8u?{KmhmAX;%wm;2^CQV1*NG4T+7)oJbZ!GWSv%XHwf z>1A5TV^usvQ!Ad&-z?#(8SUoi`B+@do}A-;6|vl z`|m;7VsTM3OtY?VR)By`uKj_Ys4k^OB|!d$K}O7vpP5}S1nXg=_s9X+IZk$BlZn{E zUKpX&N}4To=jo~;FWIw&8e2mx_2y;f`^~d&su|vjU1}1$jj|E-hTyTOwjyw(O~dU^ z@)QX9D5+b)8{#kC2`j^I1Rh0HG+P0M)w2qL-0o~sw{+6FJ}ZCw&Ab2Px#ikr@XPV! zF}V)jAg@*)9+Q@swI)3M|@;=7S5tJ=eyiiR&|6*+@2M3K0 zidV&? zdd(4aIVYTvpDLDSu6fmP`9>KoFQBZ)6L|!*b=|<3KIs?^1`4JJrdH8NKM-xz&4X^R z-Y1?pR@TLJ2FfiTnvtEYC(3*oA0fzjqGuJ9)G?;IXue zQ%=#(B8X9bG4lS%UPjtC^cfJ0&5}>;@*H2y)Y53{y^KmC=C+BroKlYn9AHl2tB+4N zl}dTy8ppdaLOVoZAd#A^O$pMWRD@)V+@xd&iW*$Deg}STTAEuzaS`36_x;_&AGdvz zksYiDh!|xtHPgb7tZ(xK#xxP_t5%!F_;l|Ao1$p$CpLyT2 z1eMuBG8sTdrC;BL@%q_94HkjtbkI`*~Rr;Yw(0GX+;?L9vL+5%c@*_T3C+HlqV^l-Cn}K;v@-*`FGsfzkwW8BzOh4j&D? zPYep~`PI&@s}#22U_CJ_B)ZpyZj~S`JQ{&l9Z!D@7+djYc;yE| zrwu<=Y&&UoY{d$+$C>?2;XK{auaX)xyscqAVfu1 z*|pZ_-?-#$bTmWAz)X*q)qE=H9-?h8)znO{OhR?A3o}6*P%A}8klxTfjI1gmSC8PFlF)f!y0!yv7TiUbML;F!Eq$Wst-4t=X2!pQ4E0QB z&6GzVAUfO}Tx{1Q${+rGG1##E4!xUc{VGF@x*1)b-)qoY&|a8rHva@tO!Z2E3A}{c zU7n4fLB4mx(KB_9YJq<}=3OQ|IDwX4Cia*;80>MB)aeuMHj?G*=TI zW;{m#SC5eSS)MoqMl`deB-Y=6F4cFsT)Ay@bZPE*CeO&iIj!r$v2cA~!ooV3Gjy_& za0ryqXKsi6atQNOcKYy>jA8wpzLaC}jzZK{OPYY?KB z1lGfD{LG@G3k;S2I4xAH#rLdVvNLG*rFAk!Nfxo+TIX6O_gZ=T)^yHTaqtTF7{~()DkOXXroF zg=bl>CrFLCUE2dc;(D@_@gUOd52LKod1!~*aUksH7{A~FGXx?i0I?DFy}Dxcy=Cba z78<2Z_3;%~0g%@CeUqZ9PF_a9S%v}lZIfi+?}quKA=#6_gudLm5fBj#IQAEI@hYtW z^Mu~t=5~ibLRl#MD{?6^Muxx^vc4eerEjXwyjATGGG?aa_wFJuA7BEv;Dw_`Db~p%SL(Kfhlq-kw)r zQ*2RjCKmB!oc;?e@#hCpE$~OJzA?07t`cwVj8r7z9iEKUxtLb#M3jJt$?LxF|eRg_@tM5Ln6yfNlG&dyt7IMrg(?A zCYvd-bc%UYk?Pw+^+qPlVY3%|LG4;M^eE;EsMB6RmgW$sJXFHSoZ^!kjo_8#g|5J3fYW(Flr$nKrlHafd0OQ%<3Lgl{ zel4iHN?mXzSKa_6z4#19Ydos_*PV(M4mgY883LUT6OH*2dem<>&JsB+PIovBt{+?R z1{)HESw{0fSWt6^dx$jS(^9Hvvu*n)KGYnV8Fc{J^#d4n#r?uGh4Z^1#Ub^x_c@DS zYVMMSMra7CJ?#pA#h+?@ab5eE`0`9$(MR$f5BZ>eJT;~A#lOa^=V>%GEu-&qb&S`U zERg)C^jk0~M$smnZW)+f=R-s*CQ>2eSkdiq*_XHj}CTn{kHbQJH?ld zyWKg&N_=~ftSGcTOzQY%!Z7i%Fa6W=JNw)6U!9yZ{MJeISlE>AsP#Oj7*g($jPwL? z#t46NQ+|hSSN={Xxi+)y>V9VC$Q(?#3p=;1zXw6e=tZVxuU40Wl*&KsQof|63Ibi# z&bTv~kdL`JGM8z(?P6uY^+D9?$#_!v24;rkQK4SBI}9Ku^oepJb=COP>fK?ISH2LW z-eQ;d^TqB!%{YNV_8eI~Vl<*G|2+}1(NzvWg|Wy6z13Sp(zOWKea-FQb(doD;y{nI zc5yrnJ?}CHxBE&|FCOW%s7#N%U?D4%z>KWI46nJg!FDbb6}0GZ+d}ug{aTnM7$+i6 ztH=^(I1xzU6J-7Lvgu+N|6a6nMZwFP9UbH}3e<&eIx-Wmdbofg3AGljpvhoU*GQ4n^ME?}nRiVdezcv|8Cwxi_FD z^Qj?+X>y}e?5Osy(K%jS`K7HpkSa}3m4lP9)SF-vtmxZUR0c9>5y@K{{7R}dbagX_ z76M|U^rhD-33|#WC<(^GFCZ|@TzP8jK7i)^U*AV|6Kgb<5vyIllN2T97ymjI&j|Uz zqUPR}H;_@*g)VJB{I;Bh5f4gdnX}mX-IzD%QY-~2EFYh0*jCq&#bN{M$DQP9)r@wC zlzjDfwm|MGBgOucbYUnZuZ0u@Sit7P;^L@4!V#@6r9p^PVA6xJ^iwg0* z6x8F@J@>79NE6r2W!)R^b-{4WWf9sL1S0Sb;}6Nyc5(Bz4a2bu|zAZFGZjIZwkB@ux1kY%6Dcj5b#`gW`+Rr9fK>D>=qzb2>HDeaO$E21x z)`=*5*dDKb%~V~RTXD3Pvs`3f&h6BrWlOTo^Z+X;a3A^w%ZU7HXnMPVk=9p{EIr2q z)VV=LW-<>&l6Fl3SCA{2KTU4r6$Ghiwpd*^$unh!TnF(8#p|gpwuy$%?JkCgOE6AX z#Xs7b_?Ve3nix>*j|DofQgCB60V~I2WQ5iLF>}^RsMDf$(Ei>Qymov1Qy{g~NSnTc zVVCX`U#VXo+X28C8VmQf7ma6!1PuqA+hP}FY8m~Q-@+bYXnd#8C(};FWH3k z4G~u^(SfVD^8n=!By8+jozI;>vvu(ThuD0BtcJ}dfcUuwf`mY&TJSvmo``Y1}8& zJTz2vVR?gUO>eT^_w6STmXt6ZO|=UT5Q0XpSb0=Mu|=L?r5mK|&%;%MAKrBG$Nbe& z9@@AUpXBbE&sCYPe)swR?-I<#|GS<4wdDVxFT4Lco`2d&-it`nPk#nw2XTKExSh~> z-3l&%jwzv_Cg1o!YFEj0eSnI05FhoMu0vN{FWSjkOpkaDl$RPvB4&B9q2kDxwjyng zv9cfEujz7tFMW-}3wjF0#1HTMoaIVM(Hbu@uyp>z zoDaXV;>(%?JK*|F2ud^NkhkY!F0J7YC9dq|+5H!(%2qh0LpjAwLn|`gb?)1H_AZo2 z$ap>Xr1E__c*BiE>JuS||53u&8bdR4`NAGf(O~Ax(x}rYc4phcG|)>)%=YOGdYRH~ z3I@j5=X-sG-V>+rKyT{2RUh&xLA(1XoRNryYy)w>67yyB3-UEGyT%P;c}8P71WLkK z4JjD-+%6;YMa;4>dWPe8NRItz9450%6wb#!cfHR*3zVh|{;+*kFtktsXL-3W>nd

!SbCct-0FF^QnUC%)q@XwJ?yu0Y@eJR4dTnPh(wSp#|C#uRHY^0Mjo%fvKo`bCXEv zPvZ6>f}gB>;V&uhnb50}uc%cL3lE~{1J$kB`F*)7!#^?O91DInVfLW{FzInro}L*C z(Bb43c!DJHJ?K}w9^&G0d4XUC!R;?LzOs3iLRnBjBBK`$DT{Ae?n8ey6?6k5ZzrBM z5ixWjV{g3HO<=mIV{TfIG91jv=R!M3?GsBgj#RkPsMKq^6a-#qNwIaXVQ+9Z`D{NZ z(4Y3JT)pkqJIlw{7_q`@@VmSpuGGWJi!3>zKF7kR*q#y2FOK(HUs%L5_jd(sJLqFw!)s-5iZjIrXbJU`TcnEto1k@cz@?hq0|3!me<zfJvb zLD;8t>NMyjnOj78>-!b$+Q_yLO1`-s%YWv4@V|3@kPQzu2}t_007K}AasT);E3x-< zWH|3^@lWF+8B%a)!j(p5mDz3miD!^IdFQbgCT9BChHWnqQCS0*t^Tv~Pkz@6eCx#N zMTU_%gKRDtQV`Ji%ateU(mTV`f}HX7sBhx=Wl|a3>Uw_jI52p}pAjLhA}T#Wawa~| zbr-FGv~g5YR(xyMsIc6ZU?xEe!+P7cot=b${!*E{Z(yOJAjr?Ty(NgrR_as!P;H99 zgfoddVSs0w<$t6tf-+|v_7(%Vn%xiS0)MSY2Yp%1%E96l`|DYc%Ga`#jZj={>|oHn z?bma%ZI>&sgFkyEQ%V3S6$Q~Xc8yxihF#C@jz9p)X#Nr1E4GoDOw`SEN8o&wMWvEJ ziPhO@p{3uUS28hGaCZm-wOOA-$?xyK2Q!9zQcO&spvo!md^ecAIGFUbmEFqU&}93h zQ}Pbkyh*@7*frrQSf0~3`T4dSJgRV!4 zf0gb*i^ZrXWdmYaqtma%GhXSu_7DI!xv!Ju2gXBe1onhp+|P9JmrQ~dePF$PjSd{L zDbN3>X0pzsQ4Bi_x^MUMNL04wj~)D_NdzwG%pCQs zRaDEpe&$QHiH5$OZp-I0TVonX7WyG~3|3p6ax+C^-GOJ8^ey!4qgsn=ORRY|TIor| z&uH5^TpyD@7N7=+GB^Z-h{pX2BX`8?hefP>b6<$|i+Eb?*xwV<$u01Qc1m?PKh7|I z(0)JnerV}DeXC{uTf&Z%pNW*`$AVD0h+#%F^(X%NsPq1)?Hu&RO8$PNgSd%JoQH*t z`6$V`)!T5cs2%RnojnQkg6a4;8v zn7=4N%afj5mmgvc<83Hx-Lq0Bak6hzeGr75;j2YuRxbc^>C1y)XW|Z|mH%?GA?6d$ zq+*jKMOrI7(Y_5M1YH!%6+{;~4|OJ@1_wo(URS(KrKH=OPVy#n9=}AP3J+T? zh=Zhvu`2Vq8ak*qV2}J*vA9_bw1(G)=Ba0_blu}YXUolWmV}XN8$4%`&D`tSWpNu; z{Y7P1@1*LRY^U-}$r)0P&e)PUg3hcOGq^*=tILyBBD5TslF4m_?{W;A6JF zQ^h&T+|4&%`YZ4+R=CynCh?g^kFaRj4)hB%lZR{1b)TL#``T{|Ty`15qth1hF>nWm z^~$A8y1DVMw2O4OlaqWuebEDuKF=I9{yo@x zlvB|~u-n=9(Ycn1QV!t(8FR2i`$-fGWo|$ADJ@AmL3CmiF%gCowQ-l zt{jFGA?cEo90x@x%d?2Um`s7{@SmZ@x9qT_Yf~K6Ghcu~lacvK;(R8VsGQ{{R#k@D%ZJ>uPJoOCOsVb=u{(H$CjuZt6G314g3sF%JbQyb+-|V5k?n@SVg%~ zQAF$6!{uD?3i$}F!v;U%^2tB}ev~hoNkuq*32IB||4L?UPgL-|y)O8*i48%T!0c2z zccvnZ%7M9N&biEbUHz|mTgsnmI6G35YAP7rlOGm#H@7eko*y>`^uG*#RZi;elY@H} zZPhPXTpDY(=GolYo{C$Y$I^x(%ww=8KY$d`5UJO%?pwX9HEum=ZG z$+A(*Uk%qBqqW}~DZfhS{!^u#gv(PdR4>)Rwziw*&y3~JgQT<7v$|xZ)|8RtjTK;a|`<)gLF(~JLFji_&w#*bj*VUXt`&3*$ zOLx$qR0O2nt>M!kVu1QEpUicr{#TjK~H?I6yZQZgOj-rJM zxgZA~!7)XndPz@Np51n2pU)$yA@^>l^4BAN>&XY89$&$jq|t#bZ9=Ps#Yp9sy5&HMWGs>`X0l4`tC zkCIjGLdLViY0jlWt^CKlLoeiS)d;QqU1UVPX};VR3f0bdEMAFP^zfjsTyRAemcBNs zY4SCZKCXV@P59hm%|0Nl&wB4|yoxI#Jct&FGZ6L&2#QZK!^goMcxE&}2EKLLmZ+0s zq6pgAj*jS_c@6$HpW4d&^jv;6U!5^#d^4?t8f!?YX&>R#^GFU@Lrq=tK5^?wd`I%< zK$)TeyQYiMHri{~#(6@Xa4$FwR?|Acnl8{=c9QF)^O_1T1{ZUK#5J?Of0|3%?znu5 zLwdHh(Zxtms?_Kr^2yPV7H*U6BkWyV^N!K~mOM~fG!{siX9;Hkl#4MGexJmCP_WGuMs&u|QIvzMy1sW#1)~x#g zbGzsTs(X;&EEUQ01x%B5CEj0;o@WuNe{4>FaWDRdoZH=7mxD0KY1OM2+(4%C#S~N~ zHsw5Lw;mg8zVC}I1>?Ovae4V&gIiR%D9pW8wH^8zCKj1Ty4u=2HsnLT?a2%Bi`sV7 z)XJ=7miLHhzcG}aWpPOA@e2E8*CEp6if#I;tljt*5;Ay|b@V4E*y^;&i`0CLZaUAp zw#rFzA4>*0cxI6+(KX~CWx9RKP)Y`(!)W0&V-8P zI3I#p5F;Pfrv`tTgISv*t{zFmE?D^P1o)C~<;^>nFvmU0IO)h|q>bha^Oj@gHWpu5 z_Sa7{t=%$wx-#3Himl-stf!n+N4-jPR-cxI;L)>v)Z%ih54`a3FzZ7}x#rknE%j=e zx_|}dcnqKtH8|S(?hu`bZn{KLTp@NDBQw=wLZ?r7o||qmXt60<5v9Ngg%>bgZA<#4 zUcY{N>jOQuaVz(-cCak4*SgLoA>4eXzX~oGS8GEj1!4Tg%ptsd(M_aE7DAKhl(cUN z8OoJmwe@IOV|(#UWiR~cNLxN~rg|VHwd8VSvwe(wWPyByPdSHiCY;7Pwc@dO+6pJD zd4?<~M@=dJcI~}ZypqK0X%H8JRwgqEg8b^Y))1T0G~RYeF}r)iIDPEe|-P=-Jq(wDYlnF;d_lM-#nP59_& z10>{Bo=eMKK7X|$7&_|w6QHYAiG8^!5}0x|-WoQx{&mE5%z)7TsT^j46O@cB>RbDM z3}+14la8>pewxhF24D0S=Wrj~uJc%t;35S*ipJEP(IQ1Dw=U!RDYk;rpsW@s|IllI)i& z<^FIb(i91WU#4jCL0L7DMjgu--j>G4%~v8Y_=t#Q`C>r8Hb4BfZsJ=f(!T@n+W{)_ z{|JTpyO&Ug6l@~~5Dx?YT2_lw*vfCJe{~byuh6kqPE!|A#N(C!eh)l`*LqIL+NP%t zq|OFkk&rOA(59$HyP4F#kWOA!H1E-Lt@`{xa}SBM`V&w=O)@>#yl3g&Sp(;zu#r{o zr=%Cd0}U+bi@n|Q#go}$@i%4$R6Mg7|nAtRRk|(NIueZ(5>RUP`4TUfJpB$ zX5x7`72H^u03eb_e?undY%+h3C?-uubBhG41|Un0D!8!)o`mQ$4wPQx-dE=TU&9EJ z{%aMJBb$gY`^z@6!ttlj4>`WWM#Cx3F2ZiXvC=)z)_=asKcqY|-!Ix|_jf?n=@iqFfAF9Rt}W|{+saIYQF z3Ssokm{Ko1W;QoM z1S&sMyDz*l=#w+?Gjw{W(6~xFF4j(yU*-ZMm*in-3Dv*01H* zq<=KV2L=d)4Zcbv5|VlT_-_UNR@yK#X#J~sld#_t|5h4U$6J@d?!flufepk}gvjiH zMQ0Q9{^BH9rNBV{X$Y|bog|D9n=i0u4S1LHVd^{S60~UQxHwwEXaE9%NQF6)1y|_k zenkR}JwGPPW3!ldp6(KE^l~4c2SU+crg%)T>7COPQ!aWqy3xV}1jxGlUtaxO1Nu2ZwHVlFy1$28X8wIvqhc zLej`&4I&2_wb+7wFP$hT%(>e%?t_4o$+3)D2(rbDI0{-A%PzE0ZvMc;;7h`3!k$|# zmB85NvG`~?T3iCI_<5QVZ-J;+`9m@zQ|76#>NgNebSg7by zgwlZ*-AXP^IyPw>XTfv!$Q`)k{$Vq50*3^dtrB_t9kqsU9f+xh{Li9#<8*223`*<> zGIj8IjMS-1L#Vhng?DK#vB)iq5LMvuKcv=4a1q~^Bw8a2h=;!UhYv!;Q~Qx_@ufSb zGR9`2DPnw26WE_FxdHus82dbFbqcJ^K( zTb}kyo3d!{<9)B5$hcUiS^wkPqP}g4p0f^Ic7Et0ki{qojHc|CCy@OK2=m?uB`XU> zes|b~dX!Nby#D;F_GWB?OcPBY*=Jh3=4A~R_k(@5Q*|&B1#(Kjm&@6XFR@1!lU$yx zQ{RP^-Wj@p@fKo7CT=_kTZO>>$lDrE2j+g&eqwrB< z%j^}OE14yF1i9$Lp4Fk{uXpRD9oJ!F-&JNo6YH{r8 z@AONQ)%B8#SLK=0h)=l#uSg6zI&u4YsLE%jA6y4vfpldj;N7*qACAm&;O^! z9XT6(LaMtzltmxde-HhRi{v96`zL#{KQ&$(bwb+fdc!~&_{s-qhy%_Mjoe@C|GlP} ze`@OShtdBX3&p=|b^N8o#F^s`(J&5yfZvnI_6(A!J#(`q-B^5X7&cgK6HR+ss%nev zlHPy_ND8=V2qQ=BDN<3c=gFV#=mZULhKzlp7ap-#-7`K0md})IdisrZ zt5<+sFW5W;*Dz%2JU`nPp5-^k+v>A~BMq zvwkmR=Xlvfh9AhQF~+@N!(C!}b91Y%Bs!7;vSIY|=N(NflegIi;|SNGiKuJWH0(V+ ztT+fCfDAPj;Ca@Gv&40i9DchtGS|6bmHhVhC!jL@wpv;pBfEHfiuH2=8KW(en5h~d zPDIRfwA0^Ger4-|uV`-F(yxQP!X0Q;AB?%28HYirtA8jv1cC8(I4_imOA_;AZ_^|v z5@uE&#nvC`#bl^}7dg-NHXde+W5^^dBqKP3jNuF7I|d)mT%hn{Y= z`2qKm&GFMD^7+LO>NXD{y5su;saR+PfpKCFU1Fp;`Ol0G>Pfwg%t#5z`c~XZ_*NJYwrP9%lLp-uF(!?E1a>s2PjacAxc0!`*mW z-$daser@L`S2IQlGctZ*U2&|K{a1__{}Cg^=FL!2ynb?(kYVSGwuTxApRk) z>bOiV4K0BGvCPX~(-h>+HOjw1=K1$6Z#4ZgvTr1bzrEMFv-kh@7{dn>eR1n%*U8^1 zcDm@y)@$IO@4Q9E?!Fi{#RxsS_=dzs9TxlxqDBVIfvwNd`L_NA{OjLe*r6{BY~B6 zI{aD$XC^-*p{qg+20LdZtpr62ndHzCwhsx(^z`0-U~ig?rcV^+qX*;gbtWc)!qY71 z4IDIKyrzUuUKK=wW`%iTb)Rj`$u?eR&Y|1MPgLzl#Iq;so0!7}xdHv+kEnGpRd=5AH;uolnl^aG zJ8x?v4`)*<0VQwVEJiSF(QU#Eq0Z*PMKCs4O)%m0xH3OO_*%E%@&Xp*h1IxyGB|Ta zzNF-}V8fO{s-N{+=a5-Dx-KRG5Ga#h8DdGqp)3qcn=6>J3eVR*k7$9$4AX9r6#}!s z7WS_nW~jU1n;fVxoS7TkFy(Q$dPXJ!9-QY&YFqI6GpP2npsAH*sjl)`czOaB-B#S$A_E z<9_gh!EpIbqPrZxPg|QowYw1AbP{M2+0r;}@^s}(M+|`k3|WB$6ipObjC`WG&)~C1 zCYs@9i#76Y%GV}^n&37eL}NYOe2S|2F+^a^mNA#{Au~gO%HvLFNY(yUF{&lYSsJ1V zlBSn8UUo5#qCX@c?*S|Ta`jm`@sE&kJe7N8KgrAPADg?we6euQ3~o9nsZjTAO!LsS zu)Ekj#qRIt`@&6qnUqu0AA>AEBlZrK7!@`*d68|W*0l)c>bGfUlHO(^N^#N)f!Zn4 zN!}ai2j&ND_GB7D1-sHNgweb@PzdB(F0{GIYwo};v8)-JAdn#(B?3lOI&GF_+~fawiBY9mqE9qD<( zOqc1{sh~tozlI^ajkjjF`ZZ5&eS=PCs+yb&U$-+(;&C&LervfdA|Yp}z;}kU0%%yO zp0Jo-qE}_douaubtE?VWCPWw@A0Re@QE1iO=DqATU-AB#a2xaR2w6Ta3#VOG;g|Xd zRRv>gRU<-`3eQFkiJd3YNBLx2 zN#$ihmMjp&(J4d7oRH7McPIl|EtX+n1RGcYE7;oNJaoO-&;v5GtCDE!Sn zLy69ZW$d;ItYe4WD4b_0ui$%>{1k&ihWg7XMQ=BKI0{?7F04Fn#s7HxMATE@CqVI4 ze#KcgdgMUH_&E+V)`H>%yMw}{h_i62&j?oQ3Hkq4!odGrwD%u629|nn7$Ubz0CFXh zeEvEVgAznazWZwft{U5%L5a|OEwuYCJ+?L~Rggbu7#r0d_WUu>Oq{saumRR8Tm;jNJ6 zOYCqXkMQhPB)$x3`^=T;y{@gHUGLC;N7wQ{rQ80^@4rMhyfQ)bca*R)79DUyZ3xNH zQ?Y6NU>(bzcE@o$knf(EZpGK!?gsIQuop310g;K)O*nXNkP|WvAV=0Ya$2G` z3z+mU{yLmE#8zlejUkid5r#|#6Ol)4{1#gzM(*=#=$3R``9x845QH+*;IN7VdWCg^ zvKP}UroF%T>MQ!M(H-!|=k3ujJ{9=>ONS674YGK(1mF_mA(0Bk zHr_7)6^wfUPLF#Y@hO|2fLe=r!y0fx%PGZ@y9!aVil1n%enD`SA{QcPk*BwZR zEc@m~!^gDjliabkfj#b#(5|PvxmXe`+2{<&?4+X?1(r8Op8TRejGTPAM7JdK{Zl5b zvvDF=?IcKov_$?E@zvS?>&Nvg3;+I5WZk3}MH3>F1lsdKj-cmF|J3wPk_BE}`bwvL z{gH=ql(xYnb@z4qVEtPT-g26u=y&T|38w2Fi&*{4;k2uBlbjHZq|hbX-QIQ@teCKb zUOmDG=6ZHocAqnuKA~JVT{G?@?h7FQ{Tfy}zXT?M`|a%Nb@x~g!Zv8iS=#=GcE~XN zQW7yy@K%Je(hcdk{r_dp#r}Ws9OZxAb6lzASdFSZiJtmmXUGDG$RS3?u%!E)$2g(% zk+qqVKJ--1dU5JP;RdM=qSf}k!~j^xD4tFsShDac%MJ*i=P#3stV=i1Pbzd~)j zLvv^Hsw#JFJExLv$jUQ>ZFHihvZH_um0u_zQR8-x^>?Q!w;-MYDcq_YWAH6zJ3efB^hW1|6?|2ygQ#``7ttzfX6b7v= z8(nUmTBy%GF05iv`8x`~L#Xmk{xjdA-Q0L7pdv(6_IG4MBd@haf&SmFRsT7}l`!W| zF&J`0YlId5Xjkj`>z9ujIf;-;!YRWa!Go%mU1_uw2NaDf_7&&zHO_T$^L@PfO0XU) z9|m~)PI^X_L%YJVt~f-nk2P>q6%LDy3zinJD7f(8XFg#luq^XfmVEMfsL4P-BFQln zw|eX8i5O4=YKCh7jEoaO4WBFuGqO)_ljgDsc3-?cm_X>)I6aDON}sluk|#n>H}`P1HB8 zSeJ$G<}Q|Jk?B90AWqFjh5f@-?eI#*wC6QoAinNBZQ2;-y+aPh>I=uALK^jyw1y+W zbQ2h=VMdus=#o6DyaX#t#C;(?v zp8o~)`9HhC!-CDwGbWeL@Ai)gzf@yktq9nNz(7AS%kCrMmIt-A$wLZy^B)gT zQ9T)wIZpEK~}NEGVk@ zDl@x4fajva$krh#GV^+PWCCm3ayKU0_ruJ>G;1JRQ2b~kW#v!VO zxapR1&8X<7|B`hM+=FTTAEg zx!_5!XJ(Cim+R{NB_^VBsD6SRSz%kvuDN(ME)mMfMd)q*QkZtJmrq^yJeV;UHvtBN zJsBaUAz5BXXBVs;Fpqq2nSf9z3mPGAa&5wUo>JZHw4Tx@kAb#%pA1`#IKKs2TK~%^ zQltz#y{A744zTFBek!)p|CnH=pGfrN1hIea;1Cwt+~L7*HIGUeyh5i+EdTXg85J!pgt3Sgm2WeQH+fWN;jgzW3KGWC{(928 zGqZr@TS@$Sj8ZQP$LK8W(*L<(3n7ib`#u$FI@^kG9|*F>Vv1@Kd(d7hd9d7#*2wZd zZ<;yCjMT|qkMluuGTE?vHftmP697CEG^XSiemO-Zqjrw5O7;mVMk&y6A;xNUDLI2X zKLoTd)9_AHO4Wy!7Lm`rTxnw#lC%~k<3k>4Ab#3Kg~P5YR1Rs{o6bK;Fda%}`{Gnc zx=_|^b1?7Gfl-A3K`JI8=kEJbr@tWF|G;tYByBH#l|Or4g4~h?ImlG*Yb_A&YItgi zw);h@AZI^j+y66CY=~B{%MJGbqx|3EC7^dtIo~ATYA0oT`kyh1fiD~kFg8p2&YO*{ zL%7!%84$7zn7F%cnzJtwZ>Yzt&*e^Ts{91F$hu?Z%F3(hKkkcx-E}?>4w%Mi?~(^O z;kCp{KxC%^pzd4&aDHG$P+cyD1UDlMYW2rtJwnF5! zlwa-Z5r3gN)~oqAdD(b>G=Gbt0^#483z4x_*`xg{-;vcUEsD-L+Lk|_U+}$bo3BXA zt&`BH+v&9h#8dU)`~89qbhcP{mn$x=Wq?Ny7d*Ut^ zh7x?+>VZnRhpY;UHgl?+(jLD!ct)VvZ&5t)%za>PUZ@3{&pDlZF|HU`?3J>#lp|6f zF^*|*Pkf#fv_T*`}d{bL}-q9H1ksDLrkuR9W9(EcQ|}YOgxtb zS*qE|iJYrK2#asIU@XE!gjft8dOq8W&zY^dTPHRPdl$mCkS@>(=6BFV@{U`!DXNlUs`A@FFfj>zP6hW zcGOS0*wfHxD0rKaX4>?W0E(#l?OqalfZ7yoBFQ^E-grBX_#xk>ItO2dc%IDd}Kg2`c zV0#JCygPaL3Hb0pm18R-z{_pKxpqTXE6dNXRcJ5Tt1%!4p|gBo&E6k`?_p&N2!g#K zcvtZ;`g;0)#hb$cOPfl&IZT#B*Yr6r&S-+;$&j>B!p`l8*wQZj`5C*{RRxVdD8iNj zj!s#gBSg+%po7t}G|0p0;(#}BPQ*zA1D`EVl(kSp z=Z)U<#Takq+v?g&Ywu7mYu61L8r8RTgV5%LtCdsBJUCb;ZXbS#Ri*0& zX6_$JlGSu}Rv{WH(0yumYcLP(o=8tgORL{wiAz$P>`y-y=4o_I);9x_mPRWqW19>8 zkQCHYt-A8eN?m@u5#pzPKiHIre=8>Sgq&Y#d8Pe4xW2Ph_*Wchyrm+J-`%XQ6EOo;>F^T|svN0_5b#ZCy#Zkb#_=nxIhv&>`(nq=4 z*?xL5i%S#vuJh-X{liXCk1bdt2>V`G9e5aPG!597rIDt)_2?#xJ9U+Ye;~CJ>QBnU z&l-Quo$0=GUa4O(<2GV>jIQhJh&;{7*ZYO5Qzi)^-?CHxW!$FLBk6EOu=;E=Ey3!3 zy^7)G(qxJMBlo1o*&hw_Cy1I~RI)@X+MXQdS6Zkoq&lTA?BmxzUO25S(6@U%ov9EV zB(NT%5rr)9+ta$L@q)H&@8bGFg3^j*g1^fQZ5$`i@qw4C49T7B$>F6@ky^RU(9<$a zZ_Czm65A~%!E-jI3ZXJgcnlQZT5;Q&%)B_}In1FThaX}1XWG2Q%r}Lc)uTn=3^1$2iUUU*qKEJ}6 zdA5{evjPMdBWKMo8YkV&1H$>@rE-vX6=e6MUx?UWU6}Deh9N$d=dk+LbpBU`+xY@v zMlYgM!v)c4;ff!0-LGE zc~ImRT;MBLVL^i7K%#R*n?*S4%cFF);cqL%l5+*lIPbojjsC$SdB(=Ir{weUD>!sV z*K#ub!Z;`78$4@W?*A_K$_B2_gHdz~PJSQ_@LHEHq00O?q|EFMk|!udAJ`rBTa7=i zj&X7(1&4@}Z~b9657UR8JkLRoav9>`K<4FaO`3v;XhOPr&dO;0bIyawBjrT5QL%e% zfB3LRCj$GnQNU5 zw3$Or3jYcK7TPQ1$`!r}=8i~u!L3tU9mU@gZb)*ko~E7Cx2Agj4#23S=l@Ceriaa0 zVJDwSO4XlYVJ>FPDY$tx`s3`E}S0%Lle@q`?M;zFGoe0%u25B z5+h3MQjQtjbgSDTS3hIDQ6g!s!S79unI<=v30{~dfLkdJ8XWB;r%0k&mHE43Lj(x= z#+t{rEU#L8b$q6alN6l&>}T_ow2J%hH!|^?BxUZM;MO9f(CQKc;7(N7(yvg&oGJ78 zXHuD;#^@z7r@>Rau2}Vfx#xZhRpQmd}PHv@iNUo^i_$NRnrSQUx(c}IGgVU@> z*J77N35PKCD~A~}3$ip1UB?X43;&i(n#<}C9 zOeerq7?6|?wTz)53>`3kBBRB#a9)~2f1%~1d8pvu2C{DSN?#u9>d=T!Oaqb^^z%WY zm?jws5KI`8FkA^p*a}Bgc!vFD+YL!vajRUhc54?4hoH#8@`)?rC!mq8CmD`?A6?mz zD=YC?yP^MGeD`Urw4z>q_w;#Hr?uYsg3BN&>)gSTK(%qJ?ZHu{v69;6)2T7ltcNM~ z#gFTv$MqCSR`8FL334*|Mikv%RTVFmg;wtF4b>YWGVIg1sCDwkEvFb%ZG}qV-Th?J zRS6GZHpG|*eCLds{Ez))!D~^Dm(*iZWzMeUTRxT7-r3UerNh8Og@7p@;u@w-eZqTO zuQ+m1sJrg-t%(sc0g5&OUM8$%Mzp5E8z~su`VK_qnw<7W+kO=ZBp+pWX0V+2WgAyY zHbW{B1b901k+5WsDs{4J+E$|d`g-f;?&v5Zo@X~{q({T=Zfq`;yFT^~VC)cGa1O?d#Z^f@58k|qc2ly8`4_J+rxnX4w)yr#8di#hX zK4*hc@(#*dB{S0+_uz-Q!)i>N&KgtIJlr|Q$U|;?d*{@7_--t!d}KHjyscV`r&b2_ zhXXDvu3&>+uyvARh6}Ju9-^RRr8))0&F^_fnJiG0^<|$q8q`c|A1I?yXb6e71F125 z=N4u!lylu&g*ZHkdJ|3dqX$~cBC0z!dy$UgBCPqL_~V!rVF&|ayhayVMaPslRjK8y z;Kfd`;X8>L@!n7amY@!m$uSbp)gecvsq-5-jC`0DVmnVL5;FP8;cYnmJ@L;$yK&84_k5&^p|=& z$Y6DIcW|X6anhLBLhiVX-O#=4ei!zu&4{1)F|wdu$XB;+-qZRsNw0+~YN#2fM_@T} zmz`)~cx4Q0+hHffDXtIM%K=^A^jD`>nQeK*-I5HR0&7V4&FGfHTijp#EdLL%x<8!% zhgM_`f+jBUar<>-V{V$`Sr4_1rQCNo1j6o@ySPo`oD(4dKf`alJ;`04`CL`^ckh** z{KESb9H#!q@q+HRf2Gr*Qha{@o&P65K;LMd=De6)DbHV$#edQ0Ha#!nfBZaus{1Fv z`+8<}_47{v_@kM~yY1UR`Mb@Yg9%ddV_R!RWs^{ajo33@e~Z*v2ONFxIc(VhpdbTL zN)Zyw*x;qp>bn%127>n7z%1vy`0xWpMpQLjG9O`OO$xYD9!T7Pczl8o(} zAxn8jMF>!#&T=AQwEZxAvG^&jW{032;h>JdcOD*x4Mqiwu0Yt>9Tj955|q)*cqCv| z#Z?)F@`2jOBtu=yC-S4D1MfCjdQn7aJst)@(1Huqb|$%yg*m^<_D|yrOgdqaJ+QE1 zGsu{$6Ph@6_pMgQQxb{BZe?P}uVvt$DB66V^*(kCU#`&$9C$zc(%oWK4Xy54&WScK z&b0{dQ&dbox%m<{^VKFvKPZ@=K7rmF=j11g8K!?FJesNN5kB?R?eSGULa-+CDecqA_Xpk_jI#~5Gq;jL!B z${F?R@mfy8qMEK&)s2GeZ28{f9ky zIkxWP$+=s&Nx8YZU9)0tBPYPiIbjXc5nHSzyvM?ce0{L4tAVXjD*hOttzCHZah-r{ zIj~T~rT@~@f?_f+sf3yA)4H#!!kW(bhf~NU_U~ zbz>u}aKMW#fD}`hnpz4csr#euVBO~CF1~Y(+ygk{DQ0>hCvm^Ip+MC-%4P+dzK^19 z{VJ@K)tYQ7i=tukV;rZxtcs4-y-O2kDGSTbOx&BA2BmpQK~o0Fip5_(`|T6}^I&x< z?U=M1rJa7ryhDmmBMrZ3>oFJ$GjL`pN}i7WKFbQ;XoBZAfJ->O1%8|7y2g*<2mA{2 zA*wm0F5@m?(PR%tsM5`aS>(s4=^?r2if{=XKxop-LX@8SagG{;XQ@b7)b2VHS1#7; zA-Fo&x|Wr*9aAqAEeK|2E<;32Wl3i8G2c$QxFj7S*~;3tuhc*43S4o{>)deQo998m zkB|WIE@Hjt{O!bR-6BLaA$1iEHQ!q+DL>9+u($p7C0zbpm?NikCPSbVxz+#P8_;Ad|sQib0SJi*rm(4%;37EMQ&FzW4eUW~Z z6`6FW9MeuXWY-Rwcg<%vRvc%FNVTIiLpLydSCK@AiJZS$t8DgNKCg7#024y5D2y(S zq={a8$2Ls2ot2-c6`gnJ?8ii_PEo|DXMK^bp24y#=Qx)HaVEOZ=@jbuchC##`n3QM)0-D6+aandy3$n0Ob%=}XqN$n62yP1u! zU!Tyz3*zZCC|(SfU7vX?FqV&Qoa@|6|@Zo)!p4#V)CtUjQ5MpRv%ceV+@xY z%lkFhtL~yXD+Z0NryVm5Fh->F1TLj^7#YnOggL0T^LQZgw<1yaU@p54ns0+Bn6-N2 zJA2G6^3pYUEu4m#jB8nAS{|=IC*Xe`gc&S(bk1KBO75mF@6t&vEJs#JMieE5sKv1J zxuB`W5eF%|%+|YQRA4iinLQjAq&2uOUh8_XI^z3!#e#2{q8d3@FwIsm3U|KA6sAbt z9{K9>yP1Lvc=!1Gq``%0^tSD*%nuVY>-udp`QwY1ci35t9T7(DSyz!C zk!5ucLr&{Ryl)gzHeJ2-vPIiRLJ8Ueh0mY)rg}Ol{NP$q^O%c4@>zE8_(Nb!tb)t- z5B{&~DPJu=<{ek#U^DqY@Y!R(=IB1HBORW#-v7pK6hF3W|Mn*UUF09i{ImM*9bOA~ zg%zERezH4f^&9~LtFp?uRcv57kNfytDR+xzB|jnY z1ojQD8Z8l*c{2hk^VYhP3zQ_M$6gpx>cps}E4vn!GFFUHqHb*+`Ate{kMM2Stn?Q7A0tHPWu!mOe*XYI zd**XZVQvW?+4|1O6VqTr?le|WM{;f2%oII*Yg$gyqwi{S81iw}{Fxx~SnfZXjg$0` zF*k;vC;CX`PwaU}1tAaPDtK}!f#IsDx&SNAb4?$H z;i)k1;#t;7xQY_K+Y?O5M;SJrrEEE_0?6X+3gI+p8)O^l<(>=7JcW>zS7l}=g>_8c zKD@=|b_#iAYK>;ltfYe8e$K|^bTM%?H)b~`xsLGFhud?+&U#t}lZR3>yC9p8oV82? zVNja4lm4lTtlA@Mt0_1g9RoA-#6bTBq0*^*+0w){SA%^s$xfzX>vN~3mf(jcor9$d z895Jt`({+h)6#?i9G^M1$^`c^sxHBA%Q&05=sW}yamHx3#iiibJS^XJuNJ#P-U~}*f_c0>Erh2~9a+~AC=be_ z3Z{^Lo@CcLUZUYGl)*bS@`6UEs8zXeDjQCm*?stY1#?pd6QWyoWBSu17eo$uTl(#)%O}yH08P_19dOg|}ueC%86~ zIB`L&=MYS;a5*+MY&I&9{H__79x?$F?y9K@w>yV}zyPiKfs{0tsf%41_+&ZB+cM0p zGR1{zOmNxqyrUauSrDDi7Go(E8sqBYie9b6_lZr#cI>)_67l?YqwJ)8O*KVO2mwFf z>mr#kSa&jrLEzm)ra>xNL`Lt_s5z6VcJt&(lk&8)5EuHa*Pi5 z8MLRqcaTN7O_mCV%;g_ky;m>LPf;q7&0`9mV=>8HEcY1D1a5?S&L@q^Cnd#|qH>B4 zKj)h4^IO+{Q)s@>`vJV*1)E^q^{LWbY}~7nSm@7m2fqGL?3|`889hkX15~Xkihp0^ z#Smp@-K>?IkycMvoV?ZHFu2H7Aun66o?>20zCgOZHoo^x99eoOQ!EOln|aSJ)Jfqw z?f8+gD^sKQ+4`P^ZhB$oKryx=*cP4ZyCUrR;8L5R;FXVYtwHIJI(P43XbXZMtuO^S zJLL@3f&5}!A+7g+4_>T~iY`4zL7XY_*+4WA&hQ|(_zONH zXuKM&7oT_^u~5TnrMk4a1$_BJS2;U@3MR+<@IB#%diZq$@=%F7YnoWJy(R54lwoBR z#@Nw1f5!nH2Yv?o;jtpCd4gJ9X=Fm@3={X+hnmvT`?#|#0`+e)1@fk|@;G>E(_d?X zmow5qaI7!sC1py*x;o~YCGNQDMM6$-A98~>-*nb8$7@ab_+m5#R!z6@G8_mMYFpdq z)YRrO197+$_+tC|1(AOXJ&%U@dIYXbtC<2U{VHd$Ow0dFj8R4cI&1HTRL=aF5J3F; zKm2DrZx$<47*%rs6{1de!VT&*lRq(2@vg2V@O_Jp^xIWX!;n{uHhS0djmZ=Z*S+CW=rpRyw*n(?Tw!R z_UtOY>eU8Hu6%lyXy=#yLBTnqN6c+nD)p4?*ZF1l>tG?^cX3_=h4WFz1jr&3kpsS{ ze!Z6-W}q|fS`VRmfdA2x3rUej?@t-%S;C!P+D2^Fx@Yi$JJ3 z@SJ4L5SuF6P0^ouYX+};8Bk;8cE(!{h~ZOf9}#Kjt48#}6jTQ43ndr~P?!KH{WN-R zi5UpnOGR}!CDi}p+0@ajwK9vmMFumUECT>lZUP2MpnmtO@XF)aMB7(CfvQA|*Lp6jZ#ScM_Hi2x#S zo_Rzl!MZrH50^ag`m93{Jh}?( z3ONnEm`p68jSLCU#n>T12aun7_4P+d6G8+tYtmv4d5=M345jiUOBF?^xV1)B_%@c!pBu{c~zn6`C*tsPqERA^?Vdtu73iI zpVy08FLDp`)jtg>Qff+p0h8-9mcY7!Ww7XZ2%JcbT;^dJeVyu`DB->ra3dUXyC`)ZU2wMmBe8>G#{lL|yx_@`Z%24G4mP;f_4!Y(4Kx zCWWGYwz=)mcZsDaFB9_I$c)J!(5Bqqa-XUTIfT4nk5=)^mPZcl*~J5boEUEA7}j-q zx+*W_)U>eg;oY0>>}vKHv)8jvt4@bb?F*#P!6`JVnXbt}vX~e(jA(wm5c;Qh(QCI^cBg_Gub+%K{=ME#7p75 zOD-e3;S^rj%8QH_utUEo2pqbn0*x!k3SH8?aicul=tkC;6(5_R*r8PYM1S#G_EW0w zr<r^NbMwGLr9H4wQk(V*(Zeg4zsczE%|E*G4PV| zTz|2EvK{x*MheN~i#N-Za^xFYITt^7Cb;tilUxWzTr(I39_MsC-1Uy4TnXSK$0~LC z=+Z3M@HacdIMJ#X>#TKwz z=#95`g3NB%vi9M|2^TTrPfs?ty(}1yUR5zX+bds&dCh5iTP5wD4n2mtZIw(D5obQ- zI8J2ONMF7`erZo1-}(A}>z5V8HSx!0GTE)Ryq=;dez-dlbO z=&CTvFJ}a!s1DZWy$1KWoEEWa9A5HMyKx}NaXm)_8Lv^M=IHnPSx7dJjx6HYb&DCX zcOZ6J{NCv5`%(J-FXu2c{4?ENSQ{Y)(=WfKi=zlQTf}#pN`Y{$k*}(H2|JfkCUh@; zpi<+ej4z(%1v5O1c=x}I{OYSy|K#X=8TAvVmPhR}>nF~?K(`-gA8|Czo1>o0bogn1 zoH#}^-JN+@EHF>~dlq@G>uI`y#^Q(j>D@0GZBRg@m$(>0fCR-T?)O*#@GXeQpoHVv z%(yC8Gqi$`(&w)k?K^s;&y>b(597GNy@eGV6q)WUdrjxb3$+i(V@7zm_c!*(GrV$W zd|32T!Zo;1kk{7(&TB__P->7e!^#7LK1_CC&L2~OPHxpOM<#FDRCf%&ps#bb2D|!u zWoYvaqmvYc3e86SMw5oezH#V5ljMb(j z#EXR9JX?hzkh$%`lA|9|)K&GEv z6-QB)ZF*|5EOKQ0p41Zus^+P{1LA%xe-o5_P{*>wt)vgBphmho#Du0k|Zk;{H8ros=Gl=t|F=7wQnO4i|)zKXDlJ9Ym> z-U}}Ith2NZ`J5L69W%Yf*;Wl!Z7)Wi8hc%qLZ+NNM-Qf|)tZZSHsS7vwR$Nk_O=OJ zU0%wxWN10j@b@c3Z!@&p>ar^qjtynaO3RU=$?9G)5cHG$I+fPfC~I6_oGMcU^6HSM zlVyU2zs82Mta1pO!(gYDJt3S%~GMA2o8ecsI17C(+xqS(9u z=vWgnh=}3gg1l#!`RsK5*mGA<$NKFdca1$>b;uvnYDo=({)a# zZIy?U$J6k%f3UmQn;4>kNB&Bg>AG)p^vXtH+IA>$k*+S%LVt~swM_R?LC)1-#O9>M zV6LB;t6yuAyGjK;ILNLHd+mb6s5n)~Z`$-Tv3*%nR}C02@KWdrI0!U2BOR$%2?(Q1 zPS0d>0_CjOQUwboAmx-HClS}^32kDqXWZF5nh7OxQLGY4kgp}M^Cpx0WjUGOP;R1JLeS#2wR;=I1k zSkV>{08Sv&WnPEOo;_^gQm#}PHk{yl4B5;MG%)cqyMynj!u1+uL};rA4ka$AG-s|W zBmPo2hQ15a%bO~8^snyw2Bwp&shMmoLu}TA^Dhtt1b0H<;7}ZTFNH1_Ll4f%tVquR zrGzr&d^1~f_^UbwxmWCu{DZneg8K)qF|ae+AUdnJ+o~9jZa3jgU>NS!MgNj zBq-WA3zk!lS#<2@ldG?=-mRl*SWdv~LI&{=$%4hH&yrtdgC_HmSw)WDx;{uR98czf zr5THAJ8M(R!FFLX#^b?7NOqTp1lKqi@$%B949|%Q?NsXp9Oku3RR;`CB{(_mRhQ#b zapv4sUY#Qu<1pcBd!tV0hsp7pTPPp;u(mYUf!v_581mlvMx`~_6d63>PQV7ZsK9Pn z70R;UppU+>_W_zjF|R6ZtU%tGAhg09X3yJS+}rw#5$0x$I%ZlOC%H^iW<^JdnCJP> z-2J+<_Qeh{=Lu`J7-1tPg+7FPFOO}^G| zY#|T`&W;3Ymg+M%gWSqa8)MMoLmNy9d6fIYo^siQ6?H*}UI`zgNhx>}75%!DKqUx5 z%npf9LhuBU9TXX0w6o0@5f~2i^u}5m!Y>d4fLuouL_yJQNT~45 zfF02KM)z=t!|+0jEZQj5_=VP7gIz7*D7)M6TYUr=3s#9e{G;HIJRdpCjwkV$Wi4A> z&DhhGyaREeheJfL?cx)lZEt&BtQ`5IM0)oYc<0d|@2&SeB zaMtIj9DD_dx;E^N;}MxMG0#<*CzBD~KOoqpoH^(&O~RWqN@Nrl{* z2q!JiBjzv+Q|_sfLWsKQQ#L*Lfcq^6cFFW3pGN*T#u5K!vr*06B}{yPW-ZAV(jUbE zgkJRjHOz24>_5T~AKCoq=({2ociwq1GkI!>ItF0#b=qC)33?t~jXb@yc|4x<1Dp#u z-fCk|Kq}fI=}#Pt8}EDjlFRl_6C_uht?|2|C4XjD>}`s0d;hifwx8QWhZE{SptT7t z|B2(?+gE2C@a}a@QmhYHhUpaN7Kn!!>6kNBE~HHwRaW4Y{v^FA(&NK@tMm`gw2Oon zn&a=UdAjt=ypN#kk%XU8Cyncu4BkghdwX=>R3nKd9TlIxHYo|g$+;CVcXr*by1rkn z3tZ}Wy+W~Iyf{5AyVVn;b@CmWq9==$?Y+iqCp;0KleV4W_&8DkEJB-ZgS~XDciN5a zj$!OjtJH=Rz<2s-2Wv@p9jT{NjSXZCHPmU@-t5IXUNz`92$(CTs+2&OcT=h%fp!^x zw6=azf7UgiEQg|%2Ca!G_%gzE%FYFs*bp^G{pz~h)VVI+V6AaeFxDQ7fLxP}1bY$# zu^C2J*7k6ym?Xybye%+6OsPy+U28@Y*~Il!)k(t_u2R}*#KGtlKc^$Z8f=Y}(B1zs z-&?CW<5cz)2%|S?V3bCxO>x4adtpV`r>d&rr?9R^2O?}lh^8Sx$0UEI?Y$A^+grqQxKugx^3{1(|Auy zY{4>#t!VO@GDSicOZv5`QV$C`wTh+acju;x8k3q6(DW;$B4p$OAIwG!btJI|2F$#H z=Gq?pyy(pc&#Gn>2mXnW^1JNPH6UCjnm*My-kG%OO5%0Pt08!oFXT07T9}we zGRe+sAIr7q8R_sxGIzSPUx*5y1A8jVB6M=^kfBDvwpW(4s(n2=yf*20ccx)yyCj5i z+0-;QrDm+biHfH5jatQ+vEk%yQZtK{5zOkQT8R}?(HB}#LbIu-QGVeEpwc#LsG3KU z^%e@T7WZ8Y!iKDWin9(4!lxaP4qy6UWeuNas77)Sim?kll}~oudgPGrSVDh2ndu8& zxqIYvli7mX0}4CO46Ui|$@&Q{clu3w?OC=(Z;r2VE#MWdqR%qd#QCU)8nL~@>|!a9 zS^q5ux99$HWyO4&14q-N5c3kF%JBQ z*8|ojHJ)>#JpopqY6qo6AOTQ!c6QA}-&gT@VPx-)p^$59cX4}ow5f+SpD$yydDq+0 zTCEO!QOHLL+UyZRR^*{thu^Yik_>Oa83tF_#evX}bGWt6CvPio;M@Io=HMtsxx+AN zf(p@2H}q6=){WgoK_9jv(gGhv;M*V)S7>XL8{hBAy7o-zW&;NWwVwerp~=BE#hs}l zmsPZpv_Of3+A1l_v`V!Qu25w#psRzY>R!ro_99`pefEK-Y$}UO z8Mc|q$6J?If~>m^kY2OC62IY7To%ZqF;_n%eAVVvtJCPr?jN%?9RoQ$ zrQ;pn(R|Msd&A$PNdeLLsh(QGEawwyk~u3S-S!E}Pz3}IRlMEH8ovJMt|rrR0e9pc z2mfY&x`IHf?iU4W0#6&si%{R5*Ol23;@^#qu+Z2)t z=Fr&+0by3J8*0hyL0l!YD2PTKlJOdtUa6`A;3mCd=0?I@c~wJhzP>G1w=J@sc2o#u zX5)__(B>-Wjt*{d8z0)`7{!v3CLlmSI;6OTSQ5BKN8WL|7c)AkP^Q_V!a*|H`D0~f z?d}xayzGxCExs-7?}aA)Kh~D-wmm8pS2Y|k-SGDjUWyypk3RU7+DqAd$xu}zQk$($ zSn+#5b4eJ8>lfw@8Y#p8A#!}b`s3rnQ_ml1&Te9XdNz#g>Azj3WWMi6>1hY~65|Jc zXG4Y?+%~o3iw@OkpS3@THV%lNnIl|hNd8^_fvKg7(Ryrd3Ly09pF+P$yet-( z4pg>bWJvv8hp;Kcq6$N^gexxf1Q28WTa4P460uauwb7S9-VXCp`Ta`8__d=`MqfZ~CE*a>Ag)&KwFAGy2&}e+C zDrD9OW}Fv?)?vSO$K3mg^CevhtN8GwPSIpGmi^TEqC5A1QM;<7XY5kKBy1>0kr`Wd zs0cZ!lk){wq_hN-3X`@6EuF=H2*QHju@%uD75^@jWBXks5L-?8J-=)-yjRhpL*qwP z=}qZqmop{B61z}&h{aI*C%}D7jj{3^KiGEU(EjiQ?Pcd!>AFp7;LH7l{#6bYB=&hMkk%^rA|~?X1>dhtqWoh;h;KsYVMG&(r{)O6{)E9$tIZ+R!#^B0qa`>kw~eQO%W!a|^4M9UsAx zL_XWpoA_L2d`e1M#<9TdtJV?6oK!c67F&%+GW7nqp=!&3i%7*CT{@%cJh#2LL8Lp+ zfwK#mqoHbg6rqJey2iH67w~pYl|dbaCWkmXNn-ZxvM&6lN9$SNl`36VI-Y4>8<`bq zevQP4P3;BdJ1Ijm>~i*JeIvqXHQAuIvN*$*Pxj-Oqz)ahrHclM7TRyMl#exMzK;3$ zCwVEn&@)s?LoZHGUa`pZ!}jr(!Lpuc$M~EZUqn?6?}(n1QHafd=9EQ5xnlk8_2`Tk z%5B8?D8IMs*?qHE?Db7Ax3oV1Vmrfu7#mFyMGHn>EJN!72@o)opJdGobRy007|>az zRR2*VP!9xO+qwS4S#}H@>TS_m>z9-vw=&wQw)=_Go^h3#;5z_h(8qlsS-Aum6#2Jr z=fClW9$nHkhBDkfhI|R%P5%MeUQ0WOMF*wa#{;Wg)11&oe$VlFi4yzXZF9uan{cd{ zqu)6NF7IqjE{3bT-_^Zm@k}h1>Wb$xG&4Hm;3e!ZpKw&YI)HF=hw7R*{pC;$u71gAZj`g_DJEt z9k3LN77YGKGA&rRr^(RJ+2lLcxTV zD<{zspY~$C7pp~0#tQfOfST`I;`|Emw|lb8oqrh-np$UjN!Vs1!XdIZcqc@OLzA@} zc*ZD+EzQCeqX(C0Ei~!fsA6%mt0REQ7Y+BcxfP@5=WK-Nw0`1f=6+7AE~uOPesLCA zwj<(zCVDedbAQdK;8iW^v!9U@PCKQ{qiqlZaKAQER=?J z8~NcUBTWO}#@Bv~J*v?BG(NbnLe$J;DCR4 z!A%RT9NP_V(^;3SAt)GHB+bP2#DcS>(perHAeJ+z zfB6VC(Dq#O{vI3}UE`C0HTPdiSAQ0#Wl&XDeGko6X6IZ2>x3ndXP+UbeMkO2PpoP0+FLR7E7qVUZF#4ZJ zu5wFh*?aMyv|4;~-2bjZ{MT7LpN4;fkmmnski<^JQWNq2S;5?Y>mKnoS_)74-jy@x z$M;XH*}Hz`{vv-`@jq1dmPk9b@%wcdH2!Sm!_)3_H#&k;XTITpMR&i)u$_f-v&9*q z0%pnV5o-ymKO6S1>-X!zF^r%{@5|#Jr2p@(4A+^0A0Oz|*JH+^`D|Oy#2CSW6Ww4tZtNw2Iv)6$ zqiMP-l-uCUPgQjS4GkgCSDwDi>gi2O!`|nDR_LXo7h>@!xZS#x6+<)j=pQnhPV?j* zzDTsXYX|bg6gK9T+m~>xpsArFn5i>T!2A=(VZ-3Kqi^h3 zK}A~_>!^D*_G_}Bk>V!~=OI_*)XgO)&v1K^m@)v}Lf2*|&Yk_eJEIRnxuRqxfbDTP zeOBb~W3I$V=*hd(c!UF5PGfqv{j3gm2rQ0s`M9QzEOFL=y>E^*=6P(KQGk^lq=FNw zwER3_FHXWgyKM|{X-dK2=!#))Aa6O89Big|b&A%36^c*3lGUDhZ&Vx09;eI+Cym6` zH<>gX+QrD&6%JFcSHW03T;ai?MnueJxL@mwcIh`t(z=v$begZ+`CaXH&ipp~$eVIr zT&lVl^{%<$IkTO3EnZFX`g1n3B~goUX%|oMhLweb@Z6ucUod5@h{{a$#N)la-A zaB8|^XYd}4yUMF`=?Tn~B8%p6n%87{iLI0!>0=2@?|Hn}3a&~uuE)byoY{NwfF%w7 zZeoOYq7?TwfW;H*`YnLmYV9X zwQ+*LB8l;$H^3S2hPPo4{JCpBEV-->Y{q(LcccO^NFkRcNI{g#Oi6u&H9B$rf`u(Q zIXHFm)yf#tx*t!;={@!LQSMz4%kSa2BOU|Dpu!>DXZ3A$P{)B z7*FQi1S_Oh4EEDFn!%ub?x7**w|U7fz2di77^XrN>t~Z81}!Q>R`5=pWY#d8Jf%-@ z*}m&JQcs{_j7=pCY6d7*Ju(i|>=111sQ|0sx0`&c1_Q|Y3WfNT)?~JoCts-W2jv`J zdQRP6nWsIdS+zEqdZ~)!D5^yn^@IASK`ZmYs}g!3$;)6keNFLsG@{nNA$Fy?@G%8LpKKajemK&`a_Xr+f)pF>Z=#A9Q5Kv*$l z)U3(2y~%FN_Wfgn0+kBG9#09uFAyjeBm$lE^Ak>dCvRgBxHJ89@8M=vMg7yK?xV~s zHd%=>`hEEyi+??-o5WkA+fBM1YOV0 zJ0YfD!faz2teYUVdd6qxQtDHo>4AnVH_g(3m zESN~*O^A1ztpxB5vWN#JtDIY`d7b+HSpUpcTYpeozN>Iw0l;8)Bm0SSz!*o92|4AD zo4Wh0+TVvx^#ngGXmaFPnn&}?`bUp=#!#1t&&T3ljIe>DrTp8--8!@&k`_V>wNj>k z(bDyRyzFw$D@mA$@l5H#VwrjhW;@F<>2*q+yvMV=P(v^TM>#I_9P?O!Q$dkHw7F#j zd9IBwo0OU8_x&>Zz?yo*!TdsbcWZei_xgDRvg(bCrdss=Cik{;0j6Cu;YkcmV)F3+!eVf!XQ zy5%Da0-{4#{#s&P9>cKBO%{$Ft(L)Bl(H{PYnf=Qi=qo7tQwpwx`8sB68$l2U;998 z!YS33+nCVS6%NdeDr%v8pwNU38`gp+70L4gVwz=56vgwllZU?8XiOM%tC(&qFUyS; zwBi%~e)fFWYX6cWipxb~hiid1(${H_ML*S;05$-Hd?ido%Eh{gDAes4FGIiEf8i*R z&@uec9Q8P#kH6Vtu7FCZlpU_9PevR+=KfGwUr~!J%LXT>q|A#C)@Q6#YyYNz3Rcy3>QHGai-@frm7~z`O!^2!O>opZR1THiKoXVoLh)4cx_}3St?vFb(0u;oT#2+_OE>r?8xy-8D@6Le2qinL zxyAZ8M!x>>V2OGIBtAVmt=C*Xxpno?O?D2cJOxgx(JHQdgYgH0i$8IU^QP?%ygel| zSq%8+1hqhLl+|Qh%QxZ4f;@ zVq-$25jAKJQD!?xR?cSe0-u0mKTXPwPfz=IxY=#huitZV5kBFmXWvcHtT0f z>4Y=|7)=jtO>Sj!hpFC{N6Li2NqaytcaxjY-1Y_@vBM-!uufxJ?Dg~Jk74iNtrLX@uq&brlWA{#BRGpRh6YfLdP-W zhS>X}_VIWl=Aa{Q8=PyXXy>gbhLuC*`l$jfPr3ZHcLcY6>>P%Mi#2B^l1fzhkH@7o zpvv@@8##?TBX<-Je4IYdv~Ihgef9l1Qr6e1>)>Ki7FVa$RjE#u-?R--y}Fz*<@7_L zhpueUY)vq4N+@zikM^UF;5>`?I~hK-qWCCL;)Fk`;;>2Zt?Y+Sa`eidRB_ zr8Vd@AH1}5o1Txn4&oJ3EPosSbs#&MzS7~fx)Hi^7mJ^be$I_as%N7`W z>rAkxR_w?j>NORMRh0)o^)S_w6O|W5Xcbp{e4--OK6SE^#I7vuVJge2)5Y~FS1aoD z!m_D91RmVezc=k~aZfC+J=Pnws+5~`T0=MT^ibsw*!M!ue@$?a^cW4OtY4tcz%ZgoP9fb!hd-(ns)yZr5qfd+B_IJkZ$R3A&qH#EcZIx&WNnSXK`j|Hn7b0yDaoAu>u?^tQRCPRTtd?KfZ=)y0iM^ z0@N$H3yOjr%lBq3*QgW*v)2Gd7hs_Ta7{HVDweO!6TPid-G(!qj<~-5Bm{9X-Vwi z+hxvG&YA10I`~ZMmF%JCFO_m$%zPs=Brkpcp^%a7s{4ApNMK11U^_Knff6cy4M;~; z2iRiN4;g6y>j8@iUvEMFx*m>z^+4-^JTI|sfZGOGcj5C^0nUv?;3WX$o_r`N=WeoN5C@er=%%>-bFK0OdY>u4G)pM7?<%XBWk1Y*E6ml#8cseZA+sO-{pD2ZQ=#*+6-zwC#{N$BIt^@{=ykx&-K|n=U9a(BwI;_=?51@%w z?e9xI_TBWjq^tDfm&iB0OPEk=jHFn$%PrJCi55G6ox;C;dYWwsKinD zLKC@ZKxtr%?(nmtdiAl=|Ewu70$`fQ`L79aD?IpzM7ws^sEPkFw0B=^Xt!AU8Z(ff zaDhdAntHKLe5rLsU6o~i`d?-(7QQ+jrCqc$x@~9%oTI-BmV zLXyUR>mLS6{omF*_`4zhyY>E0H8}8pabYG-qP=)C7R_W*kS*yP!W>krbO}CxQt{AL zD8C)~;)s-@k1*-&Vy!&dk~_yRSLm2B%GnzB z6ak-(8_V)6PZc|?kL&kj_Z14ggcx8Yl}lqzi{mv*hR3Q&Z@8Z2hGMg2gKjW)XMNVO z5dDc$A$e{RTdH}ekn>~gK;JI_8nC;c!OZUY@Ep=qe)ZAWK!b{rYFexAm8j=!Wz$oG zX?!4o1{7G3(P`F2vG)rOYd-g*gkM&8Of+cD5559$U zcfn30ybyk*H?J>IHh!F5~x8#kHQ z;PeSyVZxr3mwsD97Y}n~8$Gr$VvloX+Z?y-g*Fq!xE?PIp|#-nfuLmGh)`=vm7(%{ zuugJxQW+i|&a20(v;|7fgBh;>f-t9bi?z63%WJvAH}Tl4No}wt)_KvPg2;?%4=Qsb zC^}wbjsBiQqp5&e&RmQR+jV`IeHm<{H{GFvrlI&A_Um?D;w(_H#*d*laG!t3enK3)_y!gVnwt(!`l2zY_jTS0EX*NJ72soS1c z$mYuCA}ad^!MfJb**qVol`~%0O$NIRD<1d`^Av595Z7XmQf|w{^XBHuzB_oS*Wf&M(4-c5o`@|6iJ=}J@#KN1woay6o~Bo=fNKXBZCgM@>Ws7;4jAy= zxO!*5bn|+f@<@Bc%|t!{_J-{CHkLO$fhODLdL6(`3vY_T#6A=j5>zoazqpT8E@tR%A4C=*O+Zh?`_htp zM3?G<(4t|4(JN;UY1irjcrqjRdCns3%NcNvTZ;srm_ccazFuaanL3&iiY48kU;_R% z_0e12t~n2qMva!+w`rk01_&bjyv*2enAIzAxO{Lk>R$VUBzVRY49fk-Vs!o7zGYOo zH=HuP^z;8i(^Wz2(CbHV+>!draD@-l>o1#f{rNh71dGK7Gtk;-h)4TYIp*#?ocY5t zaEfO~uD2)SeC|i}!OM?I2e5~!wLfu8H-3Z>3!S#x@A~n=zVS*bmvC}S)b*ou{q(MC z6P){BvMBYDZ~7W|rNTxEfJ5=;Zf!3UggYj_Wzwx&4GO)v;E8Ibv!;1JZSmktEBG_ES?enAq<4yC5P%a;d0WiqL?4n@!hkdI;p$8tm zM+=*WUON3TkQR5Lr&8|d9;*!(#ZWyEJr`>%`8IBr6}#|Wl%hGfDu7QpwV7+oxq(5` zyx_1jiZ5fq$}dw6>|DMMO(F|=hl`LO`GwZfdcc!qtB9}Qpn%lw!<^?h&L1^}(12P# zT@aP)qf{Yk%o$&Ba16fo(sAOz8IM{MN6kmn@);~t=ac63i+7;c6GRje-P7#gL&W^` zImukgCfb8#xalNjM*z}`#RwQYxv`X3@-_mkJAwR|1%c*crRXJsafkUnz18i<{KP51 zTJHl>Mk=$qhW>(Y4{U6?>RIJV`q~Ur6+j^(8bs_6cE^$uMv%8`xITf1_$}etM6aTd zJqEY2BYj1bwvLLqFDA93Nt!IwKz11473WCcxi5I+TKLfYvT#xACMPEF3)2=F-NdPO znAOohYwst<>q1)JsK8nU%{N~|v zdmAAMyCxrR38=M>Ri7UVlsx;RR1&+y9Su?G%LUGIg+HDQ7~TCebo(Vh4PrVQQ{4Uw z@&+EmAj`j2kkW}LWAEQDXaiikb85(W-U*%H=%Sb*u(EuCfvmpVzD*P>B`y^QsE3%b zSa7l}T*%6y5;9wRnPXnieTr1fLk;gxcvHJ_8gg^ufyr+rZQOg7^%_6wrSCkTBeI;I zJPR>B00-U!nm8!^kLZ_^P8l&bo0(eA7_@eS6RYaJq0uS?)5nG!&bO7>Zy=ENBJXhd zr^*gxQJRE=*WDQ@>G7;md5*oPgs4rj2%XPl5XRRmV zFP}cf3p7j?+xak-UI^Q%;ZWTCemZMz4kvu{B0?SqDOiJl&7S@Cuq>!s0kiy;12QV; z$qB-2%qEl@9_Ked4#gQw_#+$qm80jnFJ~MmD%BSYgc6W;$->c|;nPV5_9YbVG|W}+73Y_Lh`&@0}C+drN>dKJAwoR~O48WlU3SjJK%*!@8}GWs+hE^AQD!#y9u zg=V|St_KmXtW3;tWdQR2eX$lwvfNaDk);(Xyhj`yf!$1H^Mpvf)HF}Ng9+@-sUfOF zM!}@#TnG@pvXoygQ*>-vlz;Bk5Mr9HP2carDxxg3a^jeq{9e%jn3IhGZ}o|<{283i z^^@Z-Fe3oxNHO3(v3|yGwWop3K;U>`;@p{Ep`$)VJJ2{~=lU<8|qIrLr_5nCXw6e$Z z0_kMsjCW|U_Fe>g^W)3oU!0?<}1@tVf8Q=K(z5P0J2gLQhQ0+vBt8n39ff7JKNF zudx#z7motJt`i`3p5t+a$OV5O+s=MO#U_hQ37-QhWBxIxsG|ZO zw3k5C)B)~^7eqiy_@bkJKE`$cUDb5D)K`ZFOLz;3jVwP~3j7ARly}4f^_-QAe46Fv z#?muZE1z8yR?>{k5CMMc%wA{4yMCgpZFDZkFX^qneAoa7ZI?1XP)z;hm)o5PIP%N+y&;tF zSKV8Hw_48ka_b-Jo{C%u{9p%K>h&-F_R3w)8~p>Epn>7*p$X&2hnm*Ubr}k|X0(Z-?a<><|%7;W)n& zR`9D`i5p&&jf}6aMRgYK4Di#XgF5+i5KUVjG^<4T^4hqb;Vp}?cs@!6zq%snaaoeO zrL-G$UO2X!0#{ zfl^^hLat!0me}vLdTX_~$!!l&u~v2A3!?SUmVqT5c641ne^5OteTt1!@@ABDlq~sk z?LR>FVI@qiu%-JX?l0~63qo=46TTN*m7IFJ@oi1)hW)wzEjA}i$va~E;a{5N8l4}# zVLVf~)cka~VKao9BL2O;>uc`Qdhe5mw@;4iAx+Op@0DiYq}X3}KaBm|Dvdwa7b%=k z?_z(6-4x4dal;PQ?5|ns(CI7F4c8r0ebsDAH9t(ZBE~R*wc?e&b9`4?SDasD8>cQF z7lPyRasI@)q3U#r_LrJnG}ui{Goe?SEM%{A0Jnjv3|TRk!&?xnRED@1|SY0cju#mVweD;cw#Orep{;;zUoqJjLy+C3!V(qhBzNWfO#5VVzJ)wK>-}D68?>mPze{6Bh zx988!(!SLAyHEVYny60TXL5h8m-KR;-VpuYy2*aqAph4*w&usAzPWp#LB?%US|xyAfY^-IZa=iL7dK6bI=`$2XN%-x ziQWDykzm)d8tVUA+`>@$tGKe)?!PJi>vW*_#cr&p3-H&4(ET^v$Nv|@W-*Lcwm*Fe zsJOVcG-c)R=1%tvqN<4w$Y(n8`Mmy-QH(IJ@{M*mclJ|l^GP*Hy$NLLL0emqWI?@Z zZw*QsItL<(m;UH-jUI=U2AJW}os>H4I!k*iZxepyJMaHg>9la6J-pDC)C)81g;v2* zlpAL!6ifg}!7^Np6MXJBJ82Y?kz$>_OWAr1%q!P=d6?a;J#U?-YPD@}E2yZcLlaa~ z(I`R8Jp87L%v1?m36R$InGrWkxYkz*FYCF3Mos&+FPalPkC|&NmPdA&2+ZkfgDopy zG@ET9zIyD_YhQ7QZ&R(Nr8$B-wU^4=O0N1=E(Pl@0&$Bq8}1o9*0UOhSejkCW1_S< za=V70cHm5DI#AEZGwH`ib~3&P567zD>E2ez%H}Q=6y7{n#hi$*2k$%R}0d>nkgACZ^eq9G#PONIZAF{pR-p;w#4 zJIvIc_l%SN0H&IYM=#gU^P@qEC9+<)rXXoM_Ld9d%(qap(u)UP)dX`^L0mQS*CT9U z+mCXSk^GpkXWya8Rj^3YdpWdI;X9~#%#T45KO_I8)dciVX|<*%h}}x22>Ai$ny@HA zFK$Ni3|VU@GiI^3K~r^qX*qAfP`>dz*BOW=bkW{q-Yi;z-k*orfp48V5>?R0Nb^s( z`pOh2%EWvqtl1f`u}xK2JNvjPGoSFJ*i9QsR2O^*g${?{mcYTW?%76P6|xDttUEoE zgH!r*>PzeO+p=etFdL;4cQUh(m7u038QyX`-y$}o8ihr_vJL{`@rEvSky$Gy%jmYB zmt7(6X-j1XA8$U|ZXy<686PZ-bE61{XB@;bFeoedmd{KVKtg16KU19aoQm^JHl0Xy zrMLj*a%7b*P#xWm*CM?a_NC65TmT*b=f;ymk0u2#TnP+4K;(4^&&KVBux()+z=N2QYQ`MJBfS`Yvo?B7R? z!=?y-;%I%(Prj2b2Gk3^cIT?LTb%I5iufF>EZiT@=sk3zx9;8{SRHg^w{nHhpyjx zexT2Op_h5Ly_q2bM1K_grRV>F1p;1=w0QH~S^X#RDtR&Hm7iKG;lZws>?sz)mm3W7 z04Tb=WQAM9lWmuADC_R9i(}}p?A=a%T7<~?2UN|azRnq?^Q3Wl0NC?Fd2KyX1lm5_ zdmMn&d4^1^$ZiyF0^AxAUt`T~e{=u!DQkw~DT+05RMzWPUZ=?ZP}@KUBn)ub!9REu zt$ZjXwB|!YC@fO!K8fVrD1=BTPYPIwigyO^XOyt*@wj)P4vb&2>v>Hqps(KdpP zkJx5Q{&>IU!OML_8C*a({iZfNyPrj;&qF9)yx^6huUl{U-R9>HH}Zet_`09F`i@>B zPd9n=i``NJcyD5%##Q^SnqJ+L4Q4o5vmYqtr(=v0Qcx2cMV}^4`}#!SH|ngrhd9@LC(2yDFUwX33*?co(G1nVh zC8|&}7x&xiz#j0qV|@d7sgZn6%;~+1xPCA8E*owL8sYw_HIuGu+?bU3=`i}#7 z=B3L3ldss{TMFBlk>?ZKMFtZy#sZPYIdXKpIt9?(t35kCkgS$E5r>8dW4WT$cKsbO z8*6vu`?mJ&Wj2v z;Mzz|@-sxtX>G=9EfJ*LGP4|#11GlRed~E5eE%%n#u>biq*W_H28(MUo0TNkq@m%o zyskkBiQO{eAndX!ue)7S950hN;%Rpqf=F&lr(-#@^!%-N$sT`?QN+k1~_MOOKvM zIh@~2)>nJD*Su5Lj6ft)dFvDy5?mlVlj3K^-|MRri#K-3RZf3T9_KVly7-^y7=}*@ zWABe6*6je~nB?o^iQV^vYBJY{y8X=2LD(ef`O=w%1oFDUW5S`e;`X zl9h5^l9zwqP5hIqq54 zW7jN(f6%&gLeu8EiqdiK z%Dv5@k9k@$Vs7|9w%uI22gh}jqCBh_&sv*fLd(~%Kf?~9N@dBv;+bc1BZhkVRg69* zt}fX-%lA;Im&&e6a`K$WUw7lL+cNXl{mD~36>D+Bg=aW_ilXn4E?4<9K>#R@8`2?3 zE`rxd2l^&MlFaBn1XinOcEijwPaxAjx;MN2U);S1P*hvjHi)7URFtSh3Ee=0~85(GEYNDVbNKQ?XEKxF&71TTJz3+YB`+fC)Q}b8NOw~*k zUF@^Z+3T!z!ruF=wf6Ivk!VS46;1V>eS{WFJv^ze)CHQQWvi=T~Fr^ zTpE9bYL``soYc3S%|UQ-t9Uj|rMgYL3{%bbxDCz^z&mpN{0ofwnHIKTQvmk|E zd$LBcH>lTYsn5u9UnIR-i16_T*xnbq1?k9soTgU%K&v9n zm*>@61&uY&U!J@SF=T%BEB(Yn2PLV|>z1(`@fxECaz1`SshXbqVQMaJa&Ul_r~Wtt z&KM0*l|`oSeGSMPUkow#n~VEtU8!KWo5-dDy>L8iHroAChUKvR^z69|%Z%9BOijCDlaY}Q8 z+@X1tYiYfNp&YS!G@u$c0JQT|E{@T#JlIj&=0avN-{fN$d*eQpza~T&mS@{n)*h=SivE+thoOIRPSY?^{2z5sMrW66jzbjI z>+=3goJUyyWPAh&Qdbib#AW$NBA%;Igy7TWRbKe_dKfHR0ZOM$d0kDo7+~HcTM&KG{h>71^?9Tmd!vwJ*2hOk6O1D?x|7yp? zZvL03!pktp-va1JrMZ*kzh(ZbS!8tDza(`QkfEa1UVMC=NV<;K;9t%DseuC8N_<)1 zUkd!)?0-_=Z^fDZw&lMm@VDNP|2F0SF$MnDSKx025_IbH+VazuGK?khTz-#MV>cf% zYHjO%!&{2(YNg(&Y9I|F1S{|q5s5>d#$Ic};pDaWZv=Ho%zOIT^>bFR5X&R%@jetpUwdWo1JB{X$uF+wcSlfz?&p;>tCin7KEa*7vBP2&8ZOF8>Rrq(2CT zfBG*wGTl@48UE?B>?rgXaPDXkx+@|3Og!V&=hH~jv6Y-04u?&WUhr2X#iw|xb#LlpiLN$qL&x&zzxF9vIXSQ{dC zQ+>(rdurQkZ)CXMs411atIvj? z-Fz_oku;7AIa2^@hs>r=wl(fEmGnqf@ndGeMI|t}_3hgqz4>a`lB1LK{gr&Y`6%v) zlTj_Kk%D)w@*kByCb6Zi4`)hdiXe4c4Gl;g&C~v3&75k4wmbikaKyl*J>>5H@HA2S z)f5AFM<#fEAv9bjtcq3CxJVne^5Wh_z#jw->9dZBc_~=qoIeO`f^JZ6E41u$eYV2P zNhZbz#pnIvz>^H`#TnDZIaJV~pWv6!IO+5pNx60I9-B1t$3w;1_3IKj;p>9pJuA}n z>cuPGauhpqc_d#9&isKEvZ)7elxTEE z#ePhwHpIXTRXM0Cypy0m^b#eXDX%G90O8Umd?hht*IHZQ>6jnsh{EG4LtNe3=?X>- z7y~jR<qwvx|Z3g=Ttq^1EhPvu5rc8~!s$sE%pu#N9h9&F-Z7 z*Q5mnC6Bs9^;m3brkc}|N45v{&zE;MRa}%alP*-3>rd_G9btBhr5clCW7lNnOx;-= z2_vu~_;}k(btm7jeK@^SG1R%L)i{0^_|&5<4y$wE$)l4U2%{=}b=zf1G8@3m4U?OC z_3?29*M}lQK}x|%_DzNWkAtn;-0Te9p~k(@Olv@&0OC<9F{G|nyDOs~u#Z;5BOvEei|F3Dig+=a_`||GaR1PaH(q>& zR&=RvOMhS4vqDJLqGjjAGX|mi)3b7YeqW7yS$OVE4hcszv*W0`*;J^8+i`PsR}Au? z!)JL0PKflqypSDt-m!&YmD^mdX`>3b%rc!2Ue1vVxcO-FBe;7w6NuTybB#gQoBToG7PHxJ0DFfsuas%W{MUzGx5F)v$sZoFK)NG`bf#88E;}1N3?LB z@)Y&EW%}N5>1&@=)SY+F(Ldn^d{UhsTDIb_`gcZUAQ;@DIaaNfyeK$Q`6Vo+oB4B3 z^@IjLj?T8RX~Kb6{(d$PL`Xm&LBQJ4a?YpJ#-%esO}FuB@kwEA>y9)wXtS6dN}y$; z5JgIG^9F`_t%yU!hcYX57~SZi#8G2e*VL4=LS5iu%433>NiiuAk||WyQU@aHdT!c+ z*K$q8uE9^r~>P+Fxz+UHa@UXYi_PhN&P%`#5ree!8R=C{9^cz(G)UC;cs;7zm1^0pW%r zwWcQMyUYOUDkU}TUO_YkcoQ721RE>W=gkUU%NAlhDqyvx2w=o5JyM07l`3 z|2o^Nr#gS-ag|_nhk?uBRT_M|mYqoy3-Og}Lk`BWT23!zxd=I?8_4V+A*Q2740SZF zOK@TRb4GvFkwG;z*!HAf8vCcG5TL1xgr!Dehn5yJJ7l-liM#&FRZqrwX@LH2$Fj$1EYRarwRXh!_g>~F*fZf3m@vIWW!IE7h^>xwNb460*Sx@G z1I`%`R)v<-aGq3{ZMh)*VyJ+z;*}m%ec1Gkr1T}xomo=Q-L%99DvN6V1IpG@iF3lU z6|Gr?&!pQ@yn8{JkoRzPo_E;maUK#3Cxa2J*GF4%<2EAf_6PPZO?FZR6&?d?<>&%A zV_Z(J%(Y;BM4`~MxYC-jSm)^Z@|Ncol|J+6qFm^??$JWV@bMdTQt zbS%NQE;2?H{UOFx?o+-jY!3Pk6q6NP4IUk=`;+@m)k&VVg@*C9Qp9VNRo(#F8p}s< z5cD=ik|{it7U*IjnN@1Z$%THZaD-pp`Qq(HvHKqSdN&+bT%&oN2A|sbkYP<+FV1Yg zeaBoE&11k3A3s3xX`qoHmjkdC7PKB3e*K;salHOUxu>Z2UQQFWR)q*u_f2wk1~IQx z`xvTalZfFvM0_nYf&GcI6I|amM?POl`b=ZaIrE{X%NaG#Spt_4Af-p~v&Xx4ar4lF zO^ghrJ5QHn$j4FOy>{FQovnZi3c0A)So{@c$gwd_+46GU0yo#YD)5Vva_jV~Y<~Ol zAkbiMB$jUoG&72{ zFsD}^%|k(AS}tD#>zE7pN{NZcsLKk7u1Sk0j|j&B*;CKF#KeoQZkJj(M0Ac*@Ao!k zk>c{Ql3EV@xnAsLtwcxrQLLuU#Sn&qAmE7tFfjPk=DOSprICMANjm(yz19g?K2I~f z7EB)@NJacvj=_ueO}|;;F1N4pU#4>LwyYH*ltlpQ5vzxhB4yjMuGs08lJ;K6-5m=PX(L<7nUWr(v zS(y#jYTOPx-Ib!qc~mj|yoN=O-kJw=9I(M_0(1FJGAnbLjq|I-e&lQ@|Hm`Dx1k^W> z2dcew6D_H@l4;`fN{zkOHd#J(SGS%1dfqBGj?C+v9j5{v1%-<>`nV77jOF-u z5AEVov#za&1AD~t6C@bqcH^?-L*PO>TfSF?Cf=Dkdl&~t`2fppjL%n)Lb$`-ng}^g z>0Vv^QIpOtFCNIe8j*!VyiAah@Y{<}*~*U}EC%T^=jzKXF3#Vc#o2OKTj=;u3|oH- z3x4e1`=5N!t!ddPmDS$trhf+^ANlVc{^l||dbo`HUW3`1{T*mz>y!liL6B0kp0G9` zz1Fx%UR{BQg$KEARca2Z3Vq#VRD1R#;|uvR-Sl(u!}Yd09P)9Zp}Lv8rvbP=!6UZ8 zW`z9CQH}kjOJ1SZY-D_(?1b}A%G@DnOzG`)Wdwt2Mi)!2$R6IS+;9pF}ubT_&iCeG~M^do6N7?^drnJ-SuPpnm>~qUPOxO3j0P$p@FopkAu9=UX&dVx( zX}C)beb5JI>5k53rTus1J-YLdlG#fRnRJJ8_f7>c89;sI{^-9epU*V>6dfIx*!!TQ z6Mjsm>cMJH9PyvZj~#j2O~KKlc640Xd`UQkXi9NKp?_C0{l~y@-IW&+|M!|5#Nn74 zFiE+*t=zwEHu>qS?ApH#jANsN`9z;<{Ycp#;;%!t;JR=%NeSc#GHV$p?UM1ozx-dI z;5QKb%=gNaBdm@JAuT1#m_aup;R55M<28Yqm0=X{hEf>;yr1xD6H3Odp^eEefOsMgT7>Ww$9&X{;s@7 z=I|uhZIgR1Qnjc%;&sjKb=3m$<-aQvww<~nMR!c>N_kDS-$78tdFCwm3&(3MXApiY zmXw%O8JA$@(I*jqVC6+D55P~tZ;+j=nC0jFz%wV>l^x=dg+6vn>KGSTuaW-DEU13s zMb2%-aeVnmA;O^Go|)a>J?@XXk0&z^zlaUk{54kQ97P8#VlxLsE#>powYE}XlY#@9 zrBB#>ZQr<=q}^qaVvF@p4Ho0JV?P%;|1x^=>-r&5!vVlb13HsW5=WBe>1NdN#-ht7!GH|xG>U;@4 zH-R=T+05+sb0@k!XbGUsdr?jDnIxXVGZyj5Zo&F-4czXpn~W&kCv-FblDC(CYN%Q! z+N7m~3C~_;?N4rQ93ji7^6TeP#nD_Gw6r7EPJB(GPJ)3#W%Iab&piK8E;B0|Wcy=6 zZsV5(Ylb?u91;vytlt3!;9DktO)Uz!ugu;ce;!HMjq~(!`l{_(6)gwE4h^egns7UV zV0mv*1j>@j{kzH(&Bpb%*w?Qh6^$S2%gVr5;Z*a7CYIed;RX&PcVOB`tKvarZDf6s zUrnQeg+q*_m&3eLZVO+83#kZ341#6y7#@f8&*O7sT4pkC_5QNWn06Hpo&d*kk?+&%fY7Q&b3^TGu8&y=`)?epVDqz96vUR z`O&m5N$E2f(+boqenZ}xVUmeBFF;0gd71(|t}!FTkK>%A;Jzhel!DH4KXS}GyWQp- zFl0^lSWBiUYVXO{;TP?eHaksLT)3R0%a7&h(?-p9tyI?!GTzubfO@bkk zb8$Id5Kon>BcVw`?O(Fvc`~5+)>l!GdLy|9td{BhmxCzRRAy4~ZiBDAlXUaEA8w2&fm5yl6p|&NFm53O-X++83 zjXU4rh7g26FMz=P`PY~$*Nxr>HUe%asTX1<2Nj-18o-Z?{&%?pEGA(TI1^a3mL$R( z`TDsc3uczxVxD|HfLghwoazQw(NaORk@riKFm5B&vy@>P3rORyQX-^NRD6ck6tlQw z_i}{$JP>JU3M4zK@=KjBmN~>t9>+0I`_CtOR+?oa9>UHS?SIw6*N}A%ga!hPn zZJ_xexom765K&CMroGZ-<_$X>PEoZm@Tu_7w&&c*O0O9jx5v1$IH}6}#YZFC*K;_J zp>g(kgmeQ`tGKsULOK}0ezUf%xs)dM@vRMm=QC)KvYX7veCG68K0f6DsK)G_{fz0x zrWjK4r{rewz*N=ub1vQfZYL`{GfP!tuLPpn*Tm9tH<$#VBNGRkZnKQ&S(+d515>VZ zX(^^*8(OOBs^3kD4et1=gRqM?26!Usf5|`(iBxQi(4CRF7+YU zOFmcUS-u2Df?=x93WPY?KivOhrm;C&@zumHb!zWt?l5@3TyGjKL>Z5n^_~7OLS4Xd z8eC0NadW%1W&rP5qDN#5TV6e5jo-0j;H(CmuQlTjZ5-`XR ztmib8^?MGec*^{YkvSJ&zmXVSQJ=`>p|k&eAoh;FN>tgYa3<4jdqx;w5 zKKBG3gq94+Ho!A`M1Vf@R<7n9j|uv@Qr&s1kDDt43QIal#|_`?1e%pnoaUtI7l8-F z6C{fllralFaZt9zvwOO5C>LC8*DqEGBX&2B@9nFtidcuHpVe&@dhlcRZSMxXOq3-` z^4RwJsoQltAMka7>|am;R#c9wtG>A{qH1Cv-R%fXK|du=@^@#IUN$O&(i^z7%NIdq zIfP;RyvY*0+wx;Wp#dTV3Q@ofanC2j@s+!JyQ6rbljr8tJB*BjbjC%Ji-Jy`dmlbN zD}|Z8=PCO-Su6N-BAb$C+|2hEuX0M-2HTjNeYJ3!-iCbL_U($rDY&tW2q`h&5Z_<# zzqSncMvq=Y*7;gWogIB{v4PuMIM+(qxZ1I8!j_5P0kUbgrzfA9JPaWSrj=pm4;n{G znuSca0(MNMKoNIXA)my?55abbuu(v!34%AGn}5n~o?tgoE&I695p&ZE!*A{#g9e_yEEW9=1n$*_Z$ zfu7^#+efLMDcsNNyQ>5ujc_N3c+p~Z_@viu?-~}4sHaf>9_RYO05+jFfnc82W|v&E zsc1>1RucFo|nDtPp^i9e5!x+lcZu=4)IE0Zbokb~qa!bo1} zcHedl!Zm&iWc4eOfyd(bVgiu~~TH zqIB__XLjwQqzGZ$2R8CPbmi{A3*#?hW`EJcME@|Dt~o8Q`4;yFL0C^H(+J^}&(g2b z`1qf0h)aAb#gco}{~I~#V*e$x++hBndFDFgmlKD!TfdNjX=6%0&l$|9Qhutu?PBc_ zFj*?q7zE2s2L51`c5`UA%p2TEG@0G`7JhOmwCg`h?Ycx8pg&Q%DEL_N$3~F>suJC# zgQ`OEDQrFBhVniR!sAsI--o#e5DT2tR;1#|iVAZWjy#t9Q=Nxrv!v^NOE4s9<<171 zyLvSAHivpV+S4fJ(o4+2i^Lse+~+2{s*cFLoh)J86ijbWB`2>7EHiex^c@%Q98WiU zdW|W948tKg@-{s8NedCg3$6GlRZ?GYSjyS&J8w~1p-cN*)o3MV-_ePCj%i_&Da8dk z)WOZyX<}+>Gl5eJsOyoCU`w@rV0%9N-f)&_Bu(wj&Urm>>#^u*V`W^$dz{eg!V^ar zR66_4oof>hXimS19r_S#<@ zZAG~B#?{M48i7nku0~uyeB72QIYInJZeNm%nrq{g-F*I#iH-(sY@R`2xsG5Bm%es# zybdp#lUveLxVe-|n<2vLlX}+}ee$Wgbc zLkZ(7nF-)&4RD?pqdyje_V+(oZKSn3NOqtSsjVAY4d`VtB?SZFl1WaFwonDBdnjihSWme8Sw=Qn&bd^&h z!$mZ$;Wk%}wFL!Mg1b73A;mF|Pu>4aH{!^)6o?E%K7LF-tUx3YyX?<^-arFE9&#M% zq@P0oEaqH0kc+Djd>pHB4O5tu+n{vVv>We*Qp;V2i}na(gkv&`V>43^N)|yk_!w&M zT~{|v-M@}<7+Wt(T~G18G))O?dgumV_L2qz5K#ON%prj45GN>0`b{GoR!577`4r~< z-6Umh6lBpgTD5r5&~A}YKIZE0pRn0Xn_SugJI&7Ck) zX1`aZFSfg-U?#RlO-=+wPBew?uo}aASX_uE_%0M#jK$H?*Sq94qD}`aJlKdmz(o1n zb{3 zM#MD<1g*!=I9**`NL&9J17@Q#es!R3kc0pEFh2O$^tQ}U27Wmoq^#fBH$63#1`2w# zy&tkmOW*gdtcFiV_N@pWS<$0B4enc1*VSMD3GbtN5c#L8 z&i6mAx}qzovtOE9!GVAAj>o6`f+ix^(y(J2?aUu^H&a9m%cOyr;~jBQ_n*gYNlB#9 zJLD{=U+Ren?wF#I0xTmhH)o0VnW)e^&=&~!yL())4#OmKDQwhg+D#uSFf|>7tNd={ z*Q4B4XIfPgo6?|4iVMgBf1Xo$`Y08E(SH@@PqL$V?crsjx%I9>rq7mnVtDb|%zzA0 z4SWQlroG%@(DkI@?oKE9S$eN>vmiaG5re8K$C;euwELXyn>$I!l&Gp?cI6`r;>#hi z@^32B*7O$6a@_5@$dc*JEnT|1J%0MdU4bR$@tR8_yl4(Bg@)T8I2P$|tXr9!fViR1 z=!-y|)3BfYDIn&z01GzZ(}lRkq3#BpntySXx=e8unj8^Mg~(1&xe?Xh^{$_kkak2; za@9FXCHNmF&UXUbKr7}25J(5Bu9$RJKoE@;`BX{jkD~>}ehapI^V^n(mr($lFJ)g} z0Rf>&0dfynIY76R|2q4h>N8Ljy+UX*-|}LxHRMPj!cRj4LpcI!!h-DRr(Uq_3M9zm zk_u5b@;ZC(nFhHoow!;voIZ?Y_H(2DnmEG0RI&2ND9**KZa{w8{S#~ynw*~9GUw;}ua*D-ou6Dxa_BfWQsjWlJ9p#n46 zDoo}u{>qA^UReZO}I;&eI1 zuKUNt@71CwWt1h43toF(+xU=8!c5ppl-x^5`An(QOD1jZq=r~3oF}fXfLJBlH%k5%!FO1f9N;U%>P-Ru{bj+O z)YbWd`W2VE1xoDjr$?};wIXRe_@$gbv|N6%OnSBp*VUN2pj?u{n<434aW@Xwq?I?$ zq%Y{IqnkTucAlF{K_WM4;%58FR&78H+(gd#L7mx?_^G*U?a z=Z9u}!?fK-o(SG)b7K=*Ju#~JJJ;RKv{WZ+a|3RXw%RC}KHeL*nJ|tftBi|sTuCoG zr}bE9PUzQ`wWw&8^5AmCJC+uZs>)3z3@%ujBBPV2#` zs$vj5_zmWP@~0hNLYR+@8f<-BHO=Ja>p{n93!iX+zoXVbDnTs0tH|KotA?Gp^HDnt zt{j1WD0I=Iz zC{znQ#^tyR&U)B_XvI}mq1Qf(r>|z|A}duPlW-j#XrD%78hY8*Qq|{%4G7>xZe{{* zYOqXqjA6Q%zq1jKEl0ClFk%MVT_CMW1%gN-`*PQzDiV1z5f8|VLMRdqG$TiuwH#Im z&R`w9e2nOF$6PPpda~NidMtSFgLM)d_)1C z7fYp&CRZ%!5?xt8N!Vr;HyQ8dzOBpUCDqoGJhVRt+z(@$mVS>9k<=U(#dP33V$BA7 zQ;d?|juQrdtYQ>p_ouEzly99hJ<*|m+>-I4Gjj4-(F10Kdi)4;ZBjC4XY8WO4Yj&qpeTjM{!glW}3<#i2tttp&a^zWZ2| zQ28|>7ex&OU@v|on8)p{E)|)<=e%Q?Vy4*VQGz%J;~XQCa-ZHD?STakN1&QHB*D5_cCdv?*^u zAP5Y+QbSlSqt>bQeo{UXY8@_VDghfvG?EZ#_*fyM%zf(sn~Nu+Z{~V{jTWl=;%aTG zIHvcBBQMw37`b@=nEk^SUj|+Riu*EO#XpM&KY zwc(;5y{cU;>ph=2Y1pQdr_H}Xfj=WE z2F#HIfr4%(nBpNW_-w>H@z^wy3d9Y%$u$+vAy85fWpP(%tG-@i$2fU? z@Y9@LZ0c#oTxE}b_zSg>RG05YbWSm9^~3iuN@z~FYBsk8|2CV1DhY$taWT#M;w?wBL-V+rka+qFbG0(q81yNKh~y*D!`c-7;yb{eVxOA~D%XPSPrCE!LYHFvx!0b;9|DM!2!Nl?|vZ|2{mcZlQ*YtvOkfG$SGhhox515kCUF9MCJC!`nSxddp*dR6lH7o_m%n-Uawpjh9 z+$ol8Qa*hfY=J7(&px#dGolLsXpycFal@5QQcP9vzIQ##UOI8vtO>5sm-c{qxWNlb+}*2oM5Gq$mUnV5eDXHlln0YiVT;YguAOpGbDP zt?Ph1JVGKue7u7?LQ1zRCz)?b<-?&x6R+O+1RGxAkz_tuwPy059tj~TCjpzb9QT>h zGT<7LbFCoK8{|6zB+ch?P9`Rj*|iwl32F0}`}4Smf|mI(w*gg3Lmt{((jMR0Y=O40bzVDO zQ1I4aES0zRIA)=!HL1fI)Z00_Y=5j6ioZZKxKZW9S$<Da@l_8vegAuI+Sk-BxPRHlIVS``1^j+Obl0u>S?|9 zQ>}53qAwV)5EOyWq53^FCZOf+HTv1Pu3*EDO-YpmZ`>z6pKc_0Og-PTLcP$RtI(^K z>NJ|Q9~a{6j`vU(;5ybR79R|NhCjD#KzMC+nzx?rI5Z7J4X+} zVPJuP_>un?egTY=v~CeDz?#pf`-LMONTkGWahQtYNtIAmA>Q#C64nz=y;11prT3N` z&tg-%ws;+OX^0Hq||P@}a8Y-CeYO2e$+j(t4XQny>zgP%+nHNUjJ zhW|mJBD4Dk0a7`X^2#lRr}V?Gb^k)}pNvCn!S~;8U37#TM~3lTN*r4JC@ARuwa_Xk z{3%%cL@_PsPnM!LEFt5E_~i*3_lPY{1DD=@S5S!l+3IPn5xJHq4EFoYZPe}bUryKY z`*gzMdGl%P{~MCTI%=2B|C{3fl=SVlPAPbuwqyk8|6`W)-|CU)cmJAS|Co2Lm&pO) z7HFM=XIT2J`{+M)SKRWDZRS;n*Z%O>jAp;UQ>1;0|0e$jLDACL9|QwT|GFGeV^$$9 z$JUyET8Ia``tL`{ZhgUDwCJ(bzmEFZGfZK&%~%&HcyL?*c^0^#Q4o?W!7kj@F1j+- zBk`_i5Y`f0hB8W>T%Y`g(IZNw3@c@zkYX#n8``Z`o)Qao=`6~kj4gmj>Qk(K@UD{VMP zQ)%E20z7gdb!g1TNp9H#|B!hJniqdE#~P?JxX9Q2`jUo+H_W|v_Q-FM&Po>T_cNLT z>78wjeBSo0NZ-DE-n!Lhf4jf)Xo2kD033+kw#V=^&U3S1c)%|E%qRlg?491HiMBf{ zm#ZD+dt&xeb3Yb6Yh1-7w6oJGg_QdH;|C93-a^+hEOICHaWb#P`RbxU!sFTOg*kne z#jx{!Ea{@;UUr%07+mzHsAn?kqy&p}EIkXzGGKPz{YU;(J#X|0T^SQ4T-Y*F9}wd5QWbOZrQ(n;MtdK9wm>W2foW*-KE2hm92%;wO4Md~1rLX5GA4yAO zE7z4Aw3};MrFg;|s~1Ju;-M8%EF!vs@ojtEO{Yzzs#+v2T&jfNi8D*?MsW`4R%MhAWL9|J{Z;v--gwazv}1GRo|gGy~a%?Cmr5nkIP}N31~E z!yKz>ep0y%|oFDL!sqi?IAsyD!x-!(X^!P7#~gNF5!= z90jMO+oZrITn;fQc$KU=y{~u>It!WGZQHPO48zC7i%Hfe`d!^gZw2 zYd4m!VN)y+9;A*?qrNG{zxN^KE)_C$ayJ%s`Z*84c@6QDoQ`&R`cTkJb?5MEp}KPS z_$(jq)1ki5ENEr%(xS=K-RE_DHYD0q{2|Xz;lsdWw!f-4>8Ny6wlkA=Yh6$tucx)i z;dpvijvu#&AxDOUcEYZvb*fCF;QAEysjD%(-;2950RA0SfZ*49y))H*!<~iB>s7A2 zz{ueGe2cSQA25U7aWM7ygpC%q>Vsfo1Wp%i-=`2z*Ns4Xd}iIDOu!)!PTIX~WVYKb z(MyMrmZn_SbO_Dd02SF0L)0qkO~p1|jzg0!jGs>Xu=o6V#QAbTv$);HQW}Wcbjh zBIGJ`Y;#b^^yz%9!Sw0Z*ScQe?jgyDR}us$&>ZX&^WPtrAYo^d{+*7mha2kER2-39 z8&$s#|kS?c_^-qKiFn9$;G@LYO@ z6fd_<0aq3!M%7DdFX2W!^Y&h>d`ey5t#EhN4sesLZMjBU=`70^-3eaq%y~;S9?@!! z7z*P4ULlI`d1NWCezg(=Fg=IRea()oU^P=Cqk@m*1z#Hx`j9KGgyQg?+r7GTuK%_o zE9}X9xuJ`Ldx5b&MLtAt(xdPEVVP)(eA=Ao)LyJ);pF5LD^%IHPT$Jha#Jv+Aj2Kq zSX^dn+jUFXyC+%!yLG7(6Bs0g?vI2ednUQuT~`BGt5^XH>z!KBiHx2j*Pi1*JLR@$HjHdCY|G-hwUbsN)~LDp!MN*Ej|+`wVvETo0+$34ExGbCB)gpVbUCFS3ON~Dia zl~UzdtS%#P*=93U%|ECZdbpsqQ&Mn{E^bFJ9y3_SBD^T~bq-VL=824WePPSBm@66b z*xn_x-K-G0pDN#*=GmZ{UNks7!|3^9Am;IEO6lVi#A+%|tA5UVTU=r-bPH13RW?QY8A>=50}=xQBz!gn7H7{IfAX%D$Us~?4cHFU^~TJ+grJWJ z(A18T;y~sbNdylG6t@7Vv?_2=G>&_*#^5w2bFN~#BF$y}=Xc(=n*mw&K&k)*p^mXp z4<7Ub4vJ&%NCq7Y$rDvh+%2freUMto_w-r)o=@c_LlavuRaz7=gMRn@zb99`G;ML_ zyqWN4NAyoN3j*=mj&;TzQU3B1E-r6vKkP>r{3K@J3fmUmv>og@rY2(nvN25hb{ z47MC)r>y;WvfPD#XszG4b0RytBTmGs22&8TJtgw zIRu29?<>O%2p`gmNwm7jULa(zd<&xV(R^kORWd%TMi6a+b0rP43;DV8{b4DE9At}y zZn#{G_rfWbdd4O_BTzYFhF>}?r9~_P2a2nl6#(*QpD;N^($f zeW)@G>N@gu5;)PsWJn4mj~*P#Z#sd+6*x%a@?R3K%-u1b!^ZATI*Nw>q$vxrD(kg0 zw0+zCs9fqi^3RHzsfW%r2Ry%+e`>F+E>`?VbRw=%*!=ZT&WMnWHipnHOQ>dC6m8_% zp#gc^_UMI1%T6`~@vTfck`bD{?uw&HU_(U3?ppNUwE$StWw_0`arswjUEP|U8g^U| z;ou{A@&(as6Muc)!xe4&olSVH=YXhxK#lYLH^Op$c$QQfuqx11dqANt138z4ZBBlp ztj%f>gkRSbGzh`D3qC(iRB#mU-A*7)kYqDYWL2_^0v%R`2Y!+(ne9Sk{lWVVvQ%!wxdOaP1hByKR_n4pP z)8SKz45zUk(l2Az444vig^mLX4?3In8%LkwARBs(jvHUx$6cKya~d+XKj_$QlQ^bK z(OPTpwB&&Nhbo@uuo%Y>+JtiuK%f8&fFXf4|jjfQgR#;QXc6}Te6n$2Y zmTI>p;bm^b-UYlCPYPtSma@|+-CW8Hc5R7d(uN^=-b)TX`-4D_*c-@JOTgw|!>EN{ z5eC#AfT~rd>uAL!tb$}MA<>|Ovythm7UAkmp$9T*es6kHD%55tvh5kHOKsalN}a1` z4`sOUF3y=H-O0V7iCyzwmM`=h^AV}<0pCGC&?@Zgk|zmTGug)x2z{sL;}s<^zmI*3 zZ_jItvoM1)9BRH(D61S}TQFI_u5qSLl!=NYqVYA!7W1ljg|=v@xAA$7Yj7gh^q6zz zY)I>3x|-)y1TI{7b5WKmK)8RoO`l9Dalo8GRi z0{h=?Lk?A7796orKVl7MdVR4^2V<=}MKZbV2mS?>paj&VTNI*FFDQcipvEvu4Qb{qA}9 zWb*E3_TEpf(hCm(DQ+%@_@D=68!rd8`z1Fg57ec{Y*n_Ub_GcROyu^or$o;wf0hl* zoE}*eT*7RwAF~X-QGa8+6@OXqT#X-}l1w~J{EOg{?S7kZZ{u)PDSX%&eaB2P*JzYU1?3b&*)8WBhL- za58eoBi{c_$-MM0niBsrO}_*A$Ul$~Mi&L< zz+%nL-9(9wx<3JiKj4So0K*>`!oOY~_mW>fo*+8m3{S%HPwe0iW&di{elQ-t!A~js zzd$H|Ovk@MDE|h0>>pvzBy>E=4hBD;7vV9D`Bq_6_qYCR@k#ZYVfx96rVZMJNhyyC zKHObN{i(II99`kcG|>)u6scwQ)5VsFID^FU&6;fUQZ|5c0a(_I?H+B6_pqdky+k* zNM-ZZWIM89nuYNALt91zg~#^knKMlsVa*TJLa{0qxeesS|?Pk1e^RiKe}G% zf-x$DCpj+P$e;>~8rRFPttB=XAmoLHewXz|e3fMQ{9alBLMzg0RBmLERX&JwqDcYz ze&>Jx{zI2f>nf!Pe2W*uLPVd~=39>E6nCBSGAz*_-L|i-O;MLoFv|_i3wr2lmVhsd zurVwL(rt~B&`yN+ZRo5lepYSrSt%ffY46gt^M<&RPZy2|m3ZkIA?==uG~5)hSIl2_ z)g3NZ2pS4%VhgWN8Sc$I8Li&ngY1%Ji49_RzYjWMf~ohO5_?8Csg?AU*}}YgRW^hd zKy_t4!4+@42Qg)k?K#BgaCJ~(I2Sjc`a4|$4dzjbM}0|D+I>?W^WJ`M9-l8Rl0l92 z{UR{Xd=2eTYpQ302DK^(gUdRE)_*!#*||vzb^nj_ z@4}cv)f1nU2fv8|B2m z%OC`h?BoEskgFUQj|w4s6^!MFzWlB?p@#loTUH{@#Il12XIpwbsxyuGi?vLwa2HyW z@!;Fe4kjHRr_Vd_9W4FWs(QojR@ZMD%>}*W)v+%Jq$B5uby}Ho{qpc^2J>>Xj`z#h z+#XgZ##dlK36d!8I`;z};dcxhl$X2Rn}3|dG#7=%TI$Kq+fCfL_VrNo zOHx9>cWU)buLdHadN}JFb=?52XL$bcqQ!anblCaJlg}M3A%fh;*LJ&0g@`_{OLMMR z$g5qvp_aE|E%OgkALg87Wcge9_CJ(MTYm~=wJDYtTUm8)f5*8_wH5X6YCHc2YX7qN zKT!KWvG)J1+T(wp;s3L>OJ@J^+*0rMCeoVRAG#aTa-Q0{VQW7NZ<%rA3sJl^`aowF zBnvAHK;(DoJsz;YYWvUa%utZXSP05*Ut%TlKBXKIA1nXqsgH%L-oqj4o;}Vf$}m?b z?ES+7HE&mrqDU+rFW#eK|8yX{K7yxOsS39FBS{i5#&@_k=L+r6d8<{f-{=envVt4a z@)Uu3pA^?v&Pe)bryT6;Jy$sS!DmJ%cW`((_(zJ74zsI#?awd2;b1eK_NFuh;Z)bP zjBg5SeMT{cHCh(5R=&fhMvn#_u6>rJdSgKZBiP8??zV#SH|O6sHjvty_O2h8>a3rd zlCi4t>sLEOOD+2SjNZpMc(+vfb?b3mnP6cWT^gtNHO|upgn(O@N{z6KMAU_^;zJ)J ze$u1c;?$R6D!{zThsI?L&Pvm*ZjO+3O$ywa?JE(!_eX*nA#2=1;bnY$5-`zx&nKyg zPd*(;Zm>$>>%z}(bh#`Pjyw5sFKPi4sZ&Aa(uiuOOtK1mEmr$@grKzO);e(GcI(#K zHU|xh*!BWg*}6@#rzB z@f)Sd$?#M*RdQa&Y!7M=kym}P7msN2YFtTM1iU85KU9i`kOlw&j#D7g{JqcL*cnBlf+o?YA$ zueFUf5E-%v*r*pH|G3JcLKp^Idzss-;J##>z<7w>H7_zKBh?PgKRnuO6PRFdc^zI3 z3~Y|{02f7|A_F|fD5~rDeVsvwuZg4(@|(^?Wr!r0>^t;Gx=H#TMyRzt~$F=*biT7yLBD=A?m=x{%VdBw*6V;X>ay1pHp^iV;$$jBbh@e$Wy^tH9P z+%LwqrK$YMeGMII#Xcp^RNCQb$UZejz$y3#g<9Dr7e6A+;fK!oN_pSng7J)tWL8GNR%}>!Z zc@e^TaBWQajC57W@tx8X@%MJP#t}YAq?Lw6fuJO-g|s!Txv}PDY6`FYn4;$(qfyH6 zldqAUVT$k8x(nu5!8rwQ1m^_o(-ZogCmlzzdrpQ`BaTKl%rde`p1mvq4aH79@Ib`h zT9c%;Lp+E3R^pWbuB)CsU)Namn;nGw%jGw$p2~5pnV+*fN-^?06#8^a)DXgm z!4*=Fe}plel>5!g$l5pD>lLrn=qxQ46JO;qTx}SY;M!{9P$w*0WdRHly~G1sYt5Dm z_gg&b-#TpU1l@xXa03VqxSr5&Bzhu)q6ZjtLiATFLXO26w%;FhqSh^gQq0OB%brVNCoBH_#@{R;HIKh+CoJzXPl-5T z>)8Kzj`Z5_>`hMuSWgQN8NsvPiC1@#iI3cr58JAT#{9hA6h zgJz6!@%c?m<@sj!k_ykVSAadh2bv8cMcr~$dNB))tp#vKOu8F3LBwgJBkP*Tj0r1+@9@G{sEwEa>UWDVb z0)lm~5=*l8V>V%K8i4%O20)4d_bVYiQM$&>aa4M)N55fU<@=GUbKu*|u?C8p;tiWT zKW*fP1M^5iR|9{N*QA|s4;=NRg;z`KSm-E5BJH@dHMQ#TJqc&2e|LZ)ARxF_FZ1=v z2CKOQ>y)AQ5&|Vh?-2Ea@@tf{e6BA3_3e)qAF+$NjWbTf+qsOpXR{3WpwKDm1azri zV|&^AwwJAfudUkKQ5PtgJz^%u9see&H4wPD`oelA-9J!hhT)pVrT(G(WO12sQcvEr z{!!;p#kPLN`^f@J&#X*)X))FYGWDwdl+5G$yzh}VCHmVBa6!WlZ=pi1GQZ&W@Sp!) z&1J6KmG=GPRQx*M-)Wg(Q!;k6r1_>-s@1eRV~IC7ZA(?jDYw+Pu;amn5Zw{_$~yr2 z3YW*ABmw@XF@h^UjB_|`B;Kr{k)?3h?BLz>HTW3erlO;ymwR9qGIw+F9zUO&E-W|y zx_%Qc(X*ZRrFJgJaj#u*$U@Uou~o3cR%}fNUr&5^{jBDm)Pn>K9Y|sP%iM2Y5}48R z!CZ3(~y)Tin_O-4xbJFA(VmLkoILZ*!k8Fs@g8o1Yk~|;Lni<`q_8L@f~4L z9}5nNhi@^x*YPSOCm?z;*makuYng8gGUNnVyL>W9edlqebLXMIpluqWL+SyB8cwOB z3tFYlVlVZZ@JNGo!DVs+=hg-6@T=}t!Xlnui3Z=ZCEWiB%Xa5}o1`!XCm(8C zK^Y#r5ynCM=$5@O4?nCyp|26n1{3*A;4Jg6OT~upD^>f1Ntx;uEPt$I)J7?9H2mwj zlS%Rskeq$+@k>%EYkwY!D5@p~~p6>pY#mg~9_TbBR37Jh;-?KN%| z9}EZTC-??^LYa!jU^@I9sSRhuPa7!80eY?sefWwGgx3ubFYBgLMZUZ^i|+%v!L+%d z(l*&S;Ow|<2UV37x@mpsoc5;&MgTc!`@&h?Z4R{pA!?-H5fWbtPT+&MDuR4+>t*iO z=;INzDeHJeO@@aV7(00bOS9AN(*B@SaqZ0lhhlx;=f{#Ol&nPdBo>rP~N*FY>TSbS`7;w37f@h7G5h6(Je11tD%lhpa9V0@irn ze%no@dL#e9hGdRd&d<+nO$KPq({fsayEX)E-QKY}*uzVLTRrpj30MLkq+|`o=`k z(L_-PDPDfH7WQ@Z8mR}wAQ!~yIOR(TyKI4--xgqxjOXX{uCe#9*`HDij%1H>SG9va zwJ1}J4B;4Dy{bC+C8L1PBlyoa|Q^&zrjaz~~W3LBp=?6w25EBxJ2-U6Cch zc+n6`Ovr83>oABVW~nn$1SH61pHJ(TR4s^3VfRWPo2O(VP?U1Mb%}1a8k&dxF_bR< zb*Ndq52d=~*fgz9*V@|bOm6&6?uvJ5wS<`dbFV>1G@CEh%!*GI%%sQvZY5b)OVfTY zCdMQqQ?I&Ctq%`M_$>1dWNwQx6YC+b^Vg|Jm1`}U zZ$w?Y$)9T&-j*u#XtbW@gqVJis6_a<;U+fpykXVZD2REI)avLL0o{Q|RFv|_RbRKs zVJCtgC4`d`*{^aw#u%}$!Ckrd+xalr)tcFHWbMktM!Er-j_IJ3YHpAoNngGcQIwXG zAWJ#acy;0c4R_6sA0mr_e6*X2amjVo`Nlt$`^ipUccy0{B(jlI;&!9e)T2--IPbq|&w90S9Vttbk8&;Z}rhEWKy(M5l)yyK`J`q?#yK zW|53px$QdqVD4a)W1abtshfBqqMXYX-$U+av zz4Kg`J87R}X+W{Zx99TpD^6vkn_TkKl95texcNgbQcGq`sa@BUjA%T07YBqg@>#p} z?$TDzMDFTJ2+VkoYg+B2OO6O?aVUKm4$B9DQH%@tZuWpxGeM-{>%as%S$tasCZ>$1 z;v)rG@NzS;EE6gI_kgDeG~Cr&&{Yte?Pm!RWnh|Kcr^An^vPKs(vZA^=#Bkt-om(bX9W((s@xR2=95`;=D=x^At7EUNTXYpQnSJ=^X4MF3Xk zM{81tu4;GiduwzU8LXR=A@2J%tBYe#@~|1>2)^8}8@CH<*d5{nMUG1>rrdhDcQEi9 zVO;#)U>6{SzPzmc!M<-{W9gcI5bPjk8PrK7%lV9biV+)=`@mS}t3i3`2?#IW)f*4FKcuwIZutEsEV z?Z-k}6DW&qb$NzS8S9?O#DT&M(+>1)S7FJKZtWI9M`*TbP}-Uxc!I>u$(gX|>vEGI zz&}NU(EdJHL!U*RA%0tp5~Me?UFR-VyvBn&$zm)?FvB+?t&to|H0y^LxY{z$i&^&m z2FjwV{em;LnoM5EhAo={x3Rfw({zR*Q4I=>aZAt359F~ww={h_!zjap)N?uiVi_O% ztJD7z<&S#u8wwkmaTh9~O{(8KbpZuxd718KeKXM1gBMzQKmYmRbkK8k0NY2YpSM}^ zr~E4QPeKx(+gH9P*}up(4NSv~+nTx~)2h`!^DA=|U~FuS@}JolgDfvIxGxvpevKZU zW@FQVgoEcp%vQ3s8`$}H7vqUHOz@OTcnZj8YhDb}V^gIZ<+5xGCD1isAY|f`YBMoY1NSO++(KuVrX?cpJ@UB^F}| z%5li z;sF2vZ z(ghxP^d3xdjq8w;;K+Zbx#{i&k`dmyU13KdWO7n!VnCtU8Sm^kHQ7t5qo(ezrC;IN zlKy6@fpvT)q{w=tL&{m70hSxc)HE!3YR^J^Mq*m25Au45nFkyNJl9U43UiB^Wo=W4e>8zE?fsl6bK>4a{Rn-MGYL1+rh=> z`M#&ri{3>5Unf4DN74o_Jfy>)nabcfa3}+1E;a{WCTCA zGJ+mwCSzM{+BBQTGOIddkcC9grPhGI)Mz)pha@-xpt=;iyh-}eZfW4mvxQ~tZ9`JK z_qZJ2auw}j9nbO^4c0H)dM2JG)uQn&LoPYpB5`P` zcvkbAVLdTHc=gSj>@TEJG2(`o1#>hpPBcwI34%PmHyjOoaH58J-CK%Exf^R`NYJ1~ zv3F`}pG=v*s{H57Dq(E{Bu3>N!PA^TLvR3wv&aU-^Ld8%j&lflPq_AL<{g#|$%%H6 z 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 literal 0 HcmV?d00001 diff --git a/docs/examples.rst b/docs/examples.rst index aacb2dc0..8092a257 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -16,8 +16,7 @@ https://github.com/jquast/blessed/blob/master/bin/editor.py This program demonstrates using the directional keys and noecho input mode. It acts as a (very dumb) fullscreen editor, with support for -saving a file, which demonstrates how to provide a line-editor -rudimentary line-editor as well. +saving a file, as well as including a rudimentary line-editor. keymatrix.py ------------ @@ -46,6 +45,8 @@ to stderr, to avoid the need to "flush" or emit newlines, and makes use of the move_x (hpa) capability to "overstrike" the display a scrolling progress bar. +.. _tprint.py: + tprint.py --------- https://github.com/jquast/blessed/blob/master/bin/tprint.py @@ -62,7 +63,27 @@ worms.py https://github.com/jquast/blessed/blob/master/bin/worms.py This program demonstrates how an interactive game could be made -with blessed. It is designed after the class game of WORMS.BAS, -distributed with early Microsoft Q-BASIC for PC-DOS, and later -more popularly known as "snake" as it was named on early mobile -platforms. +with blessed. It is similar to `NIBBLES.BAS +`_ +or "snake" of early mobile platforms. + +resize.py +--------- +https://github.com/jquast/blessed/blob/master/bin/resize.py + +This program demonstrates the :meth:`~.get_location` method, +behaving similar to `resize(1) +`_ +: set environment and terminal settings to current window size. +The window size is determined by eliciting an answerback +sequence from the connecting terminal emulator. + +.. _detect-multibyte.py: + +detect-multibyte.py +------------------- +https://github.com/jquast/blessed/blob/master/bin/detect-multibyte.py + +This program also demonstrates how the :meth:`~.get_location` method +can be used to reliably test whether the terminal emulator of the connecting +client is capable of rendering multibyte characters as a single cell. diff --git a/docs/history.rst b/docs/history.rst index 831736b2..64800382 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -1,5 +1,11 @@ Version History =============== +1.12 + * enhancement: :meth:`~.Terminal.get_location` returns the ``(row, col)`` + position of the cursor at the time of call for attached terminal. + * enhancement: a keyboard now detected as *stdin* when + :paramref:`~.Terminal.__init__.stream` is :obj:`sys.stdin`. + 1.11 * enhancement: :meth:`~.Terminal.inkey` can return more quickly for combinations such as ``Alt + Z`` when ``MetaSendsEscape`` is enabled, diff --git a/docs/intro.rst b/docs/intro.rst index 454d8cd4..640e3032 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -18,6 +18,10 @@ :alt: Downloads :target: https://pypi.python.org/pypi/blessed +.. image:: https://badges.gitter.im/Join%20Chat.svg + :alt: Join Chat + :target: https://gitter.im/jquast/blessed + Introduction ============ @@ -126,8 +130,8 @@ The same program with *Blessed* is simply:: Requirements ------------ -Blessed is compatible with Python 2.7, 3.4, and 3.5 on Debian Linux, Mac OSX, -and FreeBSD. +Blessed is tested with Python 2.7, 3.4, and 3.5 on Debian Linux, Mac, and +FreeBSD. Further Documentation --------------------- diff --git a/docs/overview.rst b/docs/overview.rst index 6d54be84..fd637b52 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -200,29 +200,8 @@ all this mashing. This compound notation comes in handy if you want to allow users to customize formatting, just allow compound formatters, like *bold_green*, as a command -line argument or configuration item:: - - #!/usr/bin/env python - import argparse - from blessed import Terminal - - parser = argparse.ArgumentParser( - description='displays argument as specified style') - parser.add_argument('style', type=str, help='style formatter') - parser.add_argument('text', type=str, nargs='+') - - term = Terminal() - - args = parser.parse_args() - - style = getattr(term, args.style) - - print(style(' '.join(args.text))) - -Saved as **tprint.py**, this could be used like:: - - $ ./tprint.py bright_blue_reverse Blue Skies - +line argument or configuration item such as in the :ref:`tprint.py` +demonstration script. Moving The Cursor ----------------- @@ -239,6 +218,25 @@ When you want to move the cursor, you have a few choices: arguments in order *(y, x)*. Please use keyword arguments as a later release may correct the argument order of :meth:`~.Terminal.location`. +Finding The Cursor +------------------ + +We can determine the cursor's current position at anytime using +:meth:`~.get_location`, returning the current (y, x) location. This uses a +kind of "answer back" sequence that your terminal emulator responds to. If +the terminal may not respond, the :paramref:`~.get_location.timeout` keyword +argument can be specified to return coordinates (-1, -1) after a blocking +timeout:: + + from blessed import Terminal + + term = Terminal() + + row, col = term.get_location(timeout=5) + + if row < term.height: + print(term.move_y(term.height) + 'Get down there!') + Moving Temporarily ~~~~~~~~~~~~~~~~~~ @@ -519,8 +517,8 @@ Its output might appear as:: A :paramref:`~.Terminal.inkey.timeout` value of *None* (default) will block forever until a keypress is received. Any other value specifies the length of time to poll for input: if no input is received after the given time has -elapsed, an empty string is returned. A -:paramref:`~.Terminal.inkey.timeout` value of *0* is non-blocking. +elapsed, an empty string is returned. A :paramref:`~.Terminal.inkey.timeout` +value of *0* is non-blocking. keyboard codes ~~~~~~~~~~~~~~ diff --git a/docs/pains.rst b/docs/pains.rst index a44ce574..00e6d1ad 100644 --- a/docs/pains.rst +++ b/docs/pains.rst @@ -251,43 +251,10 @@ emulators? `_ by `Markus Kuhn `_ of the University of Cambridge. -Detecting multibyte -~~~~~~~~~~~~~~~~~~~ - One can be assured that the connecting client is capable of representing UTF-8 and other multibyte character encodings by the Environment variable -``LANG``. If this is not possible, there is an alternative method: - - - Emit Report Cursor Position (CPR), ``\x1b[6n`` and store response. - - Emit a multibyte UTF-8 character, such as ⦰ (``\x29\xb0``). - - Emit Report Cursor Position (CPR), ``\x1b[6n`` and store response. - - Determine the difference of the *(y, x)* location of the response. - If it is *1*, then the client decoded the two UTF-8 bytes as a - single character, and can be considered capable. If it is *2*, - the client is using a `code page`_ and is incapable of decoding - a UTF-8 bytestream. - -Note that both SSH and Telnet protocols provide means for forwarding -the ``LANG`` environment variable. However, some transports such as -a link by serial cable is incapable of forwarding Environment variables. - -Detecting screen size -~~~~~~~~~~~~~~~~~~~~~ - -While we're on the subject, there are times when :attr:`height` and -:attr:`width` are not accurate -- when a transport does not provide the means -to propagate the COLUMNS and ROWS Environment values, or propagate the -SIGWINCH signals, such as through a serial link. - -The same means described above for multibyte encoding detection may be used to -detect the remote client's window size: - - - Move cursor to row 999, 999. - - Emit Report Cursor Position (CPR), ``\x1b[6n`` and store response. - - The return value is the window dimensions of the client. - -This is the method used by the program ``resize`` provided in the Xorg -distribution, and its source may be viewed as file `resize.c`_. +``LANG``. If this is not possible or reliable, there is an intrusive detection +method demonstrated in the example program :ref:`detect-multibyte.py`. Alt or meta sends Escape ------------------------ @@ -371,6 +338,5 @@ When people say 'ANSI Sequence', they are discussing: .. _f.lux: https://justgetflux.com/ .. _ZX Spectrum: https://en.wikipedia.org/wiki/List_of_8-bit_computer_hardware_palettes#ZX_Spectrum .. _Control-Sequence-Inducer: http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Controls-beginning-with-ESC -.. _resize.c: http://www.opensource.apple.com/source/X11apps/X11apps-13/xterm/xterm-207/resize.c .. _ANSI.SYS: http://www.kegel.com/nansi/ .. _ECMA-48: http://www.ecma-international.org/publications/standards/Ecma-048.htm diff --git a/version.json b/version.json index c16d5563..ec1cdfd4 100644 --- a/version.json +++ b/version.json @@ -1 +1 @@ -{"version": "1.11.0"} +{"version": "1.12.0"} From 6670d4ed12d3b58f1c954b4fec6b23e0b615be53 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Tue, 13 Oct 2015 13:55:49 -0700 Subject: [PATCH 306/459] bugfix hyperlink rst url --- CONTRIBUTING.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index c4d49cb8..25b16893 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -30,7 +30,7 @@ Install and run tox Py.test is used as the test runner, supporting positional arguments, you may for example use `looponfailing -` +`_ with python 3.5, stopping at the first failing test case, and looping (retrying) after a filesystem save is detected:: From 6e0f6534bd4efe32f16ea5154fee5da8948eff45 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Tue, 13 Oct 2015 13:57:18 -0700 Subject: [PATCH 307/459] correct change log --- docs/history.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/history.rst b/docs/history.rst index 64800382..56640df0 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -4,7 +4,7 @@ Version History * enhancement: :meth:`~.Terminal.get_location` returns the ``(row, col)`` position of the cursor at the time of call for attached terminal. * enhancement: a keyboard now detected as *stdin* when - :paramref:`~.Terminal.__init__.stream` is :obj:`sys.stdin`. + :paramref:`~.Terminal.__init__.stream` is :obj:`sys.stderr`. 1.11 * enhancement: :meth:`~.Terminal.inkey` can return more quickly for From 549234af1960786e2425d18054d6de2aaa3bfa8d Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Wed, 14 Oct 2015 10:45:52 -0700 Subject: [PATCH 308/459] chmod +x bin/detect-multibyte.py --- bin/detect-multibyte.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 bin/detect-multibyte.py diff --git a/bin/detect-multibyte.py b/bin/detect-multibyte.py old mode 100644 new mode 100755 From 674e99023661ffc8a3793ac105f96ff9ccb2c0a2 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Wed, 14 Oct 2015 13:54:37 -0700 Subject: [PATCH 309/459] ensure 'six' module is provided in deps= --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 0110faf9..3441cf23 100644 --- a/tox.ini +++ b/tox.ini @@ -19,7 +19,7 @@ commands = {envbindir}/py.test \ # CI buildchain target [testenv:coverage] -deps = coverage +deps = coverage six commands = {toxinidir}/tools/custom-combine.py # CI buildhcain target From d0e2e42e9c35c51aaf913cb62a2c7e73a574429d Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Wed, 14 Oct 2015 16:03:39 -0700 Subject: [PATCH 310/459] bugfix last commit --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 3441cf23..6ccb211f 100644 --- a/tox.ini +++ b/tox.ini @@ -19,7 +19,8 @@ commands = {envbindir}/py.test \ # CI buildchain target [testenv:coverage] -deps = coverage six +deps = coverage + six commands = {toxinidir}/tools/custom-combine.py # CI buildhcain target From 6c36099af1c9a352d2d181f1e1cb5f08e8727a88 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Wed, 14 Oct 2015 16:17:22 -0700 Subject: [PATCH 311/459] use svg and https for teamcity badge --- docs/intro.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/intro.rst b/docs/intro.rst index 640e3032..b4a4b426 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -2,7 +2,7 @@ :alt: Travis Continuous Integration :target: https://travis-ci.org/jquast/blessed/ -.. image:: https://img.shields.io/teamcity/http/teamcity-master.pexpect.org/s/Blessed_BuildHead.png +.. image:: https://img.shields.io/teamcity/https/teamcity-master.pexpect.org/s/Blessed_BuildHead.svg :alt: TeamCity Build status :target: https://teamcity-master.pexpect.org/viewType.html?buildTypeId=Blessed_BuildHead&branch_Blessed=%3Cdefault%3E&tab=buildTypeStatusDiv From 143a8d2e2ec4b2e8c40669206ed2f9eff453ef88 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 16 Oct 2015 13:15:09 -0700 Subject: [PATCH 312/459] polish contributing file --- CONTRIBUTING.rst | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 25b16893..55f05460 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -14,8 +14,8 @@ Prepare a developer environment. Then, from the blessed code folder:: pip install --editable . -Any changes made are automatically made available to the python interpreter -matching pip as the 'blessed' module path irregardless of the current working +Any changes made in this project folder are then made available to the python +interpreter as the 'telnetlib3' module irregardless of the current working directory. Running Tests @@ -56,12 +56,7 @@ static analysis tools through the **sa** target, invoked using:: tox -esa -Similarly, positional arguments can be used, for example to verify URL -links:: - - tox -esa -- -blinkcheck - -All standards enforced by the underlying tools are adhered to by the blessed -project, with the declarative exception of those found in `landscape.yml +All standards enforced by the underlying style checker tools are adhered to, +with the declarative exception of those found in `landscape.yml `_, or inline using ``pylint: disable=`` directives. From ea3bb3b0a5d536db7342a5df06e45bd57ba28259 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Sat, 24 Oct 2015 14:21:16 -0400 Subject: [PATCH 313/459] fix copied docs and grammar --- CONTRIBUTING.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 55f05460..fd54bd79 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -15,7 +15,7 @@ Prepare a developer environment. Then, from the blessed code folder:: pip install --editable . Any changes made in this project folder are then made available to the python -interpreter as the 'telnetlib3' module irregardless of the current working +interpreter as the 'blessed' package regardless of the current working directory. Running Tests From 53d4e277c5627cc191c1f31c0436bf0846b5f8c9 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Sat, 24 Oct 2015 18:38:46 -0400 Subject: [PATCH 314/459] change docstring param name to name used in code --- blessed/sequences.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blessed/sequences.py b/blessed/sequences.py index 0c6d0a6a..c94c08da 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -56,7 +56,7 @@ def _build_numeric_capability(term, cap, optional=False, :arg blessed.Terminal term: :class:`~.Terminal` instance. :arg str cap: terminal capability name. - :arg int num: the numeric to use for parameterized capability. + :arg int base_num: the numeric to use for parameterized capability. :arg int nparams: the number of parameters to use for capability. :rtype: str :returns: regular expression for the given capability. From 98dcc304415858c23d89410cbcd6510117575151 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Sat, 24 Oct 2015 18:35:46 -0400 Subject: [PATCH 315/459] iter_parse * iter_parse replaces three similar pieces of code in sequences.py * in addition to master regexes for will_move and wont_move sequences, cap, regex pairs are generated * there are now two different places where sequences are identified: identify_fragment() identifies a sequence while measure_length() reports the length of a sequence. These aren't combined because identify_fragment() is probably a lot slower. * namedtuple TextFragment(ucs, is_sequence, capname, params) --- blessed/sequences.py | 284 ++++++++++++++++++++------------ blessed/terminal.py | 4 + blessed/tests/test_sequences.py | 6 +- 3 files changed, 186 insertions(+), 108 deletions(-) diff --git a/blessed/sequences.py b/blessed/sequences.py index 0c6d0a6a..ba8d744d 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -2,11 +2,12 @@ """This module provides 'sequence awareness'.""" # std imports +import collections import functools -import textwrap -import warnings import math import re +import textwrap +import warnings # local from blessed._binterms import BINARY_TERMINALS, BINTERM_UNSUPPORTED_MSG @@ -15,7 +16,10 @@ import wcwidth import six -__all__ = ('init_sequence_patterns', 'Sequence', 'SequenceTextWrapper',) +__all__ = ('init_sequence_patterns', 'Sequence', 'SequenceTextWrapper', 'iter_parse') + + +TextFragment = collections.namedtuple('TextFragment', ['ucs', 'is_sequence', 'capname', 'params']) def _sort_sequences(regex_seqlist): @@ -40,7 +44,9 @@ def _sort_sequences(regex_seqlist): # types, it is feasible. # pylint: disable=bad-builtin # Used builtin function 'filter' - return sorted(list(filter(None, regex_seqlist)), key=len, reverse=True) + for el in regex_seqlist: + assert isinstance(el, tuple) + return sorted(list(filter(lambda x: x and x[1], regex_seqlist)), key=lambda x: len(x[1]), reverse=True) def _build_numeric_capability(term, cap, optional=False, @@ -71,7 +77,7 @@ def _build_numeric_capability(term, cap, optional=False, if str(num) in cap_re: # modify & return n to matching digit expression cap_re = cap_re.replace(str(num), r'(\d+){0}'.format(opt)) - return cap_re + return cap, cap_re warnings.warn('Unknown parameter in {0!r}, {1!r}: {2!r})' .format(cap, _cap, cap_re), UserWarning) return None # no such capability @@ -96,12 +102,21 @@ def _build_any_numeric_capability(term, cap, num=99, nparams=1): cap_re = re.escape(_cap(*((num,) * nparams))) cap_re = re.sub(r'(\d+)', r'(\d+)', cap_re) if r'(\d+)' in cap_re: - return cap_re + return cap, cap_re warnings.warn('Missing numerics in {0!r}: {1!r}' .format(cap, cap_re), UserWarning) return None # no such capability +def _build_simple_capability(term, cap): + """Build terminal capability for capname""" + _cap = getattr(term, cap) + if _cap: + return cap, re.escape(_cap) + return None + + + def get_movement_sequence_patterns(term): """ Get list of regular expressions for sequences that cause movement. @@ -110,10 +125,11 @@ def get_movement_sequence_patterns(term): :rtype: list """ bnc = functools.partial(_build_numeric_capability, term) + bsc = functools.partial(_build_simple_capability, term) return set(filter(None, [ # carriage_return - re.escape(term.cr), + bsc(cap='cr'), # column_address: Horizontal position, absolute bnc(cap='hpa'), # row_address: Vertical position #1 absolute @@ -121,28 +137,28 @@ def get_movement_sequence_patterns(term): # cursor_address: Move to row #1 columns #2 bnc(cap='cup', nparams=2), # cursor_down: Down one line - re.escape(term.cud1), + bsc(cap='ud1'), # cursor_home: Home cursor (if no cup) - re.escape(term.home), + bsc(cap='home'), # cursor_left: Move left one space - re.escape(term.cub1), + bsc(cap='cub1'), # cursor_right: Non-destructive space (move right one space) - re.escape(term.cuf1), + bsc(cap='cuf1'), # cursor_up: Up one line - re.escape(term.cuu1), + bsc(cap='cuu1'), # param_down_cursor: Down #1 lines bnc(cap='cud', optional=True), # restore_cursor: Restore cursor to position of last save_cursor - re.escape(term.rc), + bsc(cap='rc'), # clear_screen: clear screen and home cursor - re.escape(term.clear), + bsc(cap='clear'), # enter/exit_fullscreen: switch to alternate screen buffer - re.escape(term.enter_fullscreen), - re.escape(term.exit_fullscreen), + bsc(cap='enter_fullscreen'), + bsc(cap='exit_fullscreen'), # forward cursor - term._cuf, + ('_cuf', term._cuf), # backward cursor - term._cub, + ('_cub', term._cub), ])) @@ -155,90 +171,92 @@ def get_wontmove_sequence_patterns(term): """ bnc = functools.partial(_build_numeric_capability, term) bna = functools.partial(_build_any_numeric_capability, term) + bsc = functools.partial(_build_simple_capability, term) + # pylint: disable=bad-builtin # Used builtin function 'map' return set(filter(None, [ # print_screen: Print contents of screen - re.escape(term.mc0), + bsc(cap='mc0'), # prtr_off: Turn off printer - re.escape(term.mc4), + bsc(cap='mc4'), # prtr_on: Turn on printer - re.escape(term.mc5), + bsc(cap='mc5'), # save_cursor: Save current cursor position (P) - re.escape(term.sc), + bsc(cap='sc'), # set_tab: Set a tab in every row, current columns - re.escape(term.hts), + bsc(cap='hts'), # enter_bold_mode: Turn on bold (extra bright) mode - re.escape(term.bold), + bsc(cap='bold'), # enter_standout_mode - re.escape(term.standout), + bsc(cap='standout'), # enter_subscript_mode - re.escape(term.subscript), + bsc(cap='subscript'), # enter_superscript_mode - re.escape(term.superscript), + bsc(cap='superscript'), # enter_underline_mode: Begin underline mode - re.escape(term.underline), + bsc(cap='underline'), # enter_blink_mode: Turn on blinking - re.escape(term.blink), + bsc(cap='blink'), # enter_dim_mode: Turn on half-bright mode - re.escape(term.dim), + bsc(cap='dim'), # cursor_invisible: Make cursor invisible - re.escape(term.civis), + bsc(cap='civis'), # cursor_visible: Make cursor very visible - re.escape(term.cvvis), + bsc(cap='cvvis'), # cursor_normal: Make cursor appear normal (undo civis/cvvis) - re.escape(term.cnorm), + bsc(cap='cnorm'), # clear_all_tabs: Clear all tab stops - re.escape(term.tbc), + bsc(cap='tbc'), # change_scroll_region: Change region to line #1 to line #2 bnc(cap='csr', nparams=2), # clr_bol: Clear to beginning of line - re.escape(term.el1), + bsc(cap='el1'), # clr_eol: Clear to end of line - re.escape(term.el), + bsc(cap='el'), # clr_eos: Clear to end of screen - re.escape(term.clear_eos), + bsc(cap='clear_eos'), # delete_character: Delete character - re.escape(term.dch1), + bsc(cap='dch1'), # delete_line: Delete line (P*) - re.escape(term.dl1), + bsc(cap='dl1'), # erase_chars: Erase #1 characters bnc(cap='ech'), # insert_line: Insert line (P*) - re.escape(term.il1), + bsc(cap='il1'), # parm_dch: Delete #1 characters bnc(cap='dch'), # parm_delete_line: Delete #1 lines bnc(cap='dl'), # exit_alt_charset_mode: End alternate character set (P) - re.escape(term.rmacs), + bsc(cap='rmacs'), # exit_am_mode: Turn off automatic margins - re.escape(term.rmam), + bsc(cap='rmam'), # exit_attribute_mode: Turn off all attributes - re.escape(term.sgr0), + bsc(cap='sgr0'), # exit_ca_mode: Strings to end programs using cup - re.escape(term.rmcup), + bsc(cap='rmcup'), # exit_insert_mode: Exit insert mode - re.escape(term.rmir), + bsc(cap='rmir'), # exit_standout_mode: Exit standout mode - re.escape(term.rmso), + bsc(cap='rmso'), # exit_underline_mode: Exit underline mode - re.escape(term.rmul), + bsc(cap='rmul'), # flash_hook: Flash switch hook - re.escape(term.hook), + bsc(cap='hook'), # flash_screen: Visible bell (may not move cursor) - re.escape(term.flash), + bsc(cap='flash'), # keypad_local: Leave 'keyboard_transmit' mode - re.escape(term.rmkx), + bsc(cap='rmkx'), # keypad_xmit: Enter 'keyboard_transmit' mode - re.escape(term.smkx), + bsc(cap='smkx'), # meta_off: Turn off meta mode - re.escape(term.rmm), + bsc(cap='rmm'), # meta_on: Turn on meta mode (8th-bit on) - re.escape(term.smm), + bsc(cap='smm'), # orig_pair: Set default pair to its original value - re.escape(term.op), + bsc(cap='op'), # parm_ich: Insert #1 characters bnc(cap='ich'), # parm_index: Scroll forward #1 @@ -252,11 +270,11 @@ def get_wontmove_sequence_patterns(term): # parm_up_cursor: Up #1 lines bnc(cap='cuu'), # scroll_forward: Scroll text up (P) - re.escape(term.ind), + bsc(cap='ind'), # scroll_reverse: Scroll text down (P) - re.escape(term.rev), + bsc(cap='rev'), # tab: Tab to next 8-space hardware tab stop - re.escape(term.ht), + bsc(cap='ht'), # set_a_background: Set background color to #1, using ANSI escape bna(cap='setab', num=1), bna(cap='setab', num=(term.number_of_colors - 1)), @@ -268,7 +286,7 @@ def get_wontmove_sequence_patterns(term): # ( not *exactly* legal, being extra forgiving. ) bna(cap='sgr', nparams=_num) for _num in range(1, 10) # reset_{1,2,3}string: Reset string - ] + list(map(re.escape, (term.r1, term.r2, term.r3,))))) + ] + list(map(bsc, ('r1', 'r2', 'r3'))))) def init_sequence_patterns(term): @@ -339,36 +357,38 @@ def init_sequence_patterns(term): # recognize blue_on_red, even if it didn't cause it to be # generated. These are final "ok, i will match this, anyway" for # basic SGR sequences. - re.escape(u'\x1b') + r'\[(\d+)\;(\d+)\;(\d+)\;(\d+)m', - re.escape(u'\x1b') + r'\[(\d+)\;(\d+)\;(\d+)m', - re.escape(u'\x1b') + r'\[(\d+)\;(\d+)m', - re.escape(u'\x1b') + r'\[(\d+)?m', - re.escape(u'\x1b(B'), + ('?', re.escape(u'\x1b') + r'\[(\d+)\;(\d+)\;(\d+)\;(\d+)m'), + ('?', re.escape(u'\x1b') + r'\[(\d+)\;(\d+)\;(\d+)m'), + ('?', re.escape(u'\x1b') + r'\[(\d+)\;(\d+)m'), + ('?', re.escape(u'\x1b') + r'\[(\d+)?m'), + ('?', re.escape(u'\x1b(B')), ] # compile as regular expressions, OR'd. - _re_will_move = re.compile(u'({0})'.format(u'|'.join(_will_move))) - _re_wont_move = re.compile(u'({0})'.format(u'|'.join(_wont_move))) + _re_will_move = re.compile(u'({0})'.format(u'|'.join(s for c, s in _will_move if s))) + _re_wont_move = re.compile(u'({0})'.format(u'|'.join(s for c, s in _wont_move if s))) # static pattern matching for horizontal_distance(ucs, term) bnc = functools.partial(_build_numeric_capability, term) # parm_right_cursor: Move #1 characters to the right - _cuf = bnc(cap='cuf', optional=True) - _re_cuf = re.compile(_cuf) if _cuf else None + _cuf_pair = bnc(cap='cuf', optional=True) + _re_cuf = re.compile(_cuf_pair[1]) if _cuf_pair else None # cursor_right: Non-destructive space (move right one space) _cuf1 = term.cuf1 # parm_left_cursor: Move #1 characters to the left - _cub = bnc(cap='cub', optional=True) - _re_cub = re.compile(_cub) if _cub else None + _cub_pair = bnc(cap='cub', optional=True) + _re_cub = re.compile(_cub_pair[1]) if _cub_pair else None # cursor_left: Move left one space _cub1 = term.cub1 return {'_re_will_move': _re_will_move, '_re_wont_move': _re_wont_move, + '_will_move': _will_move, + '_wont_move': _wont_move, '_re_cuf': _re_cuf, '_re_cub': _re_cub, '_cuf1': _cuf1, @@ -452,24 +472,17 @@ def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width): # If we're allowed to break long words, then do so: put as much # of the next chunk onto the current line as will fit. + if self.break_long_words: term = self.term chunk = reversed_chunks[-1] - nxt = 0 - for idx in range(0, len(chunk)): - if idx == nxt: - # at sequence, point beyond it, - nxt = idx + measure_length(chunk[idx:], term) - if nxt <= idx: - # point beyond next sequence, if any, - # otherwise point to next character - nxt = idx + measure_length(chunk[idx:], term) + 1 - if Sequence(chunk[:nxt], term).length() > space_left: + for idx, part in i_and_split_nonsequences(iter_parse(term, chunk)): + if Sequence(chunk[:idx + len(part.ucs)], term).length() > space_left: break else: # our text ends with a sequence, such as in text # u'!\x1b(B\x1b[m', set index at at end (nxt) - idx = nxt + idx = idx + len(part.ucs) cur_line.append(chunk[:idx]) reversed_chunks[-1] = chunk[idx:] @@ -638,19 +651,8 @@ def strip_seqs(self): # nxt: points to first character beyond current escape sequence. # width: currently estimated display length. inp = self.padd() - outp = u'' - nxt = 0 - for idx in range(0, len(inp)): - if idx == nxt: - # at sequence, point beyond it, - nxt = idx + measure_length(inp[idx:], self._term) - if nxt <= idx: - # append non-sequence to outp, - outp += inp[idx] - # point beyond next sequence, if any, - # otherwise point to next character - nxt = idx + measure_length(inp[idx:], self._term) + 1 - return outp + return ''.join(f.ucs for f in iter_parse(self._term, inp) if not f.is_sequence) + # this implementation is slow because we have to build the sequences def padd(self): r""" @@ -674,18 +676,19 @@ def padd(self): detected, those last-most characters are destroyed. """ outp = u'' - nxt = 0 - for idx in range(0, six.text_type.__len__(self)): - width = horizontal_distance(self[idx:], self._term) - if width != 0: - nxt = idx + measure_length(self[idx:], self._term) - if width > 0: - outp += u' ' * width - elif width < 0: - outp = outp[:width] - if nxt <= idx: - outp += self[idx] - nxt = idx + 1 + for ucs, is_sequence, _, _ in iter_parse(self._term, self): + width = horizontal_distance(ucs, self._term) + #TODO this fails on a tab in a test + #assert not (width and not is_sequence), ( + # 'only sequences should have non-zero width, but nonsequence ' + + # repr((ucs, is_sequence)) + ' has width of ' + str(width) + + # ' while parsing ' + repr(self)) + if width > 0: + outp += u' ' * width + elif width < 0: + outp = outp[:width] + else: + outp += ucs return outp @@ -731,6 +734,7 @@ def measure_length(ucs, term): if matching_seq: _, end = matching_seq.span() + assert identify_fragment(term, ucs[:end]) return end # none found, must be printable! @@ -816,3 +820,73 @@ def horizontal_distance(ucs, term): return (termcap_distance(ucs, 'cub', -1, term) or termcap_distance(ucs, 'cuf', 1, term) or 0) + + +def identify_fragment(term, ucs): + # simple terminal control characters, + ctrl_seqs = u'\a\b\r\n\x0e\x0f' + + if any([ucs.startswith(_ch) for _ch in ctrl_seqs]): + return TextFragment(ucs[:1], True, '?', None) + + ret = term and ( + regex_or_match(term._will_move, ucs) or + regex_or_match(term._wont_move, ucs) + ) + if ret: + return ret + + # known multibyte sequences, + matching_seq = term and ( + term._re_cub and term._re_cub.match(ucs) or + term._re_cuf and term._re_cuf.match(ucs) + ) + + if matching_seq: + return TextFragment(matching_seq.group(), True, '?', None) + + raise ValueError("not a sequence!: "+repr(ucs)) + + +def regex_or_match(patterns, ucs): + for cap, pat in patterns: + matching_seq = re.match(pat, ucs) + if matching_seq is None: + continue + params = matching_seq.groups()[1:] + return TextFragment(matching_seq.group(), True, cap, params if params else None) + return None + +def i_and_split_nonsequences(fragments): + """Yields simple unicode characters or sequences with index""" + idx = 0 + for part in fragments: + if part.is_sequence: + yield idx, part + idx += len(part.ucs) + else: + for c in part.ucs: + yield idx, TextFragment(c, False, None, None) + idx += 1 + +def iter_parse(term, ucs): + r""" + + + """ + outp = u'' + nxt = 0 + for idx in range(0, six.text_type.__len__(ucs)): + if idx == nxt: + l = measure_length(ucs[idx:], term) + if l == 0: + outp += ucs[idx] + nxt += 1 + else: + if outp: + yield TextFragment(outp, False, None, None) + outp = u'' + yield identify_fragment(term, ucs[idx:idx+l]) + nxt += l + if outp: + yield TextFragment(outp, False, None, None) diff --git a/blessed/terminal.py b/blessed/terminal.py index 51ae9cc1..944c81e2 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -49,6 +49,7 @@ _build_any_numeric_capability, SequenceTextWrapper, Sequence, + iter_parse, ) from .keyboard import (get_keyboard_sequences, @@ -1082,6 +1083,9 @@ def inkey(self, timeout=None, esc_delay=0.35, **_kwargs): self.ungetch(ucs[len(ks):]) return ks + def iter_parse(self, text): + return iter_parse(self, text) + class WINSZ(collections.namedtuple('WINSZ', ( 'ws_row', 'ws_col', 'ws_xpixel', 'ws_ypixel'))): diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index ae71ed29..81d165b2 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -144,11 +144,11 @@ def test_unit_binpacked_unittest(): warnings.resetwarnings() -def test_sort_sequences(): +def test_sort_sequence_pairs(): """Test sequences are filtered and ordered longest-first.""" from blessed.sequences import _sort_sequences - input_list = [u'a', u'aa', u'aaa', u''] - output_expected = [u'aaa', u'aa', u'a'] + input_list = [(1, u'a'), (2, u'aa'), (3, u'aaa'), (4, u'')] + output_expected = [(3, u'aaa'), (2, u'aa'), (1, u'a')] assert (_sort_sequences(input_list) == output_expected) From c8b47be92fbc2727070c6790650aa965070452d5 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Mon, 26 Oct 2015 17:59:05 -0400 Subject: [PATCH 316/459] remove debugging assert --- blessed/sequences.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/blessed/sequences.py b/blessed/sequences.py index ba8d744d..65e84fc1 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -44,8 +44,6 @@ def _sort_sequences(regex_seqlist): # types, it is feasible. # pylint: disable=bad-builtin # Used builtin function 'filter' - for el in regex_seqlist: - assert isinstance(el, tuple) return sorted(list(filter(lambda x: x and x[1], regex_seqlist)), key=lambda x: len(x[1]), reverse=True) From 80fd8a029fddcba6caf5d6dfaba101a56df5a411 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Wed, 28 Oct 2015 16:25:01 -0700 Subject: [PATCH 317/459] Minor docfix about a (tough!) challenge --- bin/detect-multibyte.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bin/detect-multibyte.py b/bin/detect-multibyte.py index 18930c1c..c014d979 100755 --- a/bin/detect-multibyte.py +++ b/bin/detect-multibyte.py @@ -20,9 +20,10 @@ If the horizontal distance of (p0, p1) is 1 cell, we know the connecting client is certainly matching our intended encoding. -As an exercise, it may be possible to use this technique to accurately -determine to the remote encoding without protocol negotiation using cursor -positioning alone, as demonstrated by the following diagram, +As a (tough!) exercise, it may be possible to use this technique to accurately +determine the remote encoding without protocol negotiation using cursor +positioning alone through a complex state decision tree, as demonstrated +by the following diagram: .. image:: _static/soulburner-ru-family-encodings.jpg :alt: Cyrillic encodings flowchart From 0928a0abe266ea0453168f117ec83a8f81734c8e Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Wed, 28 Oct 2015 16:25:17 -0700 Subject: [PATCH 318/459] make sure travis-ci build points to master --- docs/intro.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/intro.rst b/docs/intro.rst index b4a4b426..8eb2d3e3 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -1,4 +1,4 @@ -.. image:: https://img.shields.io/travis/jquast/blessed.svg +.. image:: https://img.shields.io/travis/jquast/blessed/master.svg :alt: Travis Continuous Integration :target: https://travis-ci.org/jquast/blessed/ From 1cd63781de3387f128f7361f5c8d67281c7b54e2 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Fri, 30 Oct 2015 09:41:54 -0400 Subject: [PATCH 319/459] small changes --- blessed/sequences.py | 195 ++++++++++++++++++++++++++++++++----------- 1 file changed, 147 insertions(+), 48 deletions(-) diff --git a/blessed/sequences.py b/blessed/sequences.py index 65e84fc1..7399f34e 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -16,10 +16,35 @@ import wcwidth import six -__all__ = ('init_sequence_patterns', 'Sequence', 'SequenceTextWrapper', 'iter_parse') +__all__ = ('init_sequence_patterns', + 'Sequence', + 'SequenceTextWrapper', + 'iter_parse') -TextFragment = collections.namedtuple('TextFragment', ['ucs', 'is_sequence', 'capname', 'params']) +class TextPart(collections.namedtuple('TextPart', ( + 'ucs', 'is_sequence', 'name', 'params'))): + """ + Describes either a terminal sequence or a series of printable characters. + + .. py:attribute:: ucs + + ucs str of terminal sequence or printable characters + + .. py:attribute:: is_sequence + + bool of whether this is a terminal sequence + + .. py:attribute:: name + + str of capability name or descriptive name of the terminal + sequence, or None if not a terminal sequence + + .. py:attribute:: params + + a tuple of str parameters in the terminal sequence, + or None if not a terminal sequence + """ def _sort_sequences(regex_seqlist): @@ -42,15 +67,27 @@ def _sort_sequences(regex_seqlist): # typically occur for output sequences, though with so many # programmatically generated regular expressions for so many terminal # types, it is feasible. - # pylint: disable=bad-builtin - # Used builtin function 'filter' - return sorted(list(filter(lambda x: x and x[1], regex_seqlist)), key=lambda x: len(x[1]), reverse=True) + return sorted((x for x in regex_seqlist if x and x[1]), + key=lambda x: len(x[1]), + reverse=True) + +def _differentiate_duplicate_sequences(regex_seqlist): + """ + annotate with unique ids per list + """ + counter = collections.Counter() + def inc_get(key): + counter[key] += 1 + return counter[key] + + return [(u"{}_{}".format(name, inc_get(name)), pattern) + for name, pattern in regex_seqlist] def _build_numeric_capability(term, cap, optional=False, base_num=99, nparams=1): r""" - Return regular expression for capabilities containing specified digits. + Return regular expressions for capabilities containing specified digits. This differs from function :func:`_build_any_numeric_capability` in that, for the given ``base_num`` and ``nparams``, the value of @@ -62,8 +99,11 @@ def _build_numeric_capability(term, cap, optional=False, :arg str cap: terminal capability name. :arg int num: the numeric to use for parameterized capability. :arg int nparams: the number of parameters to use for capability. - :rtype: str - :returns: regular expression for the given capability. + :rtype: tuple + :returns: tuple of + - str name of capture group used in identifying regular expression + - str regular expression for extracting the params of a capability + """ _cap = getattr(term, cap) opt = '?' if optional else '' @@ -89,8 +129,10 @@ def _build_any_numeric_capability(term, cap, num=99, nparams=1): :arg str cap: terminal capability name. :arg int num: the numeric to use for parameterized capability. :arg int nparams: the number of parameters to use for capability. - :rtype: str - :returns: regular expression for the given capability. + :rtype: tuple + :returns: tuple of + - str name of capture group used in + - str regular expression for extracting the params of a capability Build regular expression from capabilities having *any* digit parameters: substitute any matching ``\d`` with literal ``\d`` and return. @@ -107,14 +149,21 @@ def _build_any_numeric_capability(term, cap, num=99, nparams=1): def _build_simple_capability(term, cap): - """Build terminal capability for capname""" + r""" + Return regular expression for capabilities which do not take parameters. + + :arg blessed.Terminal term: :class:`~.Terminal` instance. + :arg str cap: terminal capability name. + :rtype: str + :returns: regular expression for the given capability. + """ _cap = getattr(term, cap) if _cap: - return cap, re.escape(_cap) + cap_re = re.escape(_cap) + return cap, cap_re return None - def get_movement_sequence_patterns(term): """ Get list of regular expressions for sequences that cause movement. @@ -122,10 +171,11 @@ def get_movement_sequence_patterns(term): :arg blessed.Terminal term: :class:`~.Terminal` instance. :rtype: list """ + bnc = functools.partial(_build_numeric_capability, term) bsc = functools.partial(_build_simple_capability, term) - return set(filter(None, [ + return list(filter(None, [ # carriage_return bsc(cap='cr'), # column_address: Horizontal position, absolute @@ -171,10 +221,9 @@ def get_wontmove_sequence_patterns(term): bna = functools.partial(_build_any_numeric_capability, term) bsc = functools.partial(_build_simple_capability, term) - # pylint: disable=bad-builtin # Used builtin function 'map' - return set(filter(None, [ + return list(filter(None, [ # print_screen: Print contents of screen bsc(cap='mc0'), # prtr_off: Turn off printer @@ -336,15 +385,16 @@ def init_sequence_patterns(term): if term.kind in BINARY_TERMINALS: warnings.warn(BINTERM_UNSUPPORTED_MSG.format(term.kind)) + # Build will_move, a list of terminal capabilities that have # indeterminate effects on the terminal cursor position. - _will_move = set() + _will_move = [] if term.does_styling: _will_move = _sort_sequences(get_movement_sequence_patterns(term)) # Build wont_move, a list of terminal capabilities that mainly affect # video attributes, for use with measure_length(). - _wont_move = set() + _wont_move = [] if term.does_styling: _wont_move = _sort_sequences(get_wontmove_sequence_patterns(term)) _wont_move += [ @@ -355,11 +405,11 @@ def init_sequence_patterns(term): # recognize blue_on_red, even if it didn't cause it to be # generated. These are final "ok, i will match this, anyway" for # basic SGR sequences. - ('?', re.escape(u'\x1b') + r'\[(\d+)\;(\d+)\;(\d+)\;(\d+)m'), - ('?', re.escape(u'\x1b') + r'\[(\d+)\;(\d+)\;(\d+)m'), - ('?', re.escape(u'\x1b') + r'\[(\d+)\;(\d+)m'), - ('?', re.escape(u'\x1b') + r'\[(\d+)?m'), - ('?', re.escape(u'\x1b(B')), + ('sgr4', re.escape(u'\x1b') + r'\[(\d+)\;(\d+)\;(\d+)\;(\d+)m'), + ('sgr3', re.escape(u'\x1b') + r'\[(\d+)\;(\d+)\;(\d+)m'), + ('sgr2', re.escape(u'\x1b') + r'\[(\d+)\;(\d+)m'), + ('sgr1', re.escape(u'\x1b') + r'\[(\d+)?m'), + ('sgr0', re.escape(u'\x1b(B')), ] # compile as regular expressions, OR'd. @@ -474,8 +524,9 @@ def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width): if self.break_long_words: term = self.term chunk = reversed_chunks[-1] - for idx, part in i_and_split_nonsequences(iter_parse(term, chunk)): - if Sequence(chunk[:idx + len(part.ucs)], term).length() > space_left: + for idx, part in enumerate_by_position(iter_parse(term, chunk)): + nxt = idx + len(part.ucs) + if Sequence(chunk[:nxt], term).length() > space_left: break else: # our text ends with a sequence, such as in text @@ -649,8 +700,9 @@ def strip_seqs(self): # nxt: points to first character beyond current escape sequence. # width: currently estimated display length. inp = self.padd() - return ''.join(f.ucs for f in iter_parse(self._term, inp) if not f.is_sequence) - # this implementation is slow because we have to build the sequences + return ''.join(f.ucs + for f in iter_parse(self._term, inp) + if not f.is_sequence) def padd(self): r""" @@ -674,13 +726,8 @@ def padd(self): detected, those last-most characters are destroyed. """ outp = u'' - for ucs, is_sequence, _, _ in iter_parse(self._term, self): + for ucs, _, _, _ in iter_parse(self._term, self): width = horizontal_distance(ucs, self._term) - #TODO this fails on a tab in a test - #assert not (width and not is_sequence), ( - # 'only sequences should have non-zero width, but nonsequence ' + - # repr((ucs, is_sequence)) + ' has width of ' + str(width) + - # ' while parsing ' + repr(self)) if width > 0: outp += u' ' * width elif width < 0: @@ -732,7 +779,7 @@ def measure_length(ucs, term): if matching_seq: _, end = matching_seq.span() - assert identify_fragment(term, ucs[:end]) + assert identify_part(term, ucs[:end]) return end # none found, must be printable! @@ -820,12 +867,32 @@ def horizontal_distance(ucs, term): 0) -def identify_fragment(term, ucs): +def identify_part(term, ucs): + """ + Return a TextPart instance describing the terminal sequence in ucs. + + :arg str ucs: text beginning with a terminal sequence + :arg blessed.Terminal term: :class:`~.Terminal` instance. + :raises ValueError: ucs is not a valid terminal sequence. + :rtype: TextPart + + TextPart is a :class:`collections.namedtuple` instance describing + either a terminal sequence or a series of printable characters. + Its parameters are: + + - ``ucs``: str of terminal sequence or printable characters + - ``is_sequence``: bool for whether this is a terminal sequence + - ``name``: str of capability name or descriptive name of the + terminal sequence, or None if not a terminal sequence + - ``params``: a tuple of str parameters in the terminal sequence, + or None if not a terminal sequence + + """ # simple terminal control characters, ctrl_seqs = u'\a\b\r\n\x0e\x0f' if any([ucs.startswith(_ch) for _ch in ctrl_seqs]): - return TextFragment(ucs[:1], True, '?', None) + return TextPart(ucs[:1], True, '?', None) ret = term and ( regex_or_match(term._will_move, ucs) or @@ -841,10 +908,10 @@ def identify_fragment(term, ucs): ) if matching_seq: - return TextFragment(matching_seq.group(), True, '?', None) - - raise ValueError("not a sequence!: "+repr(ucs)) + return TextPart(matching_seq.group(), True, '?', None) + raise ValueError("identify_part called on nonsequence " + "{!r}".format(ucs)) def regex_or_match(patterns, ucs): for cap, pat in patterns: @@ -852,24 +919,55 @@ def regex_or_match(patterns, ucs): if matching_seq is None: continue params = matching_seq.groups()[1:] - return TextFragment(matching_seq.group(), True, cap, params if params else None) + return TextPart(matching_seq.group(), True, cap, params if params else None) return None -def i_and_split_nonsequences(fragments): - """Yields simple unicode characters or sequences with index""" +def enumerate_by_position(parts): + """Iterate over TextParts with an index, subdividing printable strings. + + The index is the length of characters preceding that TextPart, in other + words the cumulative sum of lengths of TextParts before the current one. + TextPart instances composed of multiple printable characters (those not + part of a terminal sequence) will be broken into multiple TextPart + instances, one per character. + + This is useful for splitting text into its smallest indivisible TextPart + units: splitting strings into characters while not breaking up terminal + sequences. + + :arg: parts: iterable of TextPart instances + :rtype: iterator of tuple pairs of (int, TextPart) + + """ idx = 0 - for part in fragments: + for part in parts: if part.is_sequence: yield idx, part idx += len(part.ucs) else: - for c in part.ucs: - yield idx, TextFragment(c, False, None, None) + for char in part.ucs: + yield idx, TextPart(char, False, None, None) idx += 1 + def iter_parse(term, ucs): r""" + Return an iterator TextPart instances: terminal sequences or strings. + + :arg ucs: str which may contain terminal sequences + :arg blessed.Terminal term: :class:`~.Terminal` instance. + :rtype: iterator of TextPart instances + + TextPart is a :class:`collections.namedtuple` instance describing + either a terminal sequence or a series of printable characters. + Its parameters are: + - ``ucs``: str of terminal sequence or printable characters + - ``is_sequence``: bool for whether this is a terminal sequence + - ``name``: str of capability name or descriptive name of the terminal + sequence, or None if not a terminal sequence + - ``params``: a tuple of str parameters in the terminal sequence, + or None if not a terminal sequence """ outp = u'' @@ -882,9 +980,10 @@ def iter_parse(term, ucs): nxt += 1 else: if outp: - yield TextFragment(outp, False, None, None) + yield TextPart(outp, False, None, None) outp = u'' - yield identify_fragment(term, ucs[idx:idx+l]) + yield identify_part(term, ucs[idx:idx+l]) nxt += l if outp: - yield TextFragment(outp, False, None, None) + # ucs ends with printable characters + yield TextPart(outp, False, None, None) From 71bfe317f48a3607872a6508f8743331b0df6416 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Fri, 30 Oct 2015 11:05:09 -0400 Subject: [PATCH 320/459] fix formating issues --- blessed/sequences.py | 100 ++++++++++++++++++++++++++----------------- blessed/terminal.py | 17 ++++++++ 2 files changed, 78 insertions(+), 39 deletions(-) diff --git a/blessed/sequences.py b/blessed/sequences.py index 7399f34e..4eaa6963 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -1,6 +1,7 @@ # encoding: utf-8 """This module provides 'sequence awareness'.""" - +# pylint: disable=too-many-lines +# Too many lines in module (1027/1000) # std imports import collections import functools @@ -71,11 +72,20 @@ def _sort_sequences(regex_seqlist): key=lambda x: len(x[1]), reverse=True) -def _differentiate_duplicate_sequences(regex_seqlist): + +def _unique_names(regex_seqlist): """ - annotate with unique ids per list + Return a list of (name, pattern) pairs liki input but with names unique. + + By adding _1, _2, _3 etc. to the name of each name_pair sequence the + first element of each tuple + + :arg list regex_seqlist: list of tuples of (str name, str pattern) + :rtype: list + :returns: a copy of the input list with names modified to be unique """ counter = collections.Counter() + def inc_get(key): counter[key] += 1 return counter[key] @@ -171,7 +181,6 @@ def get_movement_sequence_patterns(term): :arg blessed.Terminal term: :class:`~.Terminal` instance. :rtype: list """ - bnc = functools.partial(_build_numeric_capability, term) bsc = functools.partial(_build_simple_capability, term) @@ -336,6 +345,13 @@ def get_wontmove_sequence_patterns(term): ] + list(map(bsc, ('r1', 'r2', 'r3'))))) +def strip_groups(pattern): + """Return version of str regex without unnamed capture groups.""" + # make capture groups non-capturing + # TODO this incorrectly replaces () in r'[()]' -> [(?:)] + return re.sub(r'(?{})".format(c, strip_groups(s)) + for c, s in _will_move if s)) + _re_wont_move = re.compile(u'|'.join( + u"(?P<{}>{})".format(c, strip_groups(s)) + for c, s in _wont_move if s)) + if (_will_move and _wont_move and set(list(zip(*_will_move))[0]) & + set(list(zip(*_wont_move))[0])): + # _param_extractors requires names be unique + raise ValueError("will_move and wont_move contain same capname") + _param_extractors = dict((c, re.compile(s)) + for c, s in _will_move + _wont_move) # static pattern matching for horizontal_distance(ucs, term) bnc = functools.partial(_build_numeric_capability, term) @@ -435,8 +462,7 @@ def init_sequence_patterns(term): return {'_re_will_move': _re_will_move, '_re_wont_move': _re_wont_move, - '_will_move': _will_move, - '_wont_move': _wont_move, + '_param_extractors': _param_extractors, '_re_cuf': _re_cuf, '_re_cub': _re_cub, '_cuf1': _cuf1, @@ -894,12 +920,17 @@ def identify_part(term, ucs): if any([ucs.startswith(_ch) for _ch in ctrl_seqs]): return TextPart(ucs[:1], True, '?', None) - ret = term and ( - regex_or_match(term._will_move, ucs) or - regex_or_match(term._wont_move, ucs) + matching_seq = term and ( + term._re_will_move.match(ucs) or + term._re_wont_move.match(ucs) ) - if ret: - return ret + if matching_seq: + (identifier, ) = (k for k, v in matching_seq.groupdict().items() + if v is not None) + name = identifier + params = term._param_extractors[identifier].match(ucs).groups() + return TextPart(matching_seq.group(), True, + name, params if params else None) # known multibyte sequences, matching_seq = term and ( @@ -913,14 +944,6 @@ def identify_part(term, ucs): raise ValueError("identify_part called on nonsequence " "{!r}".format(ucs)) -def regex_or_match(patterns, ucs): - for cap, pat in patterns: - matching_seq = re.match(pat, ucs) - if matching_seq is None: - continue - params = matching_seq.groups()[1:] - return TextPart(matching_seq.group(), True, cap, params if params else None) - return None def enumerate_by_position(parts): """Iterate over TextParts with an index, subdividing printable strings. @@ -952,7 +975,7 @@ def enumerate_by_position(parts): def iter_parse(term, ucs): r""" - Return an iterator TextPart instances: terminal sequences or strings. + Return an iterator of TextPart instances: terminal sequences or strings. :arg ucs: str which may contain terminal sequences :arg blessed.Terminal term: :class:`~.Terminal` instance. @@ -968,22 +991,21 @@ def iter_parse(term, ucs): sequence, or None if not a terminal sequence - ``params``: a tuple of str parameters in the terminal sequence, or None if not a terminal sequence - """ outp = u'' - nxt = 0 - for idx in range(0, six.text_type.__len__(ucs)): - if idx == nxt: - l = measure_length(ucs[idx:], term) - if l == 0: - outp += ucs[idx] - nxt += 1 - else: - if outp: - yield TextPart(outp, False, None, None) - outp = u'' - yield identify_part(term, ucs[idx:idx+l]) - nxt += l + idx = 0 + while idx < six.text_type.__len__(ucs): + length = measure_length(ucs[idx:], term) + if length == 0: + outp += ucs[idx] + idx += 1 + continue + if outp: + yield TextPart(outp, False, None, None) + outp = u'' + yield identify_part(term, ucs[idx:idx + length]) + idx += length + if outp: # ucs ends with printable characters yield TextPart(outp, False, None, None) diff --git a/blessed/terminal.py b/blessed/terminal.py index 944c81e2..1f88233b 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -1084,6 +1084,23 @@ def inkey(self, timeout=None, esc_delay=0.35, **_kwargs): return ks def iter_parse(self, text): + r""" + Return iterator of TextPart instances: terminal sequences or strings. + + :arg ucs: str which may contain terminal sequences + :rtype: iterator of TextPart instances + + TextPart is a :class:`collections.namedtuple` instance describing + either a terminal sequence or a series of printable characters. + Its parameters are: + + - ``ucs``: str of terminal sequence or printable characters + - ``is_sequence``: bool for whether this is a terminal sequence + - ``name``: str of capability name or descriptive name of the + terminal sequence, or None if not a terminal sequence + - ``params``: a tuple of str parameters in the terminal sequence, + or None if not a terminal sequence + """ return iter_parse(self, text) From af1df267c1c5d824ad397f97bfb695a4dd0df05b Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 31 Oct 2015 23:10:25 -0700 Subject: [PATCH 321/459] terminal capability database builder Declarative configuration by ordered dictionary, in preferred matching order --- blessed/_capabilities.py | 171 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 blessed/_capabilities.py diff --git a/blessed/_capabilities.py b/blessed/_capabilities.py new file mode 100644 index 00000000..5e60b3db --- /dev/null +++ b/blessed/_capabilities.py @@ -0,0 +1,171 @@ +"""Terminal capability builder patterns.""" +# std imports +import re + +try: + from collections import OrderedDict +except ImportError: + # python 2.6 requires 3rd party library (backport) + # + # pylint: disable=import-error + # Unable to import 'ordereddict' + from ordereddict import OrderedDict + +__all__ = ( + 'CAPABILITY_DATABASE', + 'CAPABILITIES_RAW_MIXIN', + 'CAPABILITIES_ADDITIVES', + 'CAPABILITIES_CAUSE_MOVEMENT', +) + +CAPABILITY_DATABASE = OrderedDict(( + ('bell', ('bel', {})), + ('carriage_return', ('cr', {})), + ('change_scroll_region', ('csr', {'nparams': 2})), + ('clear_all_tabs', ('tbc', {})), + ('clear_screen', ('clear', {})), + ('clr_bol', ('el1', {})), + ('clr_eol', ('el', {})), + ('clr_eos', ('clear_eos', {})), + ('column_address', ('hpa', {'nparams': 1})), + ('cursor_address', ('cup', {'nparams': 2})), + ('cursor_down', ('cud1', {})), + ('cursor_home', ('home', {})), + ('cursor_invisible', ('civis', {})), + ('cursor_left', ('cub1', {})), + ('cursor_normal', ('cnorm', {})), + ('cursor_report', ('u6', {'nparams': 2})), + ('cursor_right', ('cuf1', {})), + ('cursor_up', ('cuu1', {})), + ('cursor_visible', ('cvvis', {})), + ('delete_character', ('dch1', {})), + ('delete_line', ('dl1', {})), + ('enter_blink_mode', ('blink', {})), + ('enter_bold_mode', ('bold', {})), + ('enter_dim_mode', ('dim', {})), + ('enter_fullscreen', ('smcup', {})), + ('enter_standout_mode', ('standout', {})), + ('enter_superscript_mode', ('superscript', {})), + ('enter_susimpleript_mode', ('susimpleript', {})), + ('enter_underline_mode', ('underline', {})), + ('erase_chars', ('ech', {'nparams': 1})), + ('exit_alt_charset_mode', ('rmacs', {})), + ('exit_am_mode', ('rmam', {})), + ('exit_attribute_mode', ('sgr0', {})), + ('exit_ca_mode', ('rmcup', {})), + ('exit_fullscreen', ('rmcup', {})), + ('exit_insert_mode', ('rmir', {})), + ('exit_standout_mode', ('rmso', {})), + ('exit_underline_mode', ('rmul', {})), + ('flash_hook', ('hook', {})), + ('flash_screen', ('flash', {})), + ('insert_line', ('il1', {})), + ('keypad_local', ('rmkx', {})), + ('keypad_xmit', ('smkx', {})), + ('meta_off', ('rmm', {})), + ('meta_on', ('smm', {})), + ('orig_pair', ('op', {})), + ('parm_down_cursor', ('cud', {'nparams': 1})), + ('parm_left_cursor', ('cub', {'nparams': 1})), + ('parm_dch', ('dch', {'nparams': 1})), + ('parm_delete_line', ('dl', {'nparams': 1})), + ('parm_ich', ('ich', {'nparams': 1})), + ('parm_index', ('indn', {'nparams': 1})), + ('parm_insert_line', ('il', {'nparams': 1})), + ('parm_right_cursor', ('cuf', {'nparams': 1})), + ('parm_rindex', ('rin', {'nparams': 1})), + ('parm_up_cursor', ('cuu', {'nparams': 1})), + ('print_screen', ('mc0', {})), + ('prtr_off', ('mc4', {})), + ('prtr_on', ('mc5', {})), + ('reset_1string', ('r1', {})), + ('reset_2string', ('r2', {})), + ('reset_3string', ('r3', {})), + ('restore_cursor', ('rc', {})), + ('row_address', ('vpa', {'nparams': 1})), + ('save_cursor', ('sc', {})), + ('scroll_forward', ('ind', {})), + ('scroll_reverse', ('rev', {})), + ('set0_des_seq', ('s0ds', {})), + ('set1_des_seq', ('s1ds', {})), + ('set2_des_seq', ('s2ds', {})), + ('set3_des_seq', ('s3ds', {})), + # this 'color' is deceiving, but often matching, and a better match + # than set_a_attributes1 or set_a_foreground. + ('color', ('_foreground_color', {'nparams': 1, 'match_any': True, + 'numeric': 1})), + + # very likely, this will be the most commonly matched inward attribute. + ('set_a_attributes1', ('sgr1', {'nparams': 1, 'match_any': True, + 'match_optional': True})), + ('set_a_attributes2', ('sgr1', {'nparams': 2, 'match_any': True})), + ('set_a_attributes3', ('sgr1', {'nparams': 3, 'match_any': True})), + ('set_a_attributes4', ('sgr1', {'nparams': 4, 'match_any': True})), + ('set_a_attributes5', ('sgr1', {'nparams': 5, 'match_any': True})), + ('set_a_attributes6', ('sgr1', {'nparams': 6, 'match_any': True})), + ('set_a_attributes7', ('sgr1', {'nparams': 7, 'match_any': True})), + ('set_a_attributes8', ('sgr1', {'nparams': 8, 'match_any': True})), + ('set_a_attributes9', ('sgr1', {'nparams': 9, 'match_any': True})), + ('set_a_foreground', ('color', {'nparams': 1, 'match_any': True, + 'numeric': 1})), + ('set_a_background', ('on_color', {'nparams': 1, 'match_any': True, + 'numeric': 1})), + ('set_tab', ('hts', {})), + ('tab', ('ht', {})), +)) + +CAPABILITIES_RAW_MIXIN = { + 'bell': re.escape('\a'), + 'carriage_return': re.escape('\r'), + 'cursor_left': re.escape('\b'), + 'cursor_report': re.escape('\x1b') + r'\[(\d+)\;(\d+)R', + 'cursor_right': re.escape('\x1b') + r'\[C', + 'exit_attribute_mode': re.escape('\x1b') + r'\[m', + 'parm_left_cursor': re.escape('\x1b') + r'\[(\d+)D', + 'parm_right_cursor': re.escape('\x1b') + r'\[(\d+)C', + 'scroll_forward': re.escape('\n'), + 'set0_des_seq': re.escape('\x1b(B'), + 'set_a_attributes1': re.escape('\x1b') + r'\[(\d+)?m', + 'set_a_attributes2': re.escape('\x1b') + r'\[(\d+)\;(\d+)m', + 'set_a_attributes3': re.escape('\x1b') + r'\[(\d+)\;(\d+)\;(\d+)m', + 'set_a_attributes4': re.escape('\x1b') + r'\[(\d+)\;(\d+)\;(\d+)\;(\d+)m', + 'tab': re.escape('\t'), + # one could get carried away, such as by adding '\x1b#8' (dec tube + # alignment test) by reversing basic vt52, ansi, and xterm sequence + # parsers. There is plans to do just that for ANSI.SYS support. +} + +CAPABILITIES_ADDITIVES = { + 'color256': ('color', re.escape('\x1b') + r'\[38;5;(\d+)m'), + 'shift_in': ('', re.escape('\x0f')), + 'shift_out': ('', re.escape('\x0e')), + # this helps where xterm's sgr0 includes set0_des_seq, we'd + # rather like to also match this immediate substring. + 'sgr0': ('sgr0', re.escape('\x1b') + r'\[m'), + 'backspace': ('', re.escape('\b')), + 'ascii_tab': ('', re.escape('\t')), +} + +CAPABILITIES_CAUSE_MOVEMENT = ( + 'ascii_tab', + 'backspace' + 'carriage_return', + 'clear_screen', + 'column_address', + 'cursor_address', + 'cursor_down', + 'cursor_home', + 'cursor_left', + 'cursor_right', + 'cursor_up', + 'enter_fullscreen', + 'exit_fullscreen', + 'parm_down_cursor', + 'parm_left_cursor', + 'parm_right_cursor', + 'parm_up_cursor', + 'restore_cursor', + 'row_address', + 'scroll_forward', + 'tab', +) From 1b5fbfd645552765dfa72fa43bc09be5bcd8718d Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 31 Oct 2015 23:10:56 -0700 Subject: [PATCH 322/459] sphinx builds using 3.5, posargs --- tox.ini | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index 6ccb211f..439419d7 100644 --- a/tox.ini +++ b/tox.ini @@ -7,11 +7,10 @@ whitelist_externals = cp setenv = PYTHONIOENCODING=UTF8 passenv = TEST_QUICK TEST_FULL deps = -rrequirements-tests.txt -commands = {envbindir}/py.test \ +commands = {envbindir}/py.test {posargs:\ --strict --verbose --verbose --color=yes \ --junit-xml=results.{envname}.xml \ - --cov blessed blessed/tests \ - {posargs:-x} + --cov blessed blessed/tests} coverage combine cp {toxinidir}/.coverage \ {toxinidir}/._coverage.{envname}.{env:COVERAGE_ID:local} @@ -50,7 +49,7 @@ commands = python -m compileall -fq {toxinidir}/blessed [testenv:sphinx] whitelist_externals = echo -basepython = python3.4 +basepython = python3.5 deps = -rrequirements-docs.txt commands = {envbindir}/sphinx-build -v -W \ -d {toxinidir}/docs/_build/doctrees \ From 788ff569af78ba1843ad29b700256f12cc66f008 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 31 Oct 2015 23:13:07 -0700 Subject: [PATCH 323/459] remove old comments, duplicated by docstring --- blessed/formatters.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/blessed/formatters.py b/blessed/formatters.py index 80ed56cf..66020738 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -282,17 +282,6 @@ def __call__(self, *args): any attributes. """ if len(args) == 0 or isinstance(args[0], int): - # I am acting as a ParameterizingString. - - # tparm can take not only ints but also (at least) strings as its - # 2nd...nth argument. But we don't support callable parameterizing - # capabilities that take non-ints yet, so we can cheap out here. - - # TODO(erikrose): Go through enough of the motions in the - # capability resolvers to determine which of 2 special-purpose - # classes, NullParameterizableString or NullFormattingString, - # to return, and retire this one. - # As a NullCallableString, even when provided with a parameter, # such as t.color(5), we must also still be callable, fe: # From 5f3cbeaf4e18e9d034773c68c67a5d3a7c505865 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 31 Oct 2015 23:14:42 -0700 Subject: [PATCH 324/459] remove api docs for removed private functions --- docs/api.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 2a278785..3a276bdf 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -41,6 +41,3 @@ sequences.py :members: :undoc-members: :private-members: -.. autofunction:: _sort_sequences -.. autofunction:: _build_numeric_capability -.. autofunction:: _build_any_numeric_capability From 0bbb0e55358c591293a3674462b108ee6bb56b6e Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 31 Oct 2015 23:15:09 -0700 Subject: [PATCH 325/459] do not test about emission of binack warnings --- blessed/tests/test_sequences.py | 109 -------------------------------- 1 file changed, 109 deletions(-) diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index 81d165b2..ba8275c0 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -87,71 +87,6 @@ def child(): child() -@pytest.mark.skipif(os.environ.get('TRAVIS', None) is not None, - reason="travis-ci does not have binary-packed terminals.") -def test_emit_warnings_about_binpacked(): - """Test known binary-packed terminals (kermit, avatar) emit a warning.""" - from blessed._binterms import BINTERM_UNSUPPORTED_MSG - - @as_subprocess - def child(kind): - import warnings - warnings.filterwarnings("error", category=RuntimeWarning) - warnings.filterwarnings("error", category=UserWarning) - - try: - TestTerminal(kind=kind, force_styling=True) - except UserWarning: - err = sys.exc_info()[1] - assert (err.args[0] == BINTERM_UNSUPPORTED_MSG.format(kind) or - err.args[0].startswith('Unknown parameter in ') or - err.args[0].startswith('Failed to setupterm(') - ), err - else: - assert 'warnings should have been emitted.' - warnings.resetwarnings() - - # Although any binary terminal should do, FreeBSD has "termcap entry bugs" - # that cause false negatives, because their underlying curses library emits - # some kind of "warning" to stderr, which our @as_subprocess decorator - # determines to be noteworthy enough to fail the test: - # - # https://gist.github.com/jquast/7b90af251fe4000baa09 - # - # so we chose only one, known good value, of beautiful lineage: - # - # http://terminals.classiccmp.org/wiki/index.php/Tektronix_4207 - - child(kind='tek4207-s') - - -def test_unit_binpacked_unittest(): - """Unit Test known binary-packed terminals emit a warning (travis-safe).""" - import warnings - from blessed._binterms import BINTERM_UNSUPPORTED_MSG - from blessed.sequences import init_sequence_patterns - warnings.filterwarnings("error", category=UserWarning) - term = mock.Mock() - term.kind = 'tek4207-s' - - try: - init_sequence_patterns(term) - except UserWarning: - err = sys.exc_info()[1] - assert err.args[0] == BINTERM_UNSUPPORTED_MSG.format(term.kind) - else: - assert False, 'Previous stmt should have raised exception.' - warnings.resetwarnings() - - -def test_sort_sequence_pairs(): - """Test sequences are filtered and ordered longest-first.""" - from blessed.sequences import _sort_sequences - input_list = [(1, u'a'), (2, u'aa'), (3, u'aaa'), (4, u'')] - output_expected = [(3, u'aaa'), (2, u'aa'), (1, u'a')] - assert (_sort_sequences(input_list) == output_expected) - - def test_location_with_styling(all_terms): """Make sure ``location()`` works on all terminals.""" @as_subprocess @@ -552,50 +487,6 @@ def child(kind): child(all_terms) -def test_bnc_parameter_emits_warning(): - """A fake capability without target digits emits a warning.""" - import warnings - from blessed.sequences import _build_numeric_capability - - # given, - warnings.filterwarnings("error", category=UserWarning) - term = mock.Mock() - fake_cap = lambda *args: u'NO-DIGIT' - term.fake_cap = fake_cap - - # exercise, - try: - _build_numeric_capability(term, 'fake_cap', base_num=1984) - except UserWarning: - err = sys.exc_info()[1] - assert err.args[0].startswith('Unknown parameter in ') - else: - assert False, 'Previous stmt should have raised exception.' - warnings.resetwarnings() - - -def test_bna_parameter_emits_warning(): - """A fake capability without any digits emits a warning.""" - import warnings - from blessed.sequences import _build_any_numeric_capability - - # given, - warnings.filterwarnings("error", category=UserWarning) - term = mock.Mock() - fake_cap = lambda *args: 'NO-DIGIT' - term.fake_cap = fake_cap - - # exercise, - try: - _build_any_numeric_capability(term, 'fake_cap') - except UserWarning: - err = sys.exc_info()[1] - assert err.args[0].startswith('Missing numerics in ') - else: - assert False, 'Previous stmt should have raised exception.' - warnings.resetwarnings() - - def test_padd(): """ Test terminal.padd(seq). """ @as_subprocess From eb481cd9539cea791649e378ee6e4f6a92424445 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 31 Oct 2015 23:15:33 -0700 Subject: [PATCH 326/459] temporarily disable measure_length comparable tests need to be constructed (see TODO) --- blessed/tests/test_length_sequence.py | 159 +++++++++++++------------- 1 file changed, 80 insertions(+), 79 deletions(-) diff --git a/blessed/tests/test_length_sequence.py b/blessed/tests/test_length_sequence.py index 0c3efffb..c1f06cbe 100644 --- a/blessed/tests/test_length_sequence.py +++ b/blessed/tests/test_length_sequence.py @@ -288,82 +288,83 @@ def child(kind, lines=25, cols=80): child(kind=all_terms) - -def test_sequence_is_movement_false(all_terms): - """Test parser about sequences that do not move the cursor.""" - @as_subprocess - def child_mnemonics_wontmove(kind): - from blessed.sequences import measure_length - t = TestTerminal(kind=kind) - assert (0 == measure_length(u'', t)) - # not even a mbs - assert (0 == measure_length(u'xyzzy', t)) - # negative numbers, though printable as %d, do not result - # in movement; just garbage. Also not a valid sequence. - assert (0 == measure_length(t.cuf(-333), t)) - assert (len(t.clear_eol) == measure_length(t.clear_eol, t)) - # various erases don't *move* - assert (len(t.clear_bol) == measure_length(t.clear_bol, t)) - assert (len(t.clear_eos) == measure_length(t.clear_eos, t)) - assert (len(t.bold) == measure_length(t.bold, t)) - # various paints don't move - assert (len(t.red) == measure_length(t.red, t)) - assert (len(t.civis) == measure_length(t.civis, t)) - if t.cvvis: - assert (len(t.cvvis) == measure_length(t.cvvis, t)) - assert (len(t.underline) == measure_length(t.underline, t)) - assert (len(t.reverse) == measure_length(t.reverse, t)) - for _num in range(t.number_of_colors): - assert (len(t.color(_num)) == measure_length(t.color(_num), t)) - assert (len(t.normal) == measure_length(t.normal, t)) - assert (len(t.normal_cursor) == measure_length(t.normal_cursor, t)) - assert (len(t.hide_cursor) == measure_length(t.hide_cursor, t)) - assert (len(t.save) == measure_length(t.save, t)) - assert (len(t.italic) == measure_length(t.italic, t)) - assert (len(t.standout) == measure_length(t.standout, t) - ), (t.standout, t._wont_move) - - child_mnemonics_wontmove(all_terms) - - -def test_sequence_is_movement_true(all_terms): - """Test parsers about sequences that move the cursor.""" - @as_subprocess - def child_mnemonics_willmove(kind): - from blessed.sequences import measure_length - t = TestTerminal(kind=kind) - # movements - assert (len(t.move(98, 76)) == - measure_length(t.move(98, 76), t)) - assert (len(t.move(54)) == - measure_length(t.move(54), t)) - assert not t.cud1 or (len(t.cud1) == - measure_length(t.cud1, t)) - assert not t.cub1 or (len(t.cub1) == - measure_length(t.cub1, t)) - assert not t.cuf1 or (len(t.cuf1) == - measure_length(t.cuf1, t)) - assert not t.cuu1 or (len(t.cuu1) == - measure_length(t.cuu1, t)) - assert not t.cub or (len(t.cub(333)) == - measure_length(t.cub(333), t)) - assert not t.cuf or (len(t.cuf(333)) == - measure_length(t.cuf(333), t)) - assert not t.home or (len(t.home) == - measure_length(t.home, t)) - assert not t.restore or (len(t.restore) == - measure_length(t.restore, t)) - assert not t.clear or (len(t.clear) == - measure_length(t.clear, t)) - - child_mnemonics_willmove(all_terms) - - -def test_foreign_sequences(): - """Test parsers about sequences received from foreign sources.""" - @as_subprocess - def child(kind): - from blessed.sequences import measure_length - t = TestTerminal(kind=kind) - assert measure_length(u'\x1b[m', t) == len('\x1b[m') - child(kind='ansi') +# TODO: next(term.iter_parse('sequence')).horizontal_distance('sequence') +# +#def test_sequence_is_movement_false(all_terms): +# """Test parser about sequences that do not move the cursor.""" +# @as_subprocess +# def child_mnemonics_wontmove(kind): +# from blessed.sequences import measure_length +# t = TestTerminal(kind=kind) +# assert (0 == measure_length(u'', t)) +# # not even a mbs +# assert (0 == measure_length(u'xyzzy', t)) +# # negative numbers, though printable as %d, do not result +# # in movement; just garbage. Also not a valid sequence. +# assert (0 == measure_length(t.cuf(-333), t)) +# assert (len(t.clear_eol) == measure_length(t.clear_eol, t)) +# # various erases don't *move* +# assert (len(t.clear_bol) == measure_length(t.clear_bol, t)) +# assert (len(t.clear_eos) == measure_length(t.clear_eos, t)) +# assert (len(t.bold) == measure_length(t.bold, t)) +# # various paints don't move +# assert (len(t.red) == measure_length(t.red, t)) +# assert (len(t.civis) == measure_length(t.civis, t)) +# if t.cvvis: +# assert (len(t.cvvis) == measure_length(t.cvvis, t)) +# assert (len(t.underline) == measure_length(t.underline, t)) +# assert (len(t.reverse) == measure_length(t.reverse, t)) +# for _num in range(t.number_of_colors): +# assert (len(t.color(_num)) == measure_length(t.color(_num), t)) +# assert (len(t.normal) == measure_length(t.normal, t)) +# assert (len(t.normal_cursor) == measure_length(t.normal_cursor, t)) +# assert (len(t.hide_cursor) == measure_length(t.hide_cursor, t)) +# assert (len(t.save) == measure_length(t.save, t)) +# assert (len(t.italic) == measure_length(t.italic, t)) +# assert (len(t.standout) == measure_length(t.standout, t) +# ), (t.standout, t._wont_move) +# +# child_mnemonics_wontmove(all_terms) +# +# +#def test_sequence_is_movement_true(all_terms): +# """Test parsers about sequences that move the cursor.""" +# @as_subprocess +# def child_mnemonics_willmove(kind): +# from blessed.sequences import measure_length +# t = TestTerminal(kind=kind) +# # movements +# assert (len(t.move(98, 76)) == +# measure_length(t.move(98, 76), t)) +# assert (len(t.move(54)) == +# measure_length(t.move(54), t)) +# assert not t.cud1 or (len(t.cud1) == +# measure_length(t.cud1, t)) +# assert not t.cub1 or (len(t.cub1) == +# measure_length(t.cub1, t)) +# assert not t.cuf1 or (len(t.cuf1) == +# measure_length(t.cuf1, t)) +# assert not t.cuu1 or (len(t.cuu1) == +# measure_length(t.cuu1, t)) +# assert not t.cub or (len(t.cub(333)) == +# measure_length(t.cub(333), t)) +# assert not t.cuf or (len(t.cuf(333)) == +# measure_length(t.cuf(333), t)) +# assert not t.home or (len(t.home) == +# measure_length(t.home, t)) +# assert not t.restore or (len(t.restore) == +# measure_length(t.restore, t)) +# assert not t.clear or (len(t.clear) == +# measure_length(t.clear, t)) +# +# child_mnemonics_willmove(all_terms) +# +# +#def test_foreign_sequences(): +# """Test parsers about sequences received from foreign sources.""" +# @as_subprocess +# def child(kind): +# from blessed.sequences import measure_length +# t = TestTerminal(kind=kind) +# assert measure_length(u'\x1b[m', t) == len('\x1b[m') +# child(kind='ansi') From db83681f2719fa1caa83ccf99cffd59e458fe00b Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 31 Oct 2015 23:17:30 -0700 Subject: [PATCH 327/459] reduce complexity of capability pattern code iter_parse(term, ucs) now yields single characters for non-matching sequences, its second item (capability) is ``None`` in such cases. See example uses. --- blessed/_binterms.py | 878 ------------------------------------------ blessed/sequences.py | 883 +++++++------------------------------------ blessed/terminal.py | 126 ++++-- 3 files changed, 225 insertions(+), 1662 deletions(-) delete mode 100644 blessed/_binterms.py diff --git a/blessed/_binterms.py b/blessed/_binterms.py deleted file mode 100644 index e6ce5879..00000000 --- a/blessed/_binterms.py +++ /dev/null @@ -1,878 +0,0 @@ -"""List of terminal definitions containing binary-packed sequences.""" - -#: This list of terminals is manually managed, it describes all of the -#: terminals that blessed cannot measure the sequence length for; they -#: contain binary-packed capabilities instead of numerics, so it is not -#: possible to build regular expressions in the way that sequences.py does. -#: -#: This may be generated by exporting TEST_BINTERMS, then analyzing the -#: jUnit result xml written to the project folder. -BINARY_TERMINALS = u""" -9term -aaa+dec -aaa+rv -aaa+rv-100 -aaa+rv-30 -aaa-rv-unk -abm80 -abm85 -abm85e -abm85h -abm85h-old -act4 -act5 -addrinfo -adds980 -adm+sgr -adm+sgr-100 -adm+sgr-30 -adm11 -adm1178 -adm12 -adm1a -adm2 -adm20 -adm21 -adm22 -adm3 -adm31 -adm31-old -adm3a -adm3a+ -adm42 -adm42-ns -adm5 -aepro -aj510 -aj830 -alto-h19 -altos4 -altos7 -altos7pc -ampex175 -ampex175-b -ampex210 -ampex232 -ampex232w -ampex80 -annarbor4080 -ansi+arrows -ansi+csr -ansi+cup -ansi+enq -ansi+erase -ansi+idc -ansi+idl -ansi+idl1 -ansi+inittabs -ansi+local -ansi+local1 -ansi+pp -ansi+rca -ansi+rep -ansi+sgr -ansi+sgrbold -ansi+sgrdim -ansi+sgrso -ansi+sgrul -ansi+tabs -ansi-color-2-emx -ansi-color-3-emx -ansi-emx -ansi-mini -ansi-mr -ansi-mtabs -ansi-nt -ansi.sys -ansi.sys-old -ansi.sysk -ansi77 -apollo -apple-80 -apple-ae -apple-soroc -apple-uterm -apple-uterm-vb -apple-videx -apple-videx2 -apple-videx3 -apple-vm80 -apple2e -apple2e-p -apple80p -appleII -appleIIgs -atari -att4415+nl -att4420 -att4424m -att5310 -att5310-100 -att5310-30 -att5620-s -avatar -avatar0 -avatar0+ -avt+s -aws -awsc -bantam -basis -beacon -beehive -blit -bq300-8 -bq300-8-pc -bq300-8-pc-rv -bq300-8-pc-w -bq300-8-pc-w-rv -bq300-8rv -bq300-8w -bq300-w-8rv -c100 -c100-rv -c108 -c108-4p -c108-rv -c108-rv-4p -c108-w -ca22851 -cbblit -cbunix -cci -cci-100 -cci-30 -cdc456 -cdc721 -cdc721-esc -cdc721-esc-100 -cdc721-esc-30 -cdc721ll -cdc752 -cdc756 -cit101e -cit101e-132 -cit101e-n -cit101e-n132 -cit80 -citoh -citoh-6lpi -citoh-8lpi -citoh-comp -citoh-elite -citoh-pica -citoh-prop -coco3 -color_xterm -commodore -contel300 -contel301 -cops10 -ct8500 -ctrm -ctrm-100 -ctrm-30 -cyb110 -cyb83 -d132 -d200 -d200-100 -d200-30 -d210-dg -d210-dg-100 -d210-dg-30 -d211-dg -d211-dg-100 -d211-dg-30 -d216-dg -d216-dg-100 -d216-dg-30 -d216-unix -d216-unix-25 -d217-unix -d217-unix-25 -d220 -d220-100 -d220-30 -d220-7b -d220-7b-100 -d220-7b-30 -d220-dg -d220-dg-100 -d220-dg-30 -d230c -d230c-100 -d230c-30 -d230c-dg -d230c-dg-100 -d230c-dg-30 -d400 -d400-100 -d400-30 -d410-dg -d410-dg-100 -d410-dg-30 -d412-dg -d412-dg-100 -d412-dg-30 -d412-unix -d412-unix-25 -d412-unix-s -d412-unix-sr -d412-unix-w -d413-unix -d413-unix-25 -d413-unix-s -d413-unix-sr -d413-unix-w -d414-unix -d414-unix-25 -d414-unix-s -d414-unix-sr -d414-unix-w -d430c-dg -d430c-dg-100 -d430c-dg-30 -d430c-dg-ccc -d430c-dg-ccc-100 -d430c-dg-ccc-30 -d430c-unix -d430c-unix-100 -d430c-unix-25 -d430c-unix-25-100 -d430c-unix-25-30 -d430c-unix-25-ccc -d430c-unix-30 -d430c-unix-ccc -d430c-unix-s -d430c-unix-s-ccc -d430c-unix-sr -d430c-unix-sr-ccc -d430c-unix-w -d430c-unix-w-ccc -d470c -d470c-7b -d470c-dg -d555-dg -d577-dg -d800 -delta -dg+ccc -dg+color -dg+color8 -dg+fixed -dg-generic -dg200 -dg210 -dg211 -dg450 -dg460-ansi -dg6053 -dg6053-old -dgkeys+11 -dgkeys+15 -dgkeys+7b -dgkeys+8b -dgmode+color -dgmode+color8 -dgunix+ccc -dgunix+fixed -diablo1620 -diablo1620-m8 -diablo1640 -diablo1640-lm -diablo1740-lm -digilog -djgpp203 -dm1520 -dm2500 -dm3025 -dm3045 -dmchat -dmterm -dp8242 -dt100 -dt100w -dt110 -dt80-sas -dtc300s -dtc382 -dumb -dw1 -dw2 -dw3 -dw4 -dwk -ecma+color -ecma+sgr -elks -elks-glasstty -elks-vt52 -emu -ep40 -ep48 -esprit -esprit-am -ex155 -f100 -f100-rv -f110 -f110-14 -f110-14w -f110-w -f200 -f200-w -f200vi -f200vi-w -falco -falco-p -fos -fox -gator-52 -gator-52t -glasstty -gnome -gnome+pcfkeys -gnome-2007 -gnome-2008 -gnome-256color -gnome-fc5 -gnome-rh72 -gnome-rh80 -gnome-rh90 -go140 -go140w -gs6300 -gsi -gt40 -gt42 -guru+rv -guru+s -h19 -h19-bs -h19-g -h19-u -h19-us -h19k -ha8675 -ha8686 -hft-old -hmod1 -hp+arrows -hp+color -hp+labels -hp+pfk+arrows -hp+pfk+cr -hp+pfk-cr -hp+printer -hp2 -hp236 -hp262x -hp2641a -hp300h -hp700-wy -hp70092 -hp9837 -hp9845 -hpterm -hpterm-color -hz1000 -hz1420 -hz1500 -hz1510 -hz1520 -hz1520-noesc -hz1552 -hz1552-rv -hz2000 -i100 -i400 -ibcs2 -ibm+16color -ibm+color -ibm-apl -ibm-system1 -ibm3101 -ibm3151 -ibm3161 -ibm3161-C -ibm3162 -ibm3164 -ibm327x -ibm5081-c -ibm8514-c -ibmaed -ibmapa8c -ibmapa8c-c -ibmega -ibmega-c -ibmmono -ibmvga -ibmvga-c -icl6404 -icl6404-w -ifmr -ims-ansi -ims950 -ims950-b -ims950-rv -intertube -intertube2 -intext -intext2 -kaypro -kermit -kermit-am -klone+acs -klone+color -klone+koi8acs -klone+sgr -klone+sgr-dumb -klone+sgr8 -konsole -konsole+pcfkeys -konsole-16color -konsole-256color -konsole-base -konsole-linux -konsole-solaris -konsole-vt100 -konsole-vt420pc -konsole-xf3x -konsole-xf4x -kt7 -kt7ix -ln03 -ln03-w -lpr -luna -megatek -mgterm -microb -mime -mime-fb -mime-hb -mime2a -mime2a-s -mime314 -mime3a -mime3ax -minitel1 -minitel1b -mlterm+pcfkeys -mm340 -modgraph2 -msk227 -msk22714 -msk227am -mt70 -ncr160vppp -ncr160vpwpp -ncr160wy50+pp -ncr160wy50+wpp -ncr160wy60pp -ncr160wy60wpp -ncr260vppp -ncr260vpwpp -ncr260wy325pp -ncr260wy325wpp -ncr260wy350pp -ncr260wy350wpp -ncr260wy50+pp -ncr260wy50+wpp -ncr260wy60pp -ncr260wy60wpp -ncr7900i -ncr7900iv -ncr7901 -ncrvt100an -ncrvt100wan -ndr9500 -ndr9500-25 -ndr9500-25-mc -ndr9500-25-mc-nl -ndr9500-25-nl -ndr9500-mc -ndr9500-mc-nl -ndr9500-nl -nec5520 -newhpkeyboard -nextshell -northstar -nsterm+c -nsterm+c41 -nsterm+s -nwp511 -oblit -oc100 -oldpc3 -origpc3 -osborne -osborne-w -osexec -otek4112 -owl -p19 -pc-coherent -pc-venix -pc6300plus -pcix -pckermit -pckermit120 -pe1251 -pe7000c -pe7000m -pilot -pmcons -prism2 -prism4 -prism5 -pro350 -psterm-fast -psterm-fast-100 -psterm-fast-30 -pt100 -pt210 -pt250 -pty -qansi -qansi-g -qansi-m -qansi-t -qansi-w -qdss -qnx -qnx-100 -qnx-30 -qnxm -qnxm-100 -qnxm-30 -qnxt -qnxt-100 -qnxt-30 -qnxt2 -qnxtmono -qnxtmono-100 -qnxtmono-30 -qnxw -qnxw-100 -qnxw-30 -qume5 -qvt101 -qvt101+ -qvt102 -qvt119+ -qvt119+-25 -qvt119+-25-w -qvt119+-w -rbcomm -rbcomm-nam -rbcomm-w -rca -regent100 -regent20 -regent25 -regent40 -regent40+ -regent60 -rt6221 -rt6221-w -rtpc -rxvt+pcfkeys -scanset -screen+fkeys -screen-16color -screen-16color-bce -screen-16color-bce-s -screen-16color-s -screen-256color -screen-256color-bce -screen-256color-bce-s -screen-256color-s -screen-bce -screen-s -screen-w -screen.linux -screen.rxvt -screen.teraterm -screen.xterm-r6 -screen2 -screen3 -screwpoint -sibo -simterm -soroc120 -soroc140 -st52 -superbee-xsb -superbeeic -superbrain -swtp -synertek -t10 -t1061 -t1061f -t3700 -t3800 -tandem6510 -tandem653 -tandem653-100 -tandem653-30 -tek -tek4013 -tek4014 -tek4014-sm -tek4015 -tek4015-sm -tek4023 -tek4105 -tek4107 -tek4113-nd -tek4205 -tek4205-100 -tek4205-30 -tek4207-s -teraterm -teraterm2.3 -teraterm4.59 -terminet1200 -ti700 -ti931 -trs16 -trs2 -tt -tty33 -tty37 -tty43 -tvi803 -tvi9065 -tvi910 -tvi910+ -tvi912 -tvi912b -tvi912b+2p -tvi912b+dim -tvi912b+dim-100 -tvi912b+dim-30 -tvi912b+mc -tvi912b+mc-100 -tvi912b+mc-30 -tvi912b+printer -tvi912b+vb -tvi912b-2p -tvi912b-2p-mc -tvi912b-2p-mc-100 -tvi912b-2p-mc-30 -tvi912b-2p-p -tvi912b-2p-unk -tvi912b-mc -tvi912b-mc-100 -tvi912b-mc-30 -tvi912b-p -tvi912b-unk -tvi912b-vb -tvi912b-vb-mc -tvi912b-vb-mc-100 -tvi912b-vb-mc-30 -tvi912b-vb-p -tvi912b-vb-unk -tvi920b -tvi920b+fn -tvi920b-2p -tvi920b-2p-mc -tvi920b-2p-mc-100 -tvi920b-2p-mc-30 -tvi920b-2p-p -tvi920b-2p-unk -tvi920b-mc -tvi920b-mc-100 -tvi920b-mc-30 -tvi920b-p -tvi920b-unk -tvi920b-vb -tvi920b-vb-mc -tvi920b-vb-mc-100 -tvi920b-vb-mc-30 -tvi920b-vb-p -tvi920b-vb-unk -tvi921 -tvi924 -tvi925 -tvi925-hi -tvi92B -tvi92D -tvi950 -tvi950-2p -tvi950-4p -tvi950-rv -tvi950-rv-2p -tvi950-rv-4p -tvipt -vanilla -vc303 -vc404 -vc404-s -vc414 -vc415 -vi200 -vi200-f -vi200-rv -vi50 -vi500 -vi50adm -vi55 -viewpoint -vp3a+ -vp60 -vp90 -vremote -vt100+enq -vt100+fnkeys -vt100+keypad -vt100+pfkeys -vt100-s -vt102+enq -vt200-js -vt220+keypad -vt50h -vt52 -vt61 -wsiris -wy100 -wy100q -wy120 -wy120-25 -wy120-vb -wy160 -wy160-25 -wy160-42 -wy160-43 -wy160-tek -wy160-tek-100 -wy160-tek-30 -wy160-vb -wy30 -wy30-mc -wy30-mc-100 -wy30-mc-30 -wy30-vb -wy325 -wy325-25 -wy325-42 -wy325-43 -wy325-vb -wy350 -wy350-100 -wy350-30 -wy350-vb -wy350-vb-100 -wy350-vb-30 -wy350-w -wy350-w-100 -wy350-w-30 -wy350-wvb -wy350-wvb-100 -wy350-wvb-30 -wy370 -wy370-100 -wy370-105k -wy370-105k-100 -wy370-105k-30 -wy370-30 -wy370-EPC -wy370-EPC-100 -wy370-EPC-30 -wy370-nk -wy370-nk-100 -wy370-nk-30 -wy370-rv -wy370-rv-100 -wy370-rv-30 -wy370-tek -wy370-tek-100 -wy370-tek-30 -wy370-vb -wy370-vb-100 -wy370-vb-30 -wy370-w -wy370-w-100 -wy370-w-30 -wy370-wvb -wy370-wvb-100 -wy370-wvb-30 -wy50 -wy50-mc -wy50-mc-100 -wy50-mc-30 -wy50-vb -wy60 -wy60-25 -wy60-42 -wy60-43 -wy60-vb -wy99-ansi -wy99a-ansi -wy99f -wy99fa -wy99gt -wy99gt-25 -wy99gt-vb -wy99gt-tek -wyse-vp -xerox1720 -xerox820 -xfce -xnuppc+100x37 -xnuppc+112x37 -xnuppc+128x40 -xnuppc+128x48 -xnuppc+144x48 -xnuppc+160x64 -xnuppc+200x64 -xnuppc+200x75 -xnuppc+256x96 -xnuppc+80x25 -xnuppc+80x30 -xnuppc+90x30 -xnuppc+c -xnuppc+c-100 -xnuppc+c-30 -xtalk -xtalk-100 -xtalk-30 -xterm+256color -xterm+256color-100 -xterm+256color-30 -xterm+88color -xterm+88color-100 -xterm+88color-30 -xterm+app -xterm+edit -xterm+noapp -xterm+pc+edit -xterm+pcc0 -xterm+pcc1 -xterm+pcc2 -xterm+pcc3 -xterm+pce2 -xterm+pcf0 -xterm+pcf2 -xterm+pcfkeys -xterm+r6f2 -xterm+vt+edit -xterm-vt52 -z100 -z100bw -z29 -zen30 -zen50 -ztx -""".split() - -#: Message displayed when terminal containing binary-packed sequences -#: is instantiated -- the 'warnings' module is used and may be filtered away. -BINTERM_UNSUPPORTED_MSG = ( - u"Terminal kind {0!r} contains binary-packed capabilities, blessed " - u"is likely to fail to measure the length of its sequences.") - -__all__ = ('BINARY_TERMINALS', 'BINTERM_UNSUPPORTED_MSG',) diff --git a/blessed/sequences.py b/blessed/sequences.py index 803260c0..70fbc2e9 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -1,7 +1,5 @@ # encoding: utf-8 """This module provides 'sequence awareness'.""" -# pylint: disable=too-many-lines -# Too many lines in module (1027/1000) # std imports import collections import functools @@ -11,462 +9,119 @@ import warnings # local -from blessed._binterms import BINARY_TERMINALS, BINTERM_UNSUPPORTED_MSG +from blessed._capabilities import ( + CAPABILITIES_CAUSE_MOVEMENT, + CAPABILITIES_RAW_MIXIN, + CAPABILITY_DATABASE, +) # 3rd party import wcwidth import six -__all__ = ('init_sequence_patterns', - 'Sequence', - 'SequenceTextWrapper', - 'iter_parse') +__all__ = ('Sequence', 'SequenceTextWrapper') +class Termcap(): + def __init__(self, name, pattern, attribute): + """ + :param str name: name describing capability. + :param str pattern: regular expression string. + :param str attribute: :class:`~.Terminal` attribute used to build + this terminal capability. + """ + self.name = name + self.pattern = pattern + self.attribute = attribute + self._re_compiled = None + + def __repr__(self): + return ''.format(self=self) + + @property + def re_compiled(self): + if self._re_compiled is None: + self._re_compiled = re.compile(self.pattern) + return self._re_compiled + + @property + def named_pattern(self): + return '(?P<{self.name}>{self.pattern})'.format(self=self) + + @property + def will_move(self): + """Whether capability causes cursor movement.""" + return self.name in CAPABILITIES_CAUSE_MOVEMENT + + def horizontal_distance(self, text): + """ + Horizontal carriage adjusted by capability, may be negative! -class TextPart(collections.namedtuple('TextPart', ( - 'ucs', 'is_sequence', 'name', 'params'))): - """ - Describes either a terminal sequence or a series of printable characters. - - .. py:attribute:: ucs - - ucs str of terminal sequence or printable characters - - .. py:attribute:: is_sequence - - bool of whether this is a terminal sequence - - .. py:attribute:: name - - str of capability name or descriptive name of the terminal - sequence, or None if not a terminal sequence - - .. py:attribute:: params - - a tuple of str parameters in the terminal sequence, - or None if not a terminal sequence - """ - - -def _sort_sequences(regex_seqlist): - """ - Sort, filter, and return ``regex_seqlist`` in ascending order of length. - - :arg list regex_seqlist: list of strings. - :rtype: list - :returns: given list filtered and sorted. - - Any items that are Falsey (such as ``None``, ``''``) are removed from - the return list. The longest expressions are returned first. - Merge a list of input sequence patterns for use in a regular expression. - Order by lengthyness (full sequence set precedent over subset), - and exclude any empty (u'') sequences. - """ - # The purpose of sorting longest-first, is that we should want to match - # a complete, longest-matching final sequence in preference of a - # shorted sequence that partially matches another. This does not - # typically occur for output sequences, though with so many - # programmatically generated regular expressions for so many terminal - # types, it is feasible. - return sorted((x for x in regex_seqlist if x and x[1]), - key=lambda x: len(x[1]), - reverse=True) - - -def _unique_names(regex_seqlist): - """ - Return a list of (name, pattern) pairs liki input but with names unique. - - By adding _1, _2, _3 etc. to the name of each name_pair sequence the - first element of each tuple - - :arg list regex_seqlist: list of tuples of (str name, str pattern) - :rtype: list - :returns: a copy of the input list with names modified to be unique - """ - counter = collections.Counter() - - def inc_get(key): - counter[key] += 1 - return counter[key] - - return [(u"{}_{}".format(name, inc_get(name)), pattern) - for name, pattern in regex_seqlist] - - -def _build_numeric_capability(term, cap, optional=False, - base_num=99, nparams=1): - r""" - Return regular expressions for capabilities containing specified digits. - - This differs from function :func:`_build_any_numeric_capability` - in that, for the given ``base_num`` and ``nparams``, the value of - ``-1``, through ``+1`` inclusive is replaced - by regular expression pattern ``\d``. Any other digits found are - *not* replaced. - - :arg blessed.Terminal term: :class:`~.Terminal` instance. - :arg str cap: terminal capability name. - :arg int base_num: the numeric to use for parameterized capability. - :arg int nparams: the number of parameters to use for capability. - :rtype: tuple - :returns: tuple of - - str name of capture group used in identifying regular expression - - str regular expression for extracting the params of a capability - - """ - _cap = getattr(term, cap) - opt = '?' if optional else '' - if _cap: - args = (base_num,) * nparams - cap_re = re.escape(_cap(*args)) - for num in range(base_num - 1, base_num + 2): - # search for matching ascii, n-1 through n+1 - if str(num) in cap_re: - # modify & return n to matching digit expression - cap_re = cap_re.replace(str(num), r'(\d+){0}'.format(opt)) - return cap, cap_re - warnings.warn('Unknown parameter in {0!r}, {1!r}: {2!r})' - .format(cap, _cap, cap_re), UserWarning) - return None # no such capability - - -def _build_any_numeric_capability(term, cap, num=99, nparams=1): - r""" - Return regular expression for capabilities containing any numerics. - - :arg blessed.Terminal term: :class:`~.Terminal` instance. - :arg str cap: terminal capability name. - :arg int num: the numeric to use for parameterized capability. - :arg int nparams: the number of parameters to use for capability. - :rtype: tuple - :returns: tuple of - - str name of capture group used in - - str regular expression for extracting the params of a capability - - Build regular expression from capabilities having *any* digit parameters: - substitute any matching ``\d`` with literal ``\d`` and return. - """ - _cap = getattr(term, cap) - if _cap: - cap_re = re.escape(_cap(*((num,) * nparams))) - cap_re = re.sub(r'(\d+)', r'(\d+)', cap_re) - if r'(\d+)' in cap_re: - return cap, cap_re - warnings.warn('Missing numerics in {0!r}: {1!r}' - .format(cap, cap_re), UserWarning) - return None # no such capability - - -def _build_simple_capability(term, cap): - r""" - Return regular expression for capabilities which do not take parameters. - - :arg blessed.Terminal term: :class:`~.Terminal` instance. - :arg str cap: terminal capability name. - :rtype: str - :returns: regular expression for the given capability. - """ - _cap = getattr(term, cap) - if _cap: - cap_re = re.escape(_cap) - return cap, cap_re - return None - - -def get_movement_sequence_patterns(term): - """ - Get list of regular expressions for sequences that cause movement. - - :arg blessed.Terminal term: :class:`~.Terminal` instance. - :rtype: list - """ - bnc = functools.partial(_build_numeric_capability, term) - bsc = functools.partial(_build_simple_capability, term) - - return list(filter(None, [ - # carriage_return - bsc(cap='cr'), - # column_address: Horizontal position, absolute - bnc(cap='hpa'), - # row_address: Vertical position #1 absolute - bnc(cap='vpa'), - # cursor_address: Move to row #1 columns #2 - bnc(cap='cup', nparams=2), - # cursor_down: Down one line - bsc(cap='ud1'), - # cursor_home: Home cursor (if no cup) - bsc(cap='home'), - # cursor_left: Move left one space - bsc(cap='cub1'), - # cursor_right: Non-destructive space (move right one space) - bsc(cap='cuf1'), - # cursor_up: Up one line - bsc(cap='cuu1'), - # param_down_cursor: Down #1 lines - bnc(cap='cud', optional=True), - # restore_cursor: Restore cursor to position of last save_cursor - bsc(cap='rc'), - # clear_screen: clear screen and home cursor - bsc(cap='clear'), - # enter/exit_fullscreen: switch to alternate screen buffer - bsc(cap='enter_fullscreen'), - bsc(cap='exit_fullscreen'), - # forward cursor - ('_cuf', term._cuf), - # backward cursor - ('_cub', term._cub), - ])) - - -def get_wontmove_sequence_patterns(term): - """ - Get list of regular expressions for sequences not causing movement. - - :arg blessed.Terminal term: :class:`~.Terminal` instance. - :rtype: list - """ - bnc = functools.partial(_build_numeric_capability, term) - bna = functools.partial(_build_any_numeric_capability, term) - bsc = functools.partial(_build_simple_capability, term) - - # pylint: disable=bad-builtin - # Used builtin function 'map' - return list(filter(None, [ - # print_screen: Print contents of screen - bsc(cap='mc0'), - # prtr_off: Turn off printer - bsc(cap='mc4'), - # prtr_on: Turn on printer - bsc(cap='mc5'), - # save_cursor: Save current cursor position (P) - bsc(cap='sc'), - # set_tab: Set a tab in every row, current columns - bsc(cap='hts'), - # enter_bold_mode: Turn on bold (extra bright) mode - bsc(cap='bold'), - # enter_standout_mode - bsc(cap='standout'), - # enter_subscript_mode - bsc(cap='subscript'), - # enter_superscript_mode - bsc(cap='superscript'), - # enter_underline_mode: Begin underline mode - bsc(cap='underline'), - # enter_blink_mode: Turn on blinking - bsc(cap='blink'), - # enter_dim_mode: Turn on half-bright mode - bsc(cap='dim'), - # cursor_invisible: Make cursor invisible - bsc(cap='civis'), - # cursor_visible: Make cursor very visible - bsc(cap='cvvis'), - # cursor_normal: Make cursor appear normal (undo civis/cvvis) - bsc(cap='cnorm'), - # clear_all_tabs: Clear all tab stops - bsc(cap='tbc'), - # change_scroll_region: Change region to line #1 to line #2 - bnc(cap='csr', nparams=2), - # clr_bol: Clear to beginning of line - bsc(cap='el1'), - # clr_eol: Clear to end of line - bsc(cap='el'), - # clr_eos: Clear to end of screen - bsc(cap='clear_eos'), - # delete_character: Delete character - bsc(cap='dch1'), - # delete_line: Delete line (P*) - bsc(cap='dl1'), - # erase_chars: Erase #1 characters - bnc(cap='ech'), - # insert_line: Insert line (P*) - bsc(cap='il1'), - # parm_dch: Delete #1 characters - bnc(cap='dch'), - # parm_delete_line: Delete #1 lines - bnc(cap='dl'), - # exit_alt_charset_mode: End alternate character set (P) - bsc(cap='rmacs'), - # exit_am_mode: Turn off automatic margins - bsc(cap='rmam'), - # exit_attribute_mode: Turn off all attributes - bsc(cap='sgr0'), - # exit_ca_mode: Strings to end programs using cup - bsc(cap='rmcup'), - # exit_insert_mode: Exit insert mode - bsc(cap='rmir'), - # exit_standout_mode: Exit standout mode - bsc(cap='rmso'), - # exit_underline_mode: Exit underline mode - bsc(cap='rmul'), - # flash_hook: Flash switch hook - bsc(cap='hook'), - # flash_screen: Visible bell (may not move cursor) - bsc(cap='flash'), - # keypad_local: Leave 'keyboard_transmit' mode - bsc(cap='rmkx'), - # keypad_xmit: Enter 'keyboard_transmit' mode - bsc(cap='smkx'), - # meta_off: Turn off meta mode - bsc(cap='rmm'), - # meta_on: Turn on meta mode (8th-bit on) - bsc(cap='smm'), - # orig_pair: Set default pair to its original value - bsc(cap='op'), - # parm_ich: Insert #1 characters - bnc(cap='ich'), - # parm_index: Scroll forward #1 - bnc(cap='indn'), - # parm_insert_line: Insert #1 lines - bnc(cap='il'), - # erase_chars: Erase #1 characters - bnc(cap='ech'), - # parm_rindex: Scroll back #1 lines - bnc(cap='rin'), - # parm_up_cursor: Up #1 lines - bnc(cap='cuu'), - # scroll_forward: Scroll text up (P) - bsc(cap='ind'), - # scroll_reverse: Scroll text down (P) - bsc(cap='rev'), - # tab: Tab to next 8-space hardware tab stop - bsc(cap='ht'), - # set_a_background: Set background color to #1, using ANSI escape - bna(cap='setab', num=1), - bna(cap='setab', num=(term.number_of_colors - 1)), - # set_a_foreground: Set foreground color to #1, using ANSI escape - bna(cap='setaf', num=1), - bna(cap='setaf', num=(term.number_of_colors - 1)), - ] + [ - # set_attributes: Define video attributes #1-#9 (PG9) - # ( not *exactly* legal, being extra forgiving. ) - bna(cap='sgr', nparams=_num) for _num in range(1, 10) - # reset_{1,2,3}string: Reset string - ] + list(map(bsc, ('r1', 'r2', 'r3'))))) - - -def strip_groups(pattern): - """Return version of str regex without unnamed capture groups.""" - # make capture groups non-capturing - # TODO this incorrectly replaces () in r'[()]' -> [(?:)] - return re.sub(r'(?{})".format(c, strip_groups(s)) - for c, s in _will_move if s)) - _re_wont_move = re.compile(u'|'.join( - u"(?P<{}>{})".format(c, strip_groups(s)) - for c, s in _wont_move if s)) - if (_will_move and _wont_move and set(list(zip(*_will_move))[0]) & - set(list(zip(*_wont_move))[0])): - # _param_extractors requires names be unique - raise ValueError("will_move and wont_move contain same capname") - _param_extractors = dict((c, re.compile(s)) - for c, s in _will_move + _wont_move) - - # static pattern matching for horizontal_distance(ucs, term) - bnc = functools.partial(_build_numeric_capability, term) - - # parm_right_cursor: Move #1 characters to the right - _cuf_pair = bnc(cap='cuf', optional=True) - _re_cuf = re.compile(_cuf_pair[1]) if _cuf_pair else None - - # cursor_right: Non-destructive space (move right one space) - _cuf1 = term.cuf1 - - # parm_left_cursor: Move #1 characters to the left - _cub_pair = bnc(cap='cub', optional=True) - _re_cub = re.compile(_cub_pair[1]) if _cub_pair else None - - # cursor_left: Move left one space - _cub1 = term.cub1 - - return {'_re_will_move': _re_will_move, - '_re_wont_move': _re_wont_move, - '_param_extractors': _param_extractors, - '_re_cuf': _re_cuf, - '_re_cub': _re_cub, - '_cuf1': _cuf1, - '_cub1': _cub1, } + pattern = re.sub(r'(\d+)', _numeric_regex, _outp) + return cls(name, pattern, attribute) class SequenceTextWrapper(textwrap.TextWrapper): @@ -550,15 +205,12 @@ def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width): if self.break_long_words: term = self.term chunk = reversed_chunks[-1] - for idx, part in enumerate_by_position(iter_parse(term, chunk)): - nxt = idx + len(part.ucs) + idx = nxt = 0 + for text, cap in iter_parse(term, chunk): + nxt += len(text) if Sequence(chunk[:nxt], term).length() > space_left: break - else: - # our text ends with a sequence, such as in text - # u'!\x1b(B\x1b[m', set index at at end (nxt) - idx = idx + len(part.ucs) - + idx = nxt cur_line.append(chunk[:idx]) reversed_chunks[-1] = chunk[idx:] @@ -577,7 +229,6 @@ def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width): SequenceTextWrapper.__doc__ = textwrap.TextWrapper.__doc__ - class Sequence(six.text_type): """ A "sequence-aware" version of the base :class:`str` class. @@ -591,8 +242,8 @@ def __new__(cls, sequence_text, term): """ Class constructor. - :arg sequence_text: A string that may contain sequences. - :arg blessed.Terminal term: :class:`~.Terminal` instance. + :param sequence_text: A string that may contain sequences. + :param blessed.Terminal term: :class:`~.Terminal` instance. """ new = six.text_type.__new__(cls, sequence_text) new._term = term @@ -602,9 +253,9 @@ def ljust(self, width, fillchar=u' '): """ Return string containing sequences, left-adjusted. - :arg int width: Total width given to right-adjust ``text``. If + :param int width: Total width given to right-adjust ``text``. If unspecified, the width of the attached terminal is used (default). - :arg str fillchar: String for padding right-of ``text``. + :param str fillchar: String for padding right-of ``text``. :returns: String of ``text``, right-aligned by ``width``. :rtype: str """ @@ -616,9 +267,9 @@ def rjust(self, width, fillchar=u' '): """ Return string containing sequences, right-adjusted. - :arg int width: Total width given to right-adjust ``text``. If + :param int width: Total width given to right-adjust ``text``. If unspecified, the width of the attached terminal is used (default). - :arg str fillchar: String for padding left-of ``text``. + :param str fillchar: String for padding left-of ``text``. :returns: String of ``text``, right-aligned by ``width``. :rtype: str """ @@ -630,9 +281,9 @@ def center(self, width, fillchar=u' '): """ Return string containing sequences, centered. - :arg int width: Total width given to center ``text``. If + :param int width: Total width given to center ``text``. If unspecified, the width of the attached terminal is used (default). - :arg str fillchar: String for padding left and right-of ``text``. + :param str fillchar: String for padding left and right-of ``text``. :returns: String of ``text``, centered by ``width``. :rtype: str """ @@ -656,7 +307,7 @@ def length(self): Unified Ideographs (Chinese, Japanese, Korean) defined by Unicode as half or full-width characters. """ - # because combining characters may return -1, "clip" their length to 0. + # because control characters may return -1, "clip" their length to 0. clip = functools.partial(max, 0) return sum(clip(wcwidth.wcwidth(w_char)) for w_char in self.strip_seqs()) @@ -681,7 +332,7 @@ def strip(self, chars=None): """ Return string of sequences, leading, and trailing whitespace removed. - :arg str chars: Remove characters in chars instead of whitespace. + :param str chars: Remove characters in chars instead of whitespace. :rtype: str """ return self.strip_seqs().strip(chars) @@ -690,7 +341,7 @@ def lstrip(self, chars=None): """ Return string of all sequences and leading whitespace removed. - :arg str chars: Remove characters in chars instead of whitespace. + :param str chars: Remove characters in chars instead of whitespace. :rtype: str """ return self.strip_seqs().lstrip(chars) @@ -699,7 +350,7 @@ def rstrip(self, chars=None): """ Return string of all sequences and trailing whitespace removed. - :arg str chars: Remove characters in chars instead of whitespace. + :param str chars: Remove characters in chars instead of whitespace. :rtype: str """ return self.strip_seqs().rstrip(chars) @@ -723,289 +374,35 @@ def strip_seqs(self): (such as ``\b`` or ``term.cuf(5)``) are replaced by destructive space or erasing. """ - # nxt: points to first character beyond current escape sequence. - # width: currently estimated display length. - inp = self.padd() - return ''.join(f.ucs - for f in iter_parse(self._term, inp) - if not f.is_sequence) + gen = iter_parse(self._term, self.padd()) + return u''.join(text for text, cap in gen if not cap) def padd(self): - r""" - Transform non-destructive space or backspace into destructive ones. - - >>> from blessed import Terminal - >>> from blessed.sequences import Sequence - >>> term = Terminal() - >>> seq = term.cuf(10) + '-->' + '\b\b' - >>> padded = Sequence(seq, Terminal()).padd() - >>> print(seq, padded) - (u'\x1b[10C-->\x08\x08', u' -') + """ + Return non-destructive horizontal movement as destructive spacing. :rtype: str - - This method is used to determine the printable width of a string, - and is the first pass of :meth:`strip_seqs`. - - Where sequence ``term.cuf(n)`` is detected, it is replaced with - ``n * u' '``, and where sequence ``term.cub1(n)`` or ``\\b`` is - detected, those last-most characters are destroyed. """ - outp = u'' - for ucs, _, _, _ in iter_parse(self._term, self): - width = horizontal_distance(ucs, self._term) - if width > 0: - outp += u' ' * width - elif width < 0: - outp = outp[:width] + outp = '' + for text, cap in iter_parse(self._term, self): + if not cap: + outp += text + continue + + value = cap.horizontal_distance(text) + if value > 0: + outp += ' ' * value + elif value < 0: + outp = outp[:value] else: - outp += ucs + outp += text return outp - -def measure_length(ucs, term): - r""" - Return non-zero for string ``ucs`` that begins with a terminal sequence. - - :arg str ucs: String that may begin with a terminal sequence. - :arg blessed.Terminal term: :class:`~.Terminal` instance. - :rtype: int - :returns: length of the sequence beginning at ``ucs``, if any. - Otherwise 0 if ``ucs`` does not begin with a terminal - sequence. - - Returns non-zero for string ``ucs`` that begins with a terminal - sequence, of the length of characters in ``ucs`` until the *first* - matching sequence ends. - - This is used as a *next* pointer to iterate over sequences. If the string - ``ucs`` does not begin with a sequence, ``0`` is returned. - - A sequence may be a typical terminal sequence beginning with Escape - (``\x1b``), especially a Control Sequence Initiator (``CSI``, ``\x1b[``, - ...), or those of ``\a``, ``\b``, ``\r``, ``\n``, ``\xe0`` (shift in), - and ``\x0f`` (shift out). They do not necessarily have to begin with CSI, - they need only match the capabilities of attributes ``_re_will_move`` and - ``_re_wont_move`` of :class:`~.Terminal` which are constructed at time - of class initialization. - """ - # simple terminal control characters, - ctrl_seqs = u'\a\b\r\n\x0e\x0f' - - if any([ucs.startswith(_ch) for _ch in ctrl_seqs]): - return 1 - - # known multibyte sequences, - matching_seq = term and ( - term._re_will_move.match(ucs) or - term._re_wont_move.match(ucs) or - term._re_cub and term._re_cub.match(ucs) or - term._re_cuf and term._re_cuf.match(ucs) - ) - - if matching_seq: - _, end = matching_seq.span() - assert identify_part(term, ucs[:end]) - return end - - # none found, must be printable! - return 0 - - -def termcap_distance(ucs, cap, unit, term): - r""" - Return distance of capabilities ``cub``, ``cub1``, ``cuf``, and ``cuf1``. - - :arg str ucs: Terminal sequence created using any of ``cub(n)``, ``cub1``, - ``cuf(n)``, or ``cuf1``. - :arg str cap: ``cub`` or ``cuf`` only. - :arg int unit: Unit multiplier, should always be ``1`` or ``-1``. - :arg blessed.Terminal term: :class:`~.Terminal` instance. - :rtype: int - :returns: the printable distance determined by the given sequence. If - the given sequence does not match any of the ``cub`` or ``cuf`` - - This supports the higher level function :func:`horizontal_distance`. - - Match horizontal distance by simple ``cap`` capability name, either - from termcap ``cub`` or ``cuf``, with string matching the sequences - identified by Terminal instance ``term`` and a distance of ``unit`` - *1* or *-1*, for right and left, respectively. - - Otherwise, by regular expression (using dynamic regular expressions built - when :class:`~.Terminal` is first initialized) of ``cub(n)`` and - ``cuf(n)``. Failing that, any of the standard SGR sequences - (``\033[C``, ``\033[D``, ``\033[C``, ``\033[D``). - - Returns 0 if unmatched. - """ - assert cap in ('cuf', 'cub'), cap - assert unit in (1, -1), unit - # match cub1(left), cuf1(right) - one = getattr(term, '_{0}1'.format(cap)) - if one and ucs.startswith(one): - return unit - - # match cub(n), cuf(n) using regular expressions - re_pattern = getattr(term, '_re_{0}'.format(cap)) - _dist = re_pattern and re_pattern.match(ucs) - if _dist: - return unit * int(_dist.group(1)) - - return 0 - - -def horizontal_distance(ucs, term): - r""" - Determine the horizontal distance of single terminal sequence, ``ucs``. - - :arg ucs: terminal sequence, which may be any of the following: - - - move_right (fe. ``[C``): returns value ``(n)``. - - move left (fe. ``[D``): returns value ``-(n)``. - - backspace (``\b``) returns value -1. - - tab (``\t``) returns value 8. - - :arg blessed.Terminal term: :class:`~.Terminal` instance. - :rtype: int - - .. note:: Tabstop (``\t``) cannot be correctly calculated, as the relative - column position cannot be determined: 8 is always (and, incorrectly) - returned. - """ - if ucs.startswith('\b'): - return -1 - - elif ucs.startswith('\t'): - # As best as I can prove it, a tabstop is always 8 by default. - # Though, given that blessed is: - # - # 1. unaware of the output device's current cursor position, and - # 2. unaware of the location the callee may chose to output any - # given string, - # - # It is not possible to determine how many cells any particular - # \t would consume on the output device! - return 8 - - return (termcap_distance(ucs, 'cub', -1, term) or - termcap_distance(ucs, 'cuf', 1, term) or - 0) - - -def identify_part(term, ucs): - """ - Return a TextPart instance describing the terminal sequence in ucs. - - :arg str ucs: text beginning with a terminal sequence - :arg blessed.Terminal term: :class:`~.Terminal` instance. - :raises ValueError: ucs is not a valid terminal sequence. - :rtype: TextPart - - TextPart is a :class:`collections.namedtuple` instance describing - either a terminal sequence or a series of printable characters. - Its parameters are: - - - ``ucs``: str of terminal sequence or printable characters - - ``is_sequence``: bool for whether this is a terminal sequence - - ``name``: str of capability name or descriptive name of the - terminal sequence, or None if not a terminal sequence - - ``params``: a tuple of str parameters in the terminal sequence, - or None if not a terminal sequence - - """ - # simple terminal control characters, - ctrl_seqs = u'\a\b\r\n\x0e\x0f' - - if any([ucs.startswith(_ch) for _ch in ctrl_seqs]): - return TextPart(ucs[:1], True, '?', None) - - matching_seq = term and ( - term._re_will_move.match(ucs) or - term._re_wont_move.match(ucs) - ) - if matching_seq: - (identifier, ) = (k for k, v in matching_seq.groupdict().items() - if v is not None) - name = identifier - params = term._param_extractors[identifier].match(ucs).groups() - return TextPart(matching_seq.group(), True, - name, params if params else None) - - # known multibyte sequences, - matching_seq = term and ( - term._re_cub and term._re_cub.match(ucs) or - term._re_cuf and term._re_cuf.match(ucs) - ) - - if matching_seq: - return TextPart(matching_seq.group(), True, '?', None) - - raise ValueError("identify_part called on nonsequence " - "{!r}".format(ucs)) - - -def enumerate_by_position(parts): - """Iterate over TextParts with an index, subdividing printable strings. - - The index is the length of characters preceding that TextPart, in other - words the cumulative sum of lengths of TextParts before the current one. - TextPart instances composed of multiple printable characters (those not - part of a terminal sequence) will be broken into multiple TextPart - instances, one per character. - - This is useful for splitting text into its smallest indivisible TextPart - units: splitting strings into characters while not breaking up terminal - sequences. - - :arg: parts: iterable of TextPart instances - :rtype: iterator of tuple pairs of (int, TextPart) - - """ - idx = 0 - for part in parts: - if part.is_sequence: - yield idx, part - idx += len(part.ucs) - else: - for char in part.ucs: - yield idx, TextPart(char, False, None, None) - idx += 1 - - def iter_parse(term, ucs): - r""" - Return an iterator of TextPart instances: terminal sequences or strings. - - :arg ucs: str which may contain terminal sequences - :arg blessed.Terminal term: :class:`~.Terminal` instance. - :rtype: iterator of TextPart instances - - TextPart is a :class:`collections.namedtuple` instance describing - either a terminal sequence or a series of printable characters. - Its parameters are: - - - ``ucs``: str of terminal sequence or printable characters - - ``is_sequence``: bool for whether this is a terminal sequence - - ``name``: str of capability name or descriptive name of the terminal - sequence, or None if not a terminal sequence - - ``params``: a tuple of str parameters in the terminal sequence, - or None if not a terminal sequence - """ - outp = u'' - idx = 0 - while idx < six.text_type.__len__(ucs): - length = measure_length(ucs[idx:], term) - if length == 0: - outp += ucs[idx] - idx += 1 + for match in re.finditer(term._caps_compiled_any, text): + name = match.lastgroup + value = match.group(name) + if name == 'MISMATCH': + yield (value, None) continue - if outp: - yield TextPart(outp, False, None, None) - outp = u'' - yield identify_part(term, ucs[idx:idx + length]) - idx += length - - if outp: - # ucs ends with printable characters - yield TextPart(outp, False, None, None) + yield value, term.caps[name] diff --git a/blessed/terminal.py b/blessed/terminal.py index 1f88233b..345a3660 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -38,6 +38,15 @@ # alias py2 exception to py3 InterruptedError = select.error +try: + from collections import OrderedDict +except ImportError: + # python 2.6 requires 3rd party library (backport) + # + # pylint: disable=import-error + # Unable to import 'ordereddict' + from ordereddict import OrderedDict + # local imports from .formatters import (ParameterizingString, NullCallableString, @@ -45,11 +54,18 @@ resolve_attribute, ) -from .sequences import (init_sequence_patterns, - _build_any_numeric_capability, - SequenceTextWrapper, - Sequence, +#_build_any_numeric_capability, XXX wtf? + +from ._capabilities import ( + CAPABILITIES_RAW_MIXIN, + CAPABILITIES_ADDITIVES, + CAPABILITY_DATABASE, +) + +from .sequences import (SequenceTextWrapper, iter_parse, + Sequence, + Termcap, ) from .keyboard import (get_keyboard_sequences, @@ -108,7 +124,12 @@ class Terminal(object): superscript='ssupm', no_superscript='rsupm', underline='smul', - no_underline='rmul') + no_underline='rmul', + cursor_report='u6', + cursor_request='u7', + terminal_answerback='u8', + terminal_enquire='u9', + ) def __init__(self, kind=None, stream=None, force_styling=False): """ @@ -159,6 +180,7 @@ def __init__(self, kind=None, stream=None, force_styling=False): except io.UnsupportedOperation: stream_fd = None + self._stream = stream self._is_a_tty = stream_fd is not None and os.isatty(stream_fd) self._does_styling = ((self.is_a_tty or force_styling) and force_styling is not None) @@ -202,13 +224,46 @@ def __init__(self, kind=None, stream=None, force_styling=False): ' returned for the remainder of this process.' % ( self._kind, _CUR_TERM,)) + # initialize capabilities database + self.__init__capabilities() + + self.__init__keycodes() + + def __init__capabilities(self): + # important that we lay these in their ordered direction, so that our + # preferred, 'color' over 'set_a_attributes1', for example. + self.caps = OrderedDict() + + # some static injected patterns, esp. without named attribute access. + for name, (attribute, pattern) in CAPABILITIES_ADDITIVES.items(): + self.caps[name] = Termcap(name, pattern, attribute) + + for name, (attribute, kwds) in CAPABILITY_DATABASE.items(): + if self.does_styling: + # attempt dynamic lookup + cap = getattr(self, attribute) + if cap: + self.caps[name] = Termcap.build( + name, cap, attribute, **kwds) + continue + + # fall-back + pattern = CAPABILITIES_RAW_MIXIN.get(name) + if pattern: + self.caps[name] = Termcap(name, pattern, attribute) + + # make a compiled named regular expression table, the matching + # '.lastgroup' is the primary lookup key for 'self.caps'. + self.caps_compiled = re.compile( + '|'.join(cap.named_pattern for name, cap in self.caps.items())) + + # for tokenizer + self._caps_compiled_any = re.compile('|'.join( + cap.named_pattern for name, cap in self.caps.items() + ) + '|(?P.)') + + def __init__keycodes(self): # Initialize keyboard data determined by capability. - # - # The following attributes are initialized: _keycodes, - # _keymap, _keyboard_buf, _encoding, and _keyboard_decoder. - for re_name, re_val in init_sequence_patterns(self).items(): - setattr(self, re_name, re_val) - # Build database of int code <=> KEY_NAME. self._keycodes = get_keyboard_codes() @@ -235,7 +290,7 @@ def __init__(self, kind=None, stream=None, force_styling=False): self._keyboard_decoder = codecs.getincrementaldecoder( self._encoding)() - self._stream = stream + def __getattr__(self, attr): r""" @@ -466,13 +521,7 @@ def get_location(self, timeout=None): query_str = self.u7 or u'\x1b[6n' # determine response format as a regular expression - response_re = re.escape(u'\x1b') + r'\[(\d+)\;(\d+)R' - - if self.u6: - with warnings.catch_warnings(): - response_re = _build_any_numeric_capability( - term=self, cap='u6', nparams=2 - ) or response_re + response_re = self.caps['cursor_report'].re_compiled # Avoid changing user's desired raw or cbreak mode if already entered, # by entering cbreak mode ourselves. This is necessary to receive user @@ -776,6 +825,21 @@ def strip_seqs(self, text): """ return Sequence(text, self).strip_seqs() + def split_seqs(self, text, maxsplit=0, flags=0): + r""" + Return ``text`` split by terminal sequences. + + :param int maxpslit: If maxsplit is nonzero, at most ``maxsplit`` + splits occur, and the remainder of the string is returned as + the final element of the list. + :rtype: list[str] + """ + return list(filter(None, re.split(self.caps_compiled, text, + maxsplit=maxsplit, flags=flags))) + + def iter_parse(self, text): + return iter_parse(text) + def wrap(self, text, width=None, **kwargs): """ Text-wrap a string, returning a list of wrapped lines. @@ -1083,27 +1147,6 @@ def inkey(self, timeout=None, esc_delay=0.35, **_kwargs): self.ungetch(ucs[len(ks):]) return ks - def iter_parse(self, text): - r""" - Return iterator of TextPart instances: terminal sequences or strings. - - :arg ucs: str which may contain terminal sequences - :rtype: iterator of TextPart instances - - TextPart is a :class:`collections.namedtuple` instance describing - either a terminal sequence or a series of printable characters. - Its parameters are: - - - ``ucs``: str of terminal sequence or printable characters - - ``is_sequence``: bool for whether this is a terminal sequence - - ``name``: str of capability name or descriptive name of the - terminal sequence, or None if not a terminal sequence - - ``params``: a tuple of str parameters in the terminal sequence, - or None if not a terminal sequence - """ - return iter_parse(self, text) - - class WINSZ(collections.namedtuple('WINSZ', ( 'ws_row', 'ws_col', 'ws_xpixel', 'ws_ypixel'))): """ @@ -1144,7 +1187,8 @@ class WINSZ(collections.namedtuple('WINSZ', ( #: #: Python - perhaps wrongly - will not allow for re-initialisation of new #: terminals through :func:`curses.setupterm`, so the value of cur_term cannot -#: be changed once set: subsequent calls to :func:`setupterm` have no effect. +#: be changed once set: subsequent calls to :func:`curses.setupterm` have no +#: effect. #: #: Therefore, the :attr:`Terminal.kind` of each :class:`Terminal` is #: essentially a singleton. This global variable reflects that, and a warning From 39a4a4f884eab212d41e448bab6ce8edcfea0ddf Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 31 Oct 2015 23:25:56 -0700 Subject: [PATCH 328/459] remove more _binterms code --- blessed/tests/accessories.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/blessed/tests/accessories.py b/blessed/tests/accessories.py index a84522e4..d2bb09bb 100644 --- a/blessed/tests/accessories.py +++ b/blessed/tests/accessories.py @@ -15,7 +15,6 @@ # local from blessed import Terminal -from blessed._binterms import BINARY_TERMINALS # 3rd-party import pytest @@ -231,12 +230,6 @@ def unicode_parm(cap, *parms): return u'' -@pytest.fixture(params=BINARY_TERMINALS) -def unsupported_sequence_terminals(request): - """Terminals that emit warnings for unsupported sequence-awareness.""" - return request.param - - @pytest.fixture(params=all_terms_params) def all_terms(request): """Common kind values for all kinds of terminals.""" From 6dcd0407e9f2de25f2c9a042d7b48b1c97575dd6 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 31 Oct 2015 23:27:18 -0700 Subject: [PATCH 329/459] Revert "remove more _binterms code" This reverts commit 39a4a4f884eab212d41e448bab6ce8edcfea0ddf. --- blessed/tests/accessories.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/blessed/tests/accessories.py b/blessed/tests/accessories.py index d2bb09bb..a84522e4 100644 --- a/blessed/tests/accessories.py +++ b/blessed/tests/accessories.py @@ -15,6 +15,7 @@ # local from blessed import Terminal +from blessed._binterms import BINARY_TERMINALS # 3rd-party import pytest @@ -230,6 +231,12 @@ def unicode_parm(cap, *parms): return u'' +@pytest.fixture(params=BINARY_TERMINALS) +def unsupported_sequence_terminals(request): + """Terminals that emit warnings for unsupported sequence-awareness.""" + return request.param + + @pytest.fixture(params=all_terms_params) def all_terms(request): """Common kind values for all kinds of terminals.""" From 2e486e3bc8733cc7cdcd4a4b54745ad03519c0b3 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 31 Oct 2015 23:25:56 -0700 Subject: [PATCH 330/459] remove more _binterms code --- blessed/tests/accessories.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/blessed/tests/accessories.py b/blessed/tests/accessories.py index a84522e4..d2bb09bb 100644 --- a/blessed/tests/accessories.py +++ b/blessed/tests/accessories.py @@ -15,7 +15,6 @@ # local from blessed import Terminal -from blessed._binterms import BINARY_TERMINALS # 3rd-party import pytest @@ -231,12 +230,6 @@ def unicode_parm(cap, *parms): return u'' -@pytest.fixture(params=BINARY_TERMINALS) -def unsupported_sequence_terminals(request): - """Terminals that emit warnings for unsupported sequence-awareness.""" - return request.param - - @pytest.fixture(params=all_terms_params) def all_terms(request): """Common kind values for all kinds of terminals.""" From 5443ea7f0e8f8ee6b6f95553861a6709126b1899 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 31 Oct 2015 23:52:30 -0700 Subject: [PATCH 331/459] reduce grouped numerics except where used --- blessed/_capabilities.py | 8 ++++---- blessed/sequences.py | 18 +++++++++++++----- blessed/terminal.py | 2 +- blessed/tests/test_sequences.py | 1 - 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/blessed/_capabilities.py b/blessed/_capabilities.py index 5e60b3db..c2f7707a 100644 --- a/blessed/_capabilities.py +++ b/blessed/_capabilities.py @@ -28,13 +28,13 @@ ('clr_eol', ('el', {})), ('clr_eos', ('clear_eos', {})), ('column_address', ('hpa', {'nparams': 1})), - ('cursor_address', ('cup', {'nparams': 2})), + ('cursor_address', ('cup', {'nparams': 2, 'match_grouped': True})), ('cursor_down', ('cud1', {})), ('cursor_home', ('home', {})), ('cursor_invisible', ('civis', {})), ('cursor_left', ('cub1', {})), ('cursor_normal', ('cnorm', {})), - ('cursor_report', ('u6', {'nparams': 2})), + ('cursor_report', ('u6', {'nparams': 2, 'match_grouped': True})), ('cursor_right', ('cuf1', {})), ('cursor_up', ('cuu1', {})), ('cursor_visible', ('cvvis', {})), @@ -66,13 +66,13 @@ ('meta_on', ('smm', {})), ('orig_pair', ('op', {})), ('parm_down_cursor', ('cud', {'nparams': 1})), - ('parm_left_cursor', ('cub', {'nparams': 1})), + ('parm_left_cursor', ('cub', {'nparams': 1, 'match_grouped': True})), ('parm_dch', ('dch', {'nparams': 1})), ('parm_delete_line', ('dl', {'nparams': 1})), ('parm_ich', ('ich', {'nparams': 1})), ('parm_index', ('indn', {'nparams': 1})), ('parm_insert_line', ('il', {'nparams': 1})), - ('parm_right_cursor', ('cuf', {'nparams': 1})), + ('parm_right_cursor', ('cuf', {'nparams': 1, 'match_grouped': True})), ('parm_rindex', ('rin', {'nparams': 1})), ('parm_up_cursor', ('cuu', {'nparams': 1})), ('print_screen', ('mc0', {})), diff --git a/blessed/sequences.py b/blessed/sequences.py index 70fbc2e9..43d0a1dc 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -85,7 +85,8 @@ def horizontal_distance(self, text): @classmethod def build(cls, name, capability, attribute, nparams=0, - numeric=99, match_any=False, match_optional=False): + numeric=99, match_grouped=False, match_any=False, + match_optional=False): """ :param str name: Variable name given for this pattern. :param str capability: A unicode string representing a terminal @@ -95,6 +96,8 @@ def build(cls, name, capability, attribute, nparams=0, :param attribute: The terminfo(5) capability name by which this pattern is known. :param int nparams: number of positional arguments for callable. + :param bool match_grouped: If the numeric pattern should be + grouped, ``(\d+)`` when ``True``, ``\d+`` default. :param bool match_any: When keyword argument ``nparams`` is given, *any* numeric found in output is suitable for building as pattern ``(\d+)``. Otherwise, only the first matching value of @@ -103,9 +106,11 @@ def build(cls, name, capability, attribute, nparams=0, :param bool match_optional: When ``True``, building of numeric patterns containing ``(\d+)`` will be built as optional, ``(\d+)?``. """ - _numeric_regex = r'(\d+)' + _numeric_regex = r'\d+' + if match_grouped: + _numeric_regex = r'(\d+)' if match_optional: - _numeric_regex += '?' + _numeric_regex = r'(\d+)?' numeric = 99 if numeric is None else numeric # basic capability attribute, not used as a callable @@ -120,7 +125,10 @@ def build(cls, name, capability, attribute, nparams=0, pattern = _outp.replace(str(num), _numeric_regex) return cls(name, pattern, attribute) - pattern = re.sub(r'(\d+)', _numeric_regex, _outp) + if match_grouped: + pattern = re.sub(r'(\d+)', _numeric_regex, _outp) + else: + pattern = re.sub(r'\d+', _numeric_regex, _outp) return cls(name, pattern, attribute) @@ -399,7 +407,7 @@ def padd(self): return outp def iter_parse(term, ucs): - for match in re.finditer(term._caps_compiled_any, text): + for match in re.finditer(term._caps_compiled_any, ucs): name = match.lastgroup value = match.group(name) if name == 'MISMATCH': diff --git a/blessed/terminal.py b/blessed/terminal.py index 345a3660..7e9e3606 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -255,7 +255,7 @@ def __init__capabilities(self): # make a compiled named regular expression table, the matching # '.lastgroup' is the primary lookup key for 'self.caps'. self.caps_compiled = re.compile( - '|'.join(cap.named_pattern for name, cap in self.caps.items())) + '|'.join(cap.pattern for name, cap in self.caps.items())) # for tokenizer self._caps_compiled_any = re.compile('|'.join( diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index ba8275c0..7fe5a420 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -8,7 +8,6 @@ # local from .accessories import ( - unsupported_sequence_terminals, all_terms, as_subprocess, TestTerminal, From 279d2f625967b73de7142eb44adc73819b4c0e6d Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 1 Nov 2015 00:09:05 -0700 Subject: [PATCH 332/459] static analysis, remove term.iter_parse for now, I'm not sure term.iter_parse() is the right name. it wasn't even meant to become a public method of Terminal, but directly, 'from blessed.sequences import iter_parse' --- blessed/sequences.py | 52 +++++++++++++++++++++++++++----------------- blessed/terminal.py | 9 +------- 2 files changed, 33 insertions(+), 28 deletions(-) diff --git a/blessed/sequences.py b/blessed/sequences.py index 43d0a1dc..bad80f34 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -1,19 +1,13 @@ # encoding: utf-8 """This module provides 'sequence awareness'.""" # std imports -import collections import functools +import textwrap import math import re -import textwrap -import warnings # local -from blessed._capabilities import ( - CAPABILITIES_CAUSE_MOVEMENT, - CAPABILITIES_RAW_MIXIN, - CAPABILITY_DATABASE, -) +from blessed._capabilities import CAPABILITIES_CAUSE_MOVEMENT # 3rd party import wcwidth @@ -21,9 +15,13 @@ __all__ = ('Sequence', 'SequenceTextWrapper') -class Termcap(): + +class Termcap(object): + """Terminal capability of given variable name and pattern.""" def __init__(self, name, pattern, attribute): """ + Class initializer. + :param str name: name describing capability. :param str pattern: regular expression string. :param str attribute: :class:`~.Terminal` attribute used to build @@ -35,18 +33,20 @@ def __init__(self, name, pattern, attribute): self._re_compiled = None def __repr__(self): + # pylint: disable=redundant-keyword-arg return ''.format(self=self) + @property + def named_pattern(self): + # pylint: disable=redundant-keyword-arg + return '(?P<{self.name}>{self.pattern})'.format(self=self) + @property def re_compiled(self): if self._re_compiled is None: self._re_compiled = re.compile(self.pattern) return self._re_compiled - @property - def named_pattern(self): - return '(?P<{self.name}>{self.pattern})'.format(self=self) - @property def will_move(self): """Whether capability causes cursor movement.""" @@ -54,7 +54,7 @@ def will_move(self): def horizontal_distance(self, text): """ - Horizontal carriage adjusted by capability, may be negative! + Horizontal carriage adjusted by capability, may be negative. :rtype: int :param str text: for capabilities *parm_left_cursor*, @@ -83,11 +83,14 @@ def horizontal_distance(self, text): return 0 + # pylint: disable=too-many-arguments @classmethod def build(cls, name, capability, attribute, nparams=0, numeric=99, match_grouped=False, match_any=False, match_optional=False): - """ + r""" + Class factory builder for given capability definition. + :param str name: Variable name given for this pattern. :param str capability: A unicode string representing a terminal capability to build for. When ``nparams`` is non-zero, it @@ -214,7 +217,7 @@ def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width): term = self.term chunk = reversed_chunks[-1] idx = nxt = 0 - for text, cap in iter_parse(term, chunk): + for text, _ in iter_parse(term, chunk): nxt += len(text) if Sequence(chunk[:nxt], term).length() > space_left: break @@ -237,6 +240,7 @@ def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width): SequenceTextWrapper.__doc__ = textwrap.TextWrapper.__doc__ + class Sequence(six.text_type): """ A "sequence-aware" version of the base :class:`str` class. @@ -406,11 +410,19 @@ def padd(self): outp += text return outp -def iter_parse(term, ucs): - for match in re.finditer(term._caps_compiled_any, ucs): + +def iter_parse(term, text): + """ + Generator yields (text, capability) for characters of ``text``. + + value for ``capability`` may be ``None``, where ``text`` is + :class:`str` of length 1. Otherwise, ``text`` is a full + matching sequence of given capability. + """ + for match in re.finditer(term._caps_compiled_any, text): name = match.lastgroup value = match.group(name) if name == 'MISMATCH': yield (value, None) - continue - yield value, term.caps[name] + else: + yield value, term.caps[name] diff --git a/blessed/terminal.py b/blessed/terminal.py index 7e9e3606..28e1ee60 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -54,8 +54,6 @@ resolve_attribute, ) -#_build_any_numeric_capability, XXX wtf? - from ._capabilities import ( CAPABILITIES_RAW_MIXIN, CAPABILITIES_ADDITIVES, @@ -63,7 +61,6 @@ ) from .sequences import (SequenceTextWrapper, - iter_parse, Sequence, Termcap, ) @@ -290,8 +287,6 @@ def __init__keycodes(self): self._keyboard_decoder = codecs.getincrementaldecoder( self._encoding)() - - def __getattr__(self, attr): r""" Return a terminal capability as Unicode string. @@ -837,9 +832,6 @@ def split_seqs(self, text, maxsplit=0, flags=0): return list(filter(None, re.split(self.caps_compiled, text, maxsplit=maxsplit, flags=flags))) - def iter_parse(self, text): - return iter_parse(text) - def wrap(self, text, width=None, **kwargs): """ Text-wrap a string, returning a list of wrapped lines. @@ -1147,6 +1139,7 @@ def inkey(self, timeout=None, esc_delay=0.35, **_kwargs): self.ungetch(ucs[len(ks):]) return ks + class WINSZ(collections.namedtuple('WINSZ', ( 'ws_row', 'ws_col', 'ws_xpixel', 'ws_ypixel'))): """ From 5ffcd519f45a8ff031761ad2cb53716e1b2aba1b Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 1 Nov 2015 00:13:38 -0700 Subject: [PATCH 333/459] test python2.6 where available --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 439419d7..0bde54ba 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = about, sa, sphinx, py{27,34,35} +envlist = about, sa, sphinx, py{26,27,34,35} skip_missing_interpreters = true [testenv] From ffd03f7d549c727ffb18cf79c6157405c94dafee Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 1 Nov 2015 00:20:56 -0700 Subject: [PATCH 334/459] comment fix, done for now. --- blessed/terminal.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/blessed/terminal.py b/blessed/terminal.py index 28e1ee60..9d5e4f6c 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -249,12 +249,12 @@ def __init__capabilities(self): if pattern: self.caps[name] = Termcap(name, pattern, attribute) - # make a compiled named regular expression table, the matching - # '.lastgroup' is the primary lookup key for 'self.caps'. + # make a compiled named regular expression table self.caps_compiled = re.compile( '|'.join(cap.pattern for name, cap in self.caps.items())) - # for tokenizer + # for tokenizer, the '.lastgroup' is the primary lookup key for + # 'self.caps', unless 'MISMATCH'; then it is an unmatched character. self._caps_compiled_any = re.compile('|'.join( cap.named_pattern for name, cap in self.caps.items() ) + '|(?P.)') From f4e71f1bb173a7497ee058a4f7bcdfee701c6004 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 1 Nov 2015 00:31:38 -0700 Subject: [PATCH 335/459] do python2.6 tests quicker! --- tox.ini | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tox.ini b/tox.ini index 0bde54ba..c8adecbd 100644 --- a/tox.ini +++ b/tox.ini @@ -63,6 +63,11 @@ commands = {envbindir}/sphinx-build -v -W \ # just do a 'quick' on py34, if exists. setenv = TEST_QUICK=1 +[testenv:py26] +# and python2.6 really only tests 'orderedict' and some various +# backports of import fallback of features +setenv = TEST_QUICK=1 + [pytest] looponfailroots = blessed norecursedirs = .git From 6765f4b81806ce9e8b36fe67549fd98fc6fe5601 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 1 Nov 2015 00:35:03 -0700 Subject: [PATCH 336/459] make landscape.io happy about some pylint nits --- .landscape.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.landscape.yml b/.landscape.yml index 1a1a1b75..5db2905c 100644 --- a/.landscape.yml +++ b/.landscape.yml @@ -70,8 +70,9 @@ pylint: good-names: _,ks,fd disable: - # Access to a protected member _sugar of a client class - protected-access + - too-few-public-methods + - start-args pyroma: # checks setup.py From fcec04e0bd7ce94ace9b3bdc8f6a7a8ee8772c8c Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Tue, 3 Nov 2015 14:12:02 -0500 Subject: [PATCH 337/459] fix double word --- blessed/terminal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blessed/terminal.py b/blessed/terminal.py index 51ae9cc1..9ebf12d6 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -808,8 +808,8 @@ def getch(self): :returns: a single unicode character, or ``u''`` if a multi-byte sequence has not yet been fully received. - This method name and behavior mimics curses ``getch(void)``, and is - supports supports :meth:`inkey`, reading only one byte from + This method name and behavior mimics curses ``getch(void)``, and + it supports :meth:`inkey`, reading only one byte from the keyboard string at a time. This method should always return without blocking if called after :meth:`kbhit` has returned True. From e8b8b1a88e9ea4dd7d4ecc2959587032587846b2 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 7 Nov 2015 21:11:42 -0800 Subject: [PATCH 338/459] re-introduce measure_length (deprecated) --- blessed/sequences.py | 17 ++- blessed/tests/test_length_sequence.py | 157 +++++++++++++------------- 2 files changed, 94 insertions(+), 80 deletions(-) diff --git a/blessed/sequences.py b/blessed/sequences.py index bad80f34..4f94e7d2 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -13,7 +13,7 @@ import wcwidth import six -__all__ = ('Sequence', 'SequenceTextWrapper') +__all__ = ('Sequence', 'SequenceTextWrapper', 'measure_length') class Termcap(object): @@ -426,3 +426,18 @@ def iter_parse(term, text): yield (value, None) else: yield value, term.caps[name] + + +def measure_length(text, term): + """ + .. deprecated:: 1.12.0 + + :rtype: int + """ + try: + text, capability = next(iter_parse(term, text)) + if capability: + return len(text) + except StopIteration: + return 0 + return 0 diff --git a/blessed/tests/test_length_sequence.py b/blessed/tests/test_length_sequence.py index c1f06cbe..b62b9ea0 100644 --- a/blessed/tests/test_length_sequence.py +++ b/blessed/tests/test_length_sequence.py @@ -289,82 +289,81 @@ def child(kind, lines=25, cols=80): child(kind=all_terms) # TODO: next(term.iter_parse('sequence')).horizontal_distance('sequence') -# -#def test_sequence_is_movement_false(all_terms): -# """Test parser about sequences that do not move the cursor.""" -# @as_subprocess -# def child_mnemonics_wontmove(kind): -# from blessed.sequences import measure_length -# t = TestTerminal(kind=kind) -# assert (0 == measure_length(u'', t)) -# # not even a mbs -# assert (0 == measure_length(u'xyzzy', t)) -# # negative numbers, though printable as %d, do not result -# # in movement; just garbage. Also not a valid sequence. -# assert (0 == measure_length(t.cuf(-333), t)) -# assert (len(t.clear_eol) == measure_length(t.clear_eol, t)) -# # various erases don't *move* -# assert (len(t.clear_bol) == measure_length(t.clear_bol, t)) -# assert (len(t.clear_eos) == measure_length(t.clear_eos, t)) -# assert (len(t.bold) == measure_length(t.bold, t)) -# # various paints don't move -# assert (len(t.red) == measure_length(t.red, t)) -# assert (len(t.civis) == measure_length(t.civis, t)) -# if t.cvvis: -# assert (len(t.cvvis) == measure_length(t.cvvis, t)) -# assert (len(t.underline) == measure_length(t.underline, t)) -# assert (len(t.reverse) == measure_length(t.reverse, t)) -# for _num in range(t.number_of_colors): -# assert (len(t.color(_num)) == measure_length(t.color(_num), t)) -# assert (len(t.normal) == measure_length(t.normal, t)) -# assert (len(t.normal_cursor) == measure_length(t.normal_cursor, t)) -# assert (len(t.hide_cursor) == measure_length(t.hide_cursor, t)) -# assert (len(t.save) == measure_length(t.save, t)) -# assert (len(t.italic) == measure_length(t.italic, t)) -# assert (len(t.standout) == measure_length(t.standout, t) -# ), (t.standout, t._wont_move) -# -# child_mnemonics_wontmove(all_terms) -# -# -#def test_sequence_is_movement_true(all_terms): -# """Test parsers about sequences that move the cursor.""" -# @as_subprocess -# def child_mnemonics_willmove(kind): -# from blessed.sequences import measure_length -# t = TestTerminal(kind=kind) -# # movements -# assert (len(t.move(98, 76)) == -# measure_length(t.move(98, 76), t)) -# assert (len(t.move(54)) == -# measure_length(t.move(54), t)) -# assert not t.cud1 or (len(t.cud1) == -# measure_length(t.cud1, t)) -# assert not t.cub1 or (len(t.cub1) == -# measure_length(t.cub1, t)) -# assert not t.cuf1 or (len(t.cuf1) == -# measure_length(t.cuf1, t)) -# assert not t.cuu1 or (len(t.cuu1) == -# measure_length(t.cuu1, t)) -# assert not t.cub or (len(t.cub(333)) == -# measure_length(t.cub(333), t)) -# assert not t.cuf or (len(t.cuf(333)) == -# measure_length(t.cuf(333), t)) -# assert not t.home or (len(t.home) == -# measure_length(t.home, t)) -# assert not t.restore or (len(t.restore) == -# measure_length(t.restore, t)) -# assert not t.clear or (len(t.clear) == -# measure_length(t.clear, t)) -# -# child_mnemonics_willmove(all_terms) -# -# -#def test_foreign_sequences(): -# """Test parsers about sequences received from foreign sources.""" -# @as_subprocess -# def child(kind): -# from blessed.sequences import measure_length -# t = TestTerminal(kind=kind) -# assert measure_length(u'\x1b[m', t) == len('\x1b[m') -# child(kind='ansi') + +def test_sequence_is_movement_false(all_terms): + """Test parser about sequences that do not move the cursor.""" + @as_subprocess + def child_mnemonics_wontmove(kind): + from blessed.sequences import measure_length + t = TestTerminal(kind=kind) + assert (0 == measure_length(u'', t)) + # not even a mbs + assert (0 == measure_length(u'xyzzy', t)) + # negative numbers, though printable as %d, do not result + # in movement; just garbage. Also not a valid sequence. + assert (0 == measure_length(t.cuf(-333), t)) + assert (len(t.clear_eol) == measure_length(t.clear_eol, t)) + # various erases don't *move* + assert (len(t.clear_bol) == measure_length(t.clear_bol, t)) + assert (len(t.clear_eos) == measure_length(t.clear_eos, t)) + assert (len(t.bold) == measure_length(t.bold, t)) + # various paints don't move + assert (len(t.red) == measure_length(t.red, t)) + assert (len(t.civis) == measure_length(t.civis, t)) + if t.cvvis: + assert (len(t.cvvis) == measure_length(t.cvvis, t)) + assert (len(t.underline) == measure_length(t.underline, t)) + assert (len(t.reverse) == measure_length(t.reverse, t)) + for _num in range(t.number_of_colors): + assert (len(t.color(_num)) == measure_length(t.color(_num), t)) + assert (len(t.normal_cursor) == measure_length(t.normal_cursor, t)) + assert (len(t.hide_cursor) == measure_length(t.hide_cursor, t)) + assert (len(t.save) == measure_length(t.save, t)) + assert (len(t.italic) == measure_length(t.italic, t)) + assert (len(t.standout) == measure_length(t.standout, t) + ), (t.standout, t._wont_move) + + child_mnemonics_wontmove(all_terms) + + +def test_sequence_is_movement_true(all_terms): + """Test parsers about sequences that move the cursor.""" + @as_subprocess + def child_mnemonics_willmove(kind): + from blessed.sequences import measure_length + t = TestTerminal(kind=kind) + # movements + assert (len(t.move(98, 76)) == + measure_length(t.move(98, 76), t)) + assert (len(t.move(54)) == + measure_length(t.move(54), t)) + assert not t.cud1 or (len(t.cud1) == + measure_length(t.cud1, t)) + assert not t.cub1 or (len(t.cub1) == + measure_length(t.cub1, t)) + assert not t.cuf1 or (len(t.cuf1) == + measure_length(t.cuf1, t)) + assert not t.cuu1 or (len(t.cuu1) == + measure_length(t.cuu1, t)) + assert not t.cub or (len(t.cub(333)) == + measure_length(t.cub(333), t)) + assert not t.cuf or (len(t.cuf(333)) == + measure_length(t.cuf(333), t)) + assert not t.home or (len(t.home) == + measure_length(t.home, t)) + assert not t.restore or (len(t.restore) == + measure_length(t.restore, t)) + assert not t.clear or (len(t.clear) == + measure_length(t.clear, t)) + + child_mnemonics_willmove(all_terms) + + +def test_foreign_sequences(): + """Test parsers about sequences received from foreign sources.""" + @as_subprocess + def child(kind): + from blessed.sequences import measure_length + t = TestTerminal(kind=kind) + assert measure_length(u'\x1b[m', t) == len('\x1b[m') + child(kind='ansi') From 47dbb50b7167c78804e61c355712650712e79035 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 8 Nov 2015 12:05:24 -0800 Subject: [PATCH 339/459] prepare for next release, including split_seqs --- blessed/_capabilities.py | 32 +- blessed/sequences.py | 63 ++-- blessed/terminal.py | 40 ++- blessed/tests/test_core.py | 19 +- blessed/tests/test_length_sequence.py | 452 +++++++++++++++----------- blessed/tests/test_sequences.py | 23 +- docs/history.rst | 10 +- docs/overview.rst | 29 +- tox.ini | 2 +- version.json | 2 +- 10 files changed, 396 insertions(+), 276 deletions(-) diff --git a/blessed/_capabilities.py b/blessed/_capabilities.py index c2f7707a..a4c6d05e 100644 --- a/blessed/_capabilities.py +++ b/blessed/_capabilities.py @@ -94,24 +94,14 @@ # than set_a_attributes1 or set_a_foreground. ('color', ('_foreground_color', {'nparams': 1, 'match_any': True, 'numeric': 1})), - - # very likely, this will be the most commonly matched inward attribute. - ('set_a_attributes1', ('sgr1', {'nparams': 1, 'match_any': True, - 'match_optional': True})), - ('set_a_attributes2', ('sgr1', {'nparams': 2, 'match_any': True})), - ('set_a_attributes3', ('sgr1', {'nparams': 3, 'match_any': True})), - ('set_a_attributes4', ('sgr1', {'nparams': 4, 'match_any': True})), - ('set_a_attributes5', ('sgr1', {'nparams': 5, 'match_any': True})), - ('set_a_attributes6', ('sgr1', {'nparams': 6, 'match_any': True})), - ('set_a_attributes7', ('sgr1', {'nparams': 7, 'match_any': True})), - ('set_a_attributes8', ('sgr1', {'nparams': 8, 'match_any': True})), - ('set_a_attributes9', ('sgr1', {'nparams': 9, 'match_any': True})), ('set_a_foreground', ('color', {'nparams': 1, 'match_any': True, 'numeric': 1})), ('set_a_background', ('on_color', {'nparams': 1, 'match_any': True, 'numeric': 1})), ('set_tab', ('hts', {})), ('tab', ('ht', {})), + ('italic', ('sitm', {})), + ('no_italic', ('sitm', {})), )) CAPABILITIES_RAW_MIXIN = { @@ -123,22 +113,30 @@ 'exit_attribute_mode': re.escape('\x1b') + r'\[m', 'parm_left_cursor': re.escape('\x1b') + r'\[(\d+)D', 'parm_right_cursor': re.escape('\x1b') + r'\[(\d+)C', + 'restore_cursor': re.escape(r'\x1b\[u'), + 'save_cursor': re.escape(r'\x1b\[s'), 'scroll_forward': re.escape('\n'), 'set0_des_seq': re.escape('\x1b(B'), - 'set_a_attributes1': re.escape('\x1b') + r'\[(\d+)?m', - 'set_a_attributes2': re.escape('\x1b') + r'\[(\d+)\;(\d+)m', - 'set_a_attributes3': re.escape('\x1b') + r'\[(\d+)\;(\d+)\;(\d+)m', - 'set_a_attributes4': re.escape('\x1b') + r'\[(\d+)\;(\d+)\;(\d+)\;(\d+)m', 'tab': re.escape('\t'), # one could get carried away, such as by adding '\x1b#8' (dec tube # alignment test) by reversing basic vt52, ansi, and xterm sequence # parsers. There is plans to do just that for ANSI.SYS support. } + CAPABILITIES_ADDITIVES = { 'color256': ('color', re.escape('\x1b') + r'\[38;5;(\d+)m'), 'shift_in': ('', re.escape('\x0f')), 'shift_out': ('', re.escape('\x0e')), + # sgr(...) outputs strangely, use the basic ANSI/EMCA-48 codes here. + 'set_a_attributes1': ( + 'sgr', re.escape('\x1b') + r'\[\d+m'), + 'set_a_attributes2': ( + 'sgr', re.escape('\x1b') + r'\[\d+\;\d+m'), + 'set_a_attributes3': ( + 'sgr', re.escape('\x1b') + r'\[\d+\;\d+\;\d+m'), + 'set_a_attributes4': ( + 'sgr', re.escape('\x1b') + r'\[\d+\;\d+\;\d+\;\d+m'), # this helps where xterm's sgr0 includes set0_des_seq, we'd # rather like to also match this immediate substring. 'sgr0': ('sgr0', re.escape('\x1b') + r'\[m'), @@ -148,7 +146,7 @@ CAPABILITIES_CAUSE_MOVEMENT = ( 'ascii_tab', - 'backspace' + 'backspace', 'carriage_return', 'clear_screen', 'column_address', diff --git a/blessed/sequences.py b/blessed/sequences.py index 4f94e7d2..bed5ef43 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -22,9 +22,9 @@ def __init__(self, name, pattern, attribute): """ Class initializer. - :param str name: name describing capability. - :param str pattern: regular expression string. - :param str attribute: :class:`~.Terminal` attribute used to build + :arg str name: name describing capability. + :arg str pattern: regular expression string. + :arg str attribute: :class:`~.Terminal` attribute used to build this terminal capability. """ self.name = name @@ -57,7 +57,7 @@ def horizontal_distance(self, text): Horizontal carriage adjusted by capability, may be negative. :rtype: int - :param str text: for capabilities *parm_left_cursor*, + :arg str text: for capabilities *parm_left_cursor*, *parm_right_cursor*, provide the matching sequence text, its interpreted distance is returned. @@ -91,22 +91,22 @@ def build(cls, name, capability, attribute, nparams=0, r""" Class factory builder for given capability definition. - :param str name: Variable name given for this pattern. - :param str capability: A unicode string representing a terminal + :arg str name: Variable name given for this pattern. + :arg str capability: A unicode string representing a terminal capability to build for. When ``nparams`` is non-zero, it must be a callable unicode string (such as the result from ``getattr(term, 'bold')``. - :param attribute: The terminfo(5) capability name by which this + :arg attribute: The terminfo(5) capability name by which this pattern is known. - :param int nparams: number of positional arguments for callable. - :param bool match_grouped: If the numeric pattern should be + :arg int nparams: number of positional arguments for callable. + :arg bool match_grouped: If the numeric pattern should be grouped, ``(\d+)`` when ``True``, ``\d+`` default. - :param bool match_any: When keyword argument ``nparams`` is given, + :arg bool match_any: When keyword argument ``nparams`` is given, *any* numeric found in output is suitable for building as pattern ``(\d+)``. Otherwise, only the first matching value of range *(numeric - 1)* through *(numeric + 1)* will be replaced by pattern ``(\d+)`` in builder. - :param bool match_optional: When ``True``, building of numeric patterns + :arg bool match_optional: When ``True``, building of numeric patterns containing ``(\d+)`` will be built as optional, ``(\d+)?``. """ _numeric_regex = r'\d+' @@ -254,8 +254,8 @@ def __new__(cls, sequence_text, term): """ Class constructor. - :param sequence_text: A string that may contain sequences. - :param blessed.Terminal term: :class:`~.Terminal` instance. + :arg sequence_text: A string that may contain sequences. + :arg blessed.Terminal term: :class:`~.Terminal` instance. """ new = six.text_type.__new__(cls, sequence_text) new._term = term @@ -265,9 +265,9 @@ def ljust(self, width, fillchar=u' '): """ Return string containing sequences, left-adjusted. - :param int width: Total width given to right-adjust ``text``. If + :arg int width: Total width given to right-adjust ``text``. If unspecified, the width of the attached terminal is used (default). - :param str fillchar: String for padding right-of ``text``. + :arg str fillchar: String for padding right-of ``text``. :returns: String of ``text``, right-aligned by ``width``. :rtype: str """ @@ -279,9 +279,9 @@ def rjust(self, width, fillchar=u' '): """ Return string containing sequences, right-adjusted. - :param int width: Total width given to right-adjust ``text``. If + :arg int width: Total width given to right-adjust ``text``. If unspecified, the width of the attached terminal is used (default). - :param str fillchar: String for padding left-of ``text``. + :arg str fillchar: String for padding left-of ``text``. :returns: String of ``text``, right-aligned by ``width``. :rtype: str """ @@ -293,9 +293,9 @@ def center(self, width, fillchar=u' '): """ Return string containing sequences, centered. - :param int width: Total width given to center ``text``. If + :arg int width: Total width given to center ``text``. If unspecified, the width of the attached terminal is used (default). - :param str fillchar: String for padding left and right-of ``text``. + :arg str fillchar: String for padding left and right-of ``text``. :returns: String of ``text``, centered by ``width``. :rtype: str """ @@ -344,7 +344,7 @@ def strip(self, chars=None): """ Return string of sequences, leading, and trailing whitespace removed. - :param str chars: Remove characters in chars instead of whitespace. + :arg str chars: Remove characters in chars instead of whitespace. :rtype: str """ return self.strip_seqs().strip(chars) @@ -353,7 +353,7 @@ def lstrip(self, chars=None): """ Return string of all sequences and leading whitespace removed. - :param str chars: Remove characters in chars instead of whitespace. + :arg str chars: Remove characters in chars instead of whitespace. :rtype: str """ return self.strip_seqs().lstrip(chars) @@ -362,29 +362,16 @@ def rstrip(self, chars=None): """ Return string of all sequences and trailing whitespace removed. - :param str chars: Remove characters in chars instead of whitespace. + :arg str chars: Remove characters in chars instead of whitespace. :rtype: str """ return self.strip_seqs().rstrip(chars) def strip_seqs(self): - r""" - Return string of all sequences removed. - - >>> from blessed import Terminal - >>> from blessed.sequences import Sequence - >>> term = Terminal() - >>> Sequence(term.cuf(5) + term.red(u'test'), term).strip_seqs() - u' test' + """ + Return ``text`` stripped of only its terminal sequences. :rtype: str - - This method is used to determine the printable width of a string, - and is the first pass of :meth:`length`. - - .. note:: Non-destructive sequences that adjust horizontal distance - (such as ``\b`` or ``term.cuf(5)``) are replaced by destructive - space or erasing. """ gen = iter_parse(self._term, self.padd()) return u''.join(text for text, cap in gen if not cap) @@ -430,7 +417,7 @@ def iter_parse(term, text): def measure_length(text, term): """ - .. deprecated:: 1.12.0 + .. deprecated:: 1.12.0. :rtype: int """ diff --git a/blessed/terminal.py b/blessed/terminal.py index 9d5e4f6c..6e6bfc8b 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -258,6 +258,9 @@ def __init__capabilities(self): self._caps_compiled_any = re.compile('|'.join( cap.named_pattern for name, cap in self.caps.items() ) + '|(?P.)') + self._caps_unnamed_any = re.compile('|'.join( + '({0})'.format(cap.pattern) for name, cap in self.caps.items() + ) + '|(.)') def __init__keycodes(self): # Initialize keyboard data determined by capability. @@ -778,9 +781,8 @@ def strip(self, text, chars=None): :rtype: str - >>> term = blessed.Terminal() - >>> term.strip(u' \x1b[0;3m XXX ') - u'XXX' + >>> term.strip(u' \x1b[0;3m xyz ') + u'xyz' """ return Sequence(text, self).strip(chars) @@ -790,9 +792,8 @@ def rstrip(self, text, chars=None): :rtype: str - >>> term = blessed.Terminal() - >>> term.rstrip(u' \x1b[0;3m XXX ') - u' XXX' + >>> term.rstrip(u' \x1b[0;3m xyz ') + u' xyz' """ return Sequence(text, self).rstrip(chars) @@ -802,9 +803,8 @@ def lstrip(self, text, chars=None): :rtype: str - >>> term = blessed.Terminal() - >>> term.lstrip(u' \x1b[0;3m XXX ') - u'XXX ' + >>> term.lstrip(u' \x1b[0;3m xyz ') + u'xyz ' """ return Sequence(text, self).lstrip(chars) @@ -814,22 +814,30 @@ def strip_seqs(self, text): :rtype: str - >>> term = blessed.Terminal() - >>> term.strip_seqs(u'\x1b[0;3mXXX') - u'XXX' + >>> term.strip_seqs(u'\x1b[0;3mxyz') + u'xyz' + >>> term.strip_seqs(term.cuf(5) + term.red(u'test')) + u' test' + + .. note:: Non-destructive sequences that adjust horizontal distance + (such as ``\b`` or ``term.cuf(5)``) are replaced by destructive + space or erasing. """ return Sequence(text, self).strip_seqs() def split_seqs(self, text, maxsplit=0, flags=0): r""" - Return ``text`` split by terminal sequences. + Return ``text`` split by individual character elements and sequences. - :param int maxpslit: If maxsplit is nonzero, at most ``maxsplit`` + :arg int maxpslit: If maxsplit is nonzero, at most ``maxsplit`` splits occur, and the remainder of the string is returned as the final element of the list. :rtype: list[str] + + >>> term.split_seqs(term.underline(u'xyz')) + ['\x1b[4m', 'x', 'y', 'z', '\x1b(B', '\x1b[m'] """ - return list(filter(None, re.split(self.caps_compiled, text, + return list(filter(None, re.split(self._caps_unnamed_any, text, maxsplit=maxsplit, flags=flags))) def wrap(self, text, width=None, **kwargs): @@ -881,7 +889,7 @@ def ungetch(self, text): """ Buffer input data to be discovered by next call to :meth:`~.inkey`. - :param str ucs: String to be buffered as keyboard input. + :arg str ucs: String to be buffered as keyboard input. """ self._keyboard_buf.extendleft(text) diff --git a/blessed/tests/test_core.py b/blessed/tests/test_core.py index e03d06c9..e4fe886a 100644 --- a/blessed/tests/test_core.py +++ b/blessed/tests/test_core.py @@ -382,7 +382,7 @@ def child(): def test_unknown_preferredencoding_warned_and_fallback_ascii(): - "Ensure a locale without a codecs incrementaldecoder emits a warning." + """Ensure a locale without a codec emits a warning.""" @as_subprocess def child(): with mock.patch('locale.getpreferredencoding') as get_enc: @@ -472,3 +472,20 @@ def test_time_left_infinite_None(): """keyboard '_time_left' routine returns None when given None.""" from blessed.keyboard import _time_left assert _time_left(stime=time.time(), timeout=None) is None + + +def test_termcap_repr(): + "Ensure ``hidden_cursor()`` writes hide_cursor and normal_cursor." + + given_ttype='vt220' + given_capname = 'cursor_up' + expected = r"" + + @as_subprocess + def child(): + import blessed + term = blessed.Terminal(given_ttype) + given = repr(term.caps[given_capname]) + assert given == expected + + child() diff --git a/blessed/tests/test_length_sequence.py b/blessed/tests/test_length_sequence.py index b62b9ea0..8efa697a 100644 --- a/blessed/tests/test_length_sequence.py +++ b/blessed/tests/test_length_sequence.py @@ -57,141 +57,141 @@ def child(): def test_sequence_length(all_terms): - """Ensure T.length(string containing sequence) is correct.""" + """Ensure T.length(string containing sequence) is correcterm.""" @as_subprocess def child(kind): - t = TestTerminal(kind=kind) + term = TestTerminal(kind=kind) # Create a list of ascii characters, to be separated # by word, to be zipped up with a cycling list of # terminal sequences. Then, compare the length of - # each, the basic plain_text.__len__ vs. the Terminal + # each, the basic plain_texterm.__len__ vs. the Terminal # method length. They should be equal. plain_text = (u'The softest things of the world ' u'Override the hardest things of the world ' u'That which has no substance ' u'Enters into that which has no openings') - if t.bold: - assert (t.length(t.bold) == 0) - assert (t.length(t.bold(u'x')) == 1) - assert (t.length(t.bold_red) == 0) - assert (t.length(t.bold_red(u'x')) == 1) - assert (t.strip(t.bold) == u'') - assert (t.rstrip(t.bold) == u'') - assert (t.lstrip(t.bold) == u'') - assert (t.strip(t.bold(u' x ')) == u'x') - assert (t.strip(t.bold(u'z x q'), 'zq') == u' x ') - assert (t.rstrip(t.bold(u' x ')) == u' x') - assert (t.lstrip(t.bold(u' x ')) == u'x ') - assert (t.strip(t.bold_red) == u'') - assert (t.rstrip(t.bold_red) == u'') - assert (t.lstrip(t.bold_red) == u'') - assert (t.strip(t.bold_red(u' x ')) == u'x') - assert (t.rstrip(t.bold_red(u' x ')) == u' x') - assert (t.lstrip(t.bold_red(u' x ')) == u'x ') - assert (t.strip_seqs(t.bold) == u'') - assert (t.strip_seqs(t.bold(u' x ')) == u' x ') - assert (t.strip_seqs(t.bold_red) == u'') - assert (t.strip_seqs(t.bold_red(u' x ')) == u' x ') - - if t.underline: - assert (t.length(t.underline) == 0) - assert (t.length(t.underline(u'x')) == 1) - assert (t.length(t.underline_red) == 0) - assert (t.length(t.underline_red(u'x')) == 1) - assert (t.strip(t.underline) == u'') - assert (t.strip(t.underline(u' x ')) == u'x') - assert (t.strip(t.underline_red) == u'') - assert (t.strip(t.underline_red(u' x ')) == u'x') - assert (t.rstrip(t.underline_red(u' x ')) == u' x') - assert (t.lstrip(t.underline_red(u' x ')) == u'x ') - assert (t.strip_seqs(t.underline) == u'') - assert (t.strip_seqs(t.underline(u' x ')) == u' x ') - assert (t.strip_seqs(t.underline_red) == u'') - assert (t.strip_seqs(t.underline_red(u' x ')) == u' x ') - - if t.reverse: - assert (t.length(t.reverse) == 0) - assert (t.length(t.reverse(u'x')) == 1) - assert (t.length(t.reverse_red) == 0) - assert (t.length(t.reverse_red(u'x')) == 1) - assert (t.strip(t.reverse) == u'') - assert (t.strip(t.reverse(u' x ')) == u'x') - assert (t.strip(t.reverse_red) == u'') - assert (t.strip(t.reverse_red(u' x ')) == u'x') - assert (t.rstrip(t.reverse_red(u' x ')) == u' x') - assert (t.lstrip(t.reverse_red(u' x ')) == u'x ') - assert (t.strip_seqs(t.reverse) == u'') - assert (t.strip_seqs(t.reverse(u' x ')) == u' x ') - assert (t.strip_seqs(t.reverse_red) == u'') - assert (t.strip_seqs(t.reverse_red(u' x ')) == u' x ') - - if t.blink: - assert (t.length(t.blink) == 0) - assert (t.length(t.blink(u'x')) == 1) - assert (t.length(t.blink_red) == 0) - assert (t.length(t.blink_red(u'x')) == 1) - assert (t.strip(t.blink) == u'') - assert (t.strip(t.blink(u' x ')) == u'x') - assert (t.strip(t.blink(u'z x q'), u'zq') == u' x ') - assert (t.strip(t.blink_red) == u'') - assert (t.strip(t.blink_red(u' x ')) == u'x') - assert (t.strip_seqs(t.blink) == u'') - assert (t.strip_seqs(t.blink(u' x ')) == u' x ') - assert (t.strip_seqs(t.blink_red) == u'') - assert (t.strip_seqs(t.blink_red(u' x ')) == u' x ') - - if t.home: - assert (t.length(t.home) == 0) - assert (t.strip(t.home) == u'') - if t.clear_eol: - assert (t.length(t.clear_eol) == 0) - assert (t.strip(t.clear_eol) == u'') - if t.enter_fullscreen: - assert (t.length(t.enter_fullscreen) == 0) - assert (t.strip(t.enter_fullscreen) == u'') - if t.exit_fullscreen: - assert (t.length(t.exit_fullscreen) == 0) - assert (t.strip(t.exit_fullscreen) == u'') + if term.bold: + assert (term.length(term.bold) == 0) + assert (term.length(term.bold(u'x')) == 1) + assert (term.length(term.bold_red) == 0) + assert (term.length(term.bold_red(u'x')) == 1) + assert (term.strip(term.bold) == u'') + assert (term.rstrip(term.bold) == u'') + assert (term.lstrip(term.bold) == u'') + assert (term.strip(term.bold(u' x ')) == u'x') + assert (term.strip(term.bold(u'z x q'), 'zq') == u' x ') + assert (term.rstrip(term.bold(u' x ')) == u' x') + assert (term.lstrip(term.bold(u' x ')) == u'x ') + assert (term.strip(term.bold_red) == u'') + assert (term.rstrip(term.bold_red) == u'') + assert (term.lstrip(term.bold_red) == u'') + assert (term.strip(term.bold_red(u' x ')) == u'x') + assert (term.rstrip(term.bold_red(u' x ')) == u' x') + assert (term.lstrip(term.bold_red(u' x ')) == u'x ') + assert (term.strip_seqs(term.bold) == u'') + assert (term.strip_seqs(term.bold(u' x ')) == u' x ') + assert (term.strip_seqs(term.bold_red) == u'') + assert (term.strip_seqs(term.bold_red(u' x ')) == u' x ') + + if term.underline: + assert (term.length(term.underline) == 0) + assert (term.length(term.underline(u'x')) == 1) + assert (term.length(term.underline_red) == 0) + assert (term.length(term.underline_red(u'x')) == 1) + assert (term.strip(term.underline) == u'') + assert (term.strip(term.underline(u' x ')) == u'x') + assert (term.strip(term.underline_red) == u'') + assert (term.strip(term.underline_red(u' x ')) == u'x') + assert (term.rstrip(term.underline_red(u' x ')) == u' x') + assert (term.lstrip(term.underline_red(u' x ')) == u'x ') + assert (term.strip_seqs(term.underline) == u'') + assert (term.strip_seqs(term.underline(u' x ')) == u' x ') + assert (term.strip_seqs(term.underline_red) == u'') + assert (term.strip_seqs(term.underline_red(u' x ')) == u' x ') + + if term.reverse: + assert (term.length(term.reverse) == 0) + assert (term.length(term.reverse(u'x')) == 1) + assert (term.length(term.reverse_red) == 0) + assert (term.length(term.reverse_red(u'x')) == 1) + assert (term.strip(term.reverse) == u'') + assert (term.strip(term.reverse(u' x ')) == u'x') + assert (term.strip(term.reverse_red) == u'') + assert (term.strip(term.reverse_red(u' x ')) == u'x') + assert (term.rstrip(term.reverse_red(u' x ')) == u' x') + assert (term.lstrip(term.reverse_red(u' x ')) == u'x ') + assert (term.strip_seqs(term.reverse) == u'') + assert (term.strip_seqs(term.reverse(u' x ')) == u' x ') + assert (term.strip_seqs(term.reverse_red) == u'') + assert (term.strip_seqs(term.reverse_red(u' x ')) == u' x ') + + if term.blink: + assert (term.length(term.blink) == 0) + assert (term.length(term.blink(u'x')) == 1) + assert (term.length(term.blink_red) == 0) + assert (term.length(term.blink_red(u'x')) == 1) + assert (term.strip(term.blink) == u'') + assert (term.strip(term.blink(u' x ')) == u'x') + assert (term.strip(term.blink(u'z x q'), u'zq') == u' x ') + assert (term.strip(term.blink_red) == u'') + assert (term.strip(term.blink_red(u' x ')) == u'x') + assert (term.strip_seqs(term.blink) == u'') + assert (term.strip_seqs(term.blink(u' x ')) == u' x ') + assert (term.strip_seqs(term.blink_red) == u'') + assert (term.strip_seqs(term.blink_red(u' x ')) == u' x ') + + if term.home: + assert (term.length(term.home) == 0) + assert (term.strip(term.home) == u'') + if term.clear_eol: + assert (term.length(term.clear_eol) == 0) + assert (term.strip(term.clear_eol) == u'') + if term.enter_fullscreen: + assert (term.length(term.enter_fullscreen) == 0) + assert (term.strip(term.enter_fullscreen) == u'') + if term.exit_fullscreen: + assert (term.length(term.exit_fullscreen) == 0) + assert (term.strip(term.exit_fullscreen) == u'') # horizontally, we decide move_down and move_up are 0, - assert (t.length(t.move_down) == 0) - assert (t.length(t.move_down(2)) == 0) - assert (t.length(t.move_up) == 0) - assert (t.length(t.move_up(2)) == 0) + assert (term.length(term.move_down) == 0) + assert (term.length(term.move_down(2)) == 0) + assert (term.length(term.move_up) == 0) + assert (term.length(term.move_up(2)) == 0) # other things aren't so simple, somewhat edge cases, # moving backwards and forwards horizontally must be # accounted for as a "length", as # will result in a printed column length of 12 (even # though columns 2-11 are non-destructive space - assert (t.length(u'x\b') == 0) - assert (t.strip(u'x\b') == u'') + assert (term.length(u'x\b') == 0) + assert (term.strip(u'x\b') == u'') # XXX why are some terminals width of 9 here ?? - assert (t.length(u'\t') in (8, 9)) - assert (t.strip(u'\t') == u'') - assert (t.length(u'_' + t.move_left) == 0) + assert (term.length(u'\t') in (8, 9)) + assert (term.strip(u'\t') == u'') + assert (term.length(u'_' + term.move_left) == 0) - if t.cub: - assert (t.length((u'_' * 10) + t.cub(10)) == 0) + if term.cub: + assert (term.length((u'_' * 10) + term.cub(10)) == 0) - assert (t.length(t.move_right) == 1) + assert (term.length(term.move_right) == 1) - if t.cuf: - assert (t.length(t.cuf(10)) == 10) + if term.cuf: + assert (term.length(term.cuf(10)) == 10) # vertical spacing is unaccounted as a 'length' - assert (t.length(t.move_up) == 0) - assert (t.length(t.cuu(10)) == 0) - assert (t.length(t.move_down) == 0) - assert (t.length(t.cud(10)) == 0) + assert (term.length(term.move_up) == 0) + assert (term.length(term.cuu(10)) == 0) + assert (term.length(term.move_down) == 0) + assert (term.length(term.cud(10)) == 0) # this is how manpages perform underlining, this is done # with the 'overstrike' capability of teletypes, and aparently # less(1), '123' -> '1\b_2\b_3\b_' text_wseqs = u''.join(itertools.chain( *zip(plain_text, itertools.cycle(['\b_'])))) - assert (t.length(text_wseqs) == len(plain_text)) + assert (term.length(text_wseqs) == len(plain_text)) child(all_terms) @@ -203,17 +203,17 @@ def child(): # set the pty's virtual window size os.environ['COLUMNS'] = '99' os.environ['LINES'] = '11' - t = TestTerminal(stream=six.StringIO()) - save_init = t._init_descriptor + term = TestTerminal(stream=six.StringIO()) + save_init = term._init_descriptor save_stdout = sys.__stdout__ try: - t._init_descriptor = None + term._init_descriptor = None sys.__stdout__ = None - winsize = t._height_and_width() - width = t.width - height = t.height + winsize = term._height_and_width() + width = term.width + height = term.height finally: - t._init_descriptor = save_init + term._init_descriptor = save_init sys.__stdout__ = save_stdout assert winsize.ws_col == width == 99 assert winsize.ws_row == height == 11 @@ -228,10 +228,10 @@ def child(lines=25, cols=80): # set the pty's virtual window size val = struct.pack('HHHH', lines, cols, 0, 0) fcntl.ioctl(sys.__stdout__.fileno(), termios.TIOCSWINSZ, val) - t = TestTerminal() - winsize = t._height_and_width() - assert t.width == cols - assert t.height == lines + term = TestTerminal() + winsize = term._height_and_width() + assert term.width == cols + assert term.height == lines assert winsize.ws_col == cols assert winsize.ws_row == lines @@ -241,23 +241,23 @@ def child(lines=25, cols=80): def test_Sequence_alignment_fixed_width(): @as_subprocess def child(kind): - t = TestTerminal(kind=kind) + term = TestTerminal(kind=kind) pony_msg = 'pony express, all aboard, choo, choo!' pony_len = len(pony_msg) pony_colored = u''.join( - ['%s%s' % (t.color(n % 7), ch,) + ['%s%s' % (term.color(n % 7), ch,) for n, ch in enumerate(pony_msg)]) - pony_colored += t.normal - ladjusted = t.ljust(pony_colored, 88) - radjusted = t.rjust(pony_colored, 88) - centered = t.center(pony_colored, 88) - assert (t.length(pony_colored) == pony_len) - assert (t.length(centered.strip()) == pony_len) - assert (t.length(centered) == len(pony_msg.center(88))) - assert (t.length(ladjusted.strip()) == pony_len) - assert (t.length(ladjusted) == len(pony_msg.ljust(88))) - assert (t.length(radjusted.strip()) == pony_len) - assert (t.length(radjusted) == len(pony_msg.rjust(88))) + pony_colored += term.normal + ladjusted = term.ljust(pony_colored, 88) + radjusted = term.rjust(pony_colored, 88) + centered = term.center(pony_colored, 88) + assert (term.length(pony_colored) == pony_len) + assert (term.length(centered.strip()) == pony_len) + assert (term.length(centered) == len(pony_msg.center(88))) + assert (term.length(ladjusted.strip()) == pony_len) + assert (term.length(ladjusted) == len(pony_msg.ljust(88))) + assert (term.length(radjusted.strip()) == pony_len) + assert (term.length(radjusted) == len(pony_msg.rjust(88))) def test_Sequence_alignment(all_terms): @@ -267,96 +267,156 @@ def child(kind, lines=25, cols=80): # set the pty's virtual window size val = struct.pack('HHHH', lines, cols, 0, 0) fcntl.ioctl(sys.__stdout__.fileno(), termios.TIOCSWINSZ, val) - t = TestTerminal(kind=kind) + term = TestTerminal(kind=kind) pony_msg = 'pony express, all aboard, choo, choo!' pony_len = len(pony_msg) pony_colored = u''.join( - ['%s%s' % (t.color(n % 7), ch,) + ['%s%s' % (term.color(n % 7), ch,) for n, ch in enumerate(pony_msg)]) - pony_colored += t.normal - ladjusted = t.ljust(pony_colored) - radjusted = t.rjust(pony_colored) - centered = t.center(pony_colored) - assert (t.length(pony_colored) == pony_len) - assert (t.length(centered.strip()) == pony_len) - assert (t.length(centered) == len(pony_msg.center(t.width))) - assert (t.length(ladjusted.strip()) == pony_len) - assert (t.length(ladjusted) == len(pony_msg.ljust(t.width))) - assert (t.length(radjusted.strip()) == pony_len) - assert (t.length(radjusted) == len(pony_msg.rjust(t.width))) + pony_colored += term.normal + ladjusted = term.ljust(pony_colored) + radjusted = term.rjust(pony_colored) + centered = term.center(pony_colored) + assert (term.length(pony_colored) == pony_len) + assert (term.length(centered.strip()) == pony_len) + assert (term.length(centered) == len(pony_msg.center(term.width))) + assert (term.length(ladjusted.strip()) == pony_len) + assert (term.length(ladjusted) == len(pony_msg.ljust(term.width))) + assert (term.length(radjusted.strip()) == pony_len) + assert (term.length(radjusted) == len(pony_msg.rjust(term.width))) child(kind=all_terms) -# TODO: next(term.iter_parse('sequence')).horizontal_distance('sequence') - def test_sequence_is_movement_false(all_terms): """Test parser about sequences that do not move the cursor.""" @as_subprocess - def child_mnemonics_wontmove(kind): + def child(kind): from blessed.sequences import measure_length - t = TestTerminal(kind=kind) - assert (0 == measure_length(u'', t)) + term = TestTerminal(kind=kind) + assert (0 == measure_length(u'', term)) # not even a mbs - assert (0 == measure_length(u'xyzzy', t)) + assert (0 == measure_length(u'xyzzy', term)) # negative numbers, though printable as %d, do not result # in movement; just garbage. Also not a valid sequence. - assert (0 == measure_length(t.cuf(-333), t)) - assert (len(t.clear_eol) == measure_length(t.clear_eol, t)) + assert (0 == measure_length(term.cuf(-333), term)) + assert (len(term.clear_eol) == measure_length(term.clear_eol, term)) # various erases don't *move* - assert (len(t.clear_bol) == measure_length(t.clear_bol, t)) - assert (len(t.clear_eos) == measure_length(t.clear_eos, t)) - assert (len(t.bold) == measure_length(t.bold, t)) + assert (len(term.clear_bol) == measure_length(term.clear_bol, term)) + assert (len(term.clear_eos) == measure_length(term.clear_eos, term)) + assert (len(term.bold) == measure_length(term.bold, term)) # various paints don't move - assert (len(t.red) == measure_length(t.red, t)) - assert (len(t.civis) == measure_length(t.civis, t)) - if t.cvvis: - assert (len(t.cvvis) == measure_length(t.cvvis, t)) - assert (len(t.underline) == measure_length(t.underline, t)) - assert (len(t.reverse) == measure_length(t.reverse, t)) - for _num in range(t.number_of_colors): - assert (len(t.color(_num)) == measure_length(t.color(_num), t)) - assert (len(t.normal_cursor) == measure_length(t.normal_cursor, t)) - assert (len(t.hide_cursor) == measure_length(t.hide_cursor, t)) - assert (len(t.save) == measure_length(t.save, t)) - assert (len(t.italic) == measure_length(t.italic, t)) - assert (len(t.standout) == measure_length(t.standout, t) - ), (t.standout, t._wont_move) - - child_mnemonics_wontmove(all_terms) + assert (len(term.red) == measure_length(term.red, term)) + assert (len(term.civis) == measure_length(term.civis, term)) + if term.cvvis: + assert (len(term.cvvis) == measure_length(term.cvvis, term)) + assert (len(term.underline) == measure_length(term.underline, term)) + assert (len(term.reverse) == measure_length(term.reverse, term)) + for _num in (0, term.number_of_colors): + expected = len(term.color(_num)) + given = measure_length(term.color(_num), term) + assert (expected == given) + assert (len(term.normal_cursor) == measure_length(term.normal_cursor, term)) + assert (len(term.hide_cursor) == measure_length(term.hide_cursor, term)) + assert (len(term.save) == measure_length(term.save, term)) + assert (len(term.italic) == measure_length(term.italic, term)) + assert (len(term.standout) == measure_length(term.standout, term) + ), (term.standout, term._wont_move) + + child(all_terms) + +def test_termcap_will_move_false(all_terms): + """Test parser about sequences that do not move the cursor.""" + @as_subprocess + def child(kind): + from blessed.sequences import iter_parse + term = TestTerminal(kind=kind) + if term.clear_eol: + assert not next(iter_parse(term, term.clear_eol))[1].will_move + if term.clear_bol: + assert not next(iter_parse(term, term.clear_bol))[1].will_move + if term.clear_eos: + assert not next(iter_parse(term, term.clear_eos))[1].will_move + if term.bold: + assert not next(iter_parse(term, term.bold))[1].will_move + if term.red: + assert not next(iter_parse(term, term.red))[1].will_move + if term.civis: + assert not next(iter_parse(term, term.civis))[1].will_move + if term.cvvis: + assert not next(iter_parse(term, term.cvvis))[1].will_move + if term.underline: + assert not next(iter_parse(term, term.underline))[1].will_move + if term.reverse: + assert not next(iter_parse(term, term.reverse))[1].will_move + if term.color(0): + assert not next(iter_parse(term, term.color(0)))[1].will_move + if term.normal_cursor: + assert not next(iter_parse(term, term.normal_cursor))[1].will_move + if term.save: + assert not next(iter_parse(term, term.save))[1].will_move + if term.italic: + assert not next(iter_parse(term, term.italic))[1].will_move + if term.standout: + assert not next(iter_parse(term, term.standout))[1].will_move + + child(all_terms) + def test_sequence_is_movement_true(all_terms): """Test parsers about sequences that move the cursor.""" @as_subprocess - def child_mnemonics_willmove(kind): + def child(kind): from blessed.sequences import measure_length - t = TestTerminal(kind=kind) + term = TestTerminal(kind=kind) # movements - assert (len(t.move(98, 76)) == - measure_length(t.move(98, 76), t)) - assert (len(t.move(54)) == - measure_length(t.move(54), t)) - assert not t.cud1 or (len(t.cud1) == - measure_length(t.cud1, t)) - assert not t.cub1 or (len(t.cub1) == - measure_length(t.cub1, t)) - assert not t.cuf1 or (len(t.cuf1) == - measure_length(t.cuf1, t)) - assert not t.cuu1 or (len(t.cuu1) == - measure_length(t.cuu1, t)) - assert not t.cub or (len(t.cub(333)) == - measure_length(t.cub(333), t)) - assert not t.cuf or (len(t.cuf(333)) == - measure_length(t.cuf(333), t)) - assert not t.home or (len(t.home) == - measure_length(t.home, t)) - assert not t.restore or (len(t.restore) == - measure_length(t.restore, t)) - assert not t.clear or (len(t.clear) == - measure_length(t.clear, t)) - - child_mnemonics_willmove(all_terms) + assert (len(term.move(98, 76)) == + measure_length(term.move(98, 76), term)) + assert (len(term.move(54)) == + measure_length(term.move(54), term)) + assert not term.cud1 or (len(term.cud1) == + measure_length(term.cud1, term)) + assert not term.cub1 or (len(term.cub1) == + measure_length(term.cub1, term)) + assert not term.cuf1 or (len(term.cuf1) == + measure_length(term.cuf1, term)) + assert not term.cuu1 or (len(term.cuu1) == + measure_length(term.cuu1, term)) + assert not term.cub or (len(term.cub(333)) == + measure_length(term.cub(333), term)) + assert not term.cuf or (len(term.cuf(333)) == + measure_length(term.cuf(333), term)) + assert not term.home or (len(term.home) == + measure_length(term.home, term)) + assert not term.restore or (len(term.restore) == + measure_length(term.restore, term)) + assert not term.clear or (len(term.clear) == + measure_length(term.clear, term)) + + child(all_terms) + +def test_termcap_will_move_true(all_terms): + """Test parser about sequences that move the cursor.""" + @as_subprocess + def child(kind): + from blessed.sequences import iter_parse + term = TestTerminal(kind=kind) + assert next(iter_parse(term, term.move(98, 76)))[1].will_move + assert next(iter_parse(term, term.move(54)))[1].will_move + assert next(iter_parse(term, term.cud1))[1].will_move + assert next(iter_parse(term, term.cub1))[1].will_move + assert next(iter_parse(term, term.cuf1))[1].will_move + assert next(iter_parse(term, term.cuu1))[1].will_move + if term.cub(333): + assert next(iter_parse(term, term.cub(333)))[1].will_move + if term.cuf(333): + assert next(iter_parse(term, term.cuf(333)))[1].will_move + assert next(iter_parse(term, term.home))[1].will_move + assert next(iter_parse(term, term.restore))[1].will_move + assert next(iter_parse(term, term.clear))[1].will_move + child(all_terms) + def test_foreign_sequences(): @@ -364,6 +424,6 @@ def test_foreign_sequences(): @as_subprocess def child(kind): from blessed.sequences import measure_length - t = TestTerminal(kind=kind) - assert measure_length(u'\x1b[m', t) == len('\x1b[m') + term = TestTerminal(kind=kind) + assert measure_length(u'\x1b[m', term) == len('\x1b[m') child(kind='ansi') diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index 7fe5a420..9a1d66aa 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -487,7 +487,7 @@ def child(kind): def test_padd(): - """ Test terminal.padd(seq). """ + """ Test Terminal.padd(seq). """ @as_subprocess def child(): from blessed.sequences import Sequence @@ -499,3 +499,24 @@ def child(): assert Sequence('\x1b[3D', term).padd() == u'' # "Trim left" child() + +def test_split_seqs(all_terms): + """Test Terminal.split_seqs.""" + @as_subprocess + def child(kind): + from blessed import Terminal + term = Terminal(kind) + + if term.sc and term.rc: + given_text = term.sc + 'AB' + term.rc + 'CD' + expected = [term.sc, 'A', 'B', term.rc, 'C', 'D'] + result = list(term.split_seqs(given_text)) + assert result == expected + + if term.bold: + given_text = term.bold + 'bbq' + expected = [term.bold, 'b', 'b', 'q'] + result = list(term.split_seqs(given_text)) + assert result == expected + + child(all_terms) diff --git a/docs/history.rst b/docs/history.rst index 56640df0..9c37e728 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -1,5 +1,11 @@ Version History =============== +1.13 + * enhancement: :meth:`~.Terminal.split_seqs` introduced, and 4x cost + reduction in related sequence-aware functions. + * deprecated: ``blessed.sequences.measure_length`` function superseded by + :func:`~.iter_parse` if necessary. + 1.12 * enhancement: :meth:`~.Terminal.get_location` returns the ``(row, col)`` position of the cursor at the time of call for attached terminal. @@ -107,8 +113,8 @@ Version History :attr:`~.color`\(n) may be called on terminals without color capabilities. * bugfix: for terminals without underline, such as vt220, - ``term.underline('text')`` would emit ``u'text' + term.normal``. - Now it emits only ``u'text'``. + ``term.underline('text')`` would emit ``'text' + term.normal``. + Now it emits only ``'text'``. * enhancement: some attributes are now properties, raise exceptions when assigned. * enhancement: pypy is now a supported python platform implementation. diff --git a/docs/overview.rst b/docs/overview.rst index fd637b52..b319294f 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -21,7 +21,7 @@ Its color support: And use construct strings containing color and styling: >>> term.green_reverse('ALL SYSTEMS GO') - u'\x1b[32m\x1b[7mALL SYSTEMS GO\x1b[m' + '\x1b[32m\x1b[7mALL SYSTEMS GO\x1b[m' Furthermore, the special sequences inserted with application keys (arrow and function keys) are understood and decoded, as well as your @@ -431,6 +431,29 @@ poem word-wrapped to 25 columns:: for line in poem: print('\n'.join(term.wrap(line, width=25, subsequent_indent=' ' * 4))) +Sometimes it is necessary to make sense of sequences, and to distinguish them +from plain text. The :meth:`~.Terminal.split_seqs` method can allow us to +iterate over a terminal string by its characters or sequences:: + + from blessed import Terminal + + term = Terminal() + + phrase = term.bold('bbq') + print(term.split_seqs(phrase)) + +Will display something like, ``['\x1b[1m', 'b', 'b', 'q', '\x1b(B', '\x1b[m']`` + +Similarly, the method :meth:`~.Terminal.strip_seqs` may be used on a string to +remove all occurrences of terminal sequences:: + + from blessed import Terminal + + term = Terminal() + phrase = term.bold_black('coffee') + print(repr(term.strip_seqs(phrase))) + +Will display only ``'coffee'`` Keyboard Input -------------- @@ -488,8 +511,8 @@ also provides the special attributes :attr:`~.Keystroke.is_sequence`, print("press 'q' to quit.") with term.cbreak(): - val = u'' - while val not in (u'q', u'Q',): + val = '' + while val.lower() != 'q': val = term.inkey(timeout=5) if not val: # timeout diff --git a/tox.ini b/tox.ini index c8adecbd..6b632f67 100644 --- a/tox.ini +++ b/tox.ini @@ -70,7 +70,7 @@ setenv = TEST_QUICK=1 [pytest] looponfailroots = blessed -norecursedirs = .git +norecursedirs = .git .tox build [coverage] rcfile = {toxinidir}/.coveragerc diff --git a/version.json b/version.json index ec1cdfd4..10f769c2 100644 --- a/version.json +++ b/version.json @@ -1 +1 @@ -{"version": "1.12.0"} +{"version": "1.13.0"} From 9a4dd1a8c4898668770673357fba5ef9055cdc23 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 8 Nov 2015 12:13:15 -0800 Subject: [PATCH 340/459] link github issue and make py2-compatable test --- blessed/tests/test_core.py | 5 +++-- docs/history.rst | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/blessed/tests/test_core.py b/blessed/tests/test_core.py index e4fe886a..046088fa 100644 --- a/blessed/tests/test_core.py +++ b/blessed/tests/test_core.py @@ -479,13 +479,14 @@ def test_termcap_repr(): given_ttype='vt220' given_capname = 'cursor_up' - expected = r"" + expected = [r"", + r""] @as_subprocess def child(): import blessed term = blessed.Terminal(given_ttype) given = repr(term.caps[given_capname]) - assert given == expected + assert given in expected child() diff --git a/docs/history.rst b/docs/history.rst index 9c37e728..9f9b88d1 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -2,9 +2,11 @@ Version History =============== 1.13 * enhancement: :meth:`~.Terminal.split_seqs` introduced, and 4x cost - reduction in related sequence-aware functions. + reduction in related sequence-aware functions, :ghissue:`29`. * deprecated: ``blessed.sequences.measure_length`` function superseded by :func:`~.iter_parse` if necessary. + * deprecated: warnings about "binary-packed capabilities" are no longer + emitted on strange terminal types, making best effort. 1.12 * enhancement: :meth:`~.Terminal.get_location` returns the ``(row, col)`` From f347bfe6f4feae31bac12da5447c96fc2f30dba9 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 8 Nov 2015 12:18:08 -0800 Subject: [PATCH 341/459] use **kwds to avoid py2/3 compat in re.split --- blessed/terminal.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/blessed/terminal.py b/blessed/terminal.py index 6e6bfc8b..55c6ce77 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -825,20 +825,18 @@ def strip_seqs(self, text): """ return Sequence(text, self).strip_seqs() - def split_seqs(self, text, maxsplit=0, flags=0): + def split_seqs(self, text, **kwds): r""" Return ``text`` split by individual character elements and sequences. - :arg int maxpslit: If maxsplit is nonzero, at most ``maxsplit`` - splits occur, and the remainder of the string is returned as - the final element of the list. + :arg kwds: remaining keyword arguments for :func:`re.split`. :rtype: list[str] >>> term.split_seqs(term.underline(u'xyz')) ['\x1b[4m', 'x', 'y', 'z', '\x1b(B', '\x1b[m'] """ - return list(filter(None, re.split(self._caps_unnamed_any, text, - maxsplit=maxsplit, flags=flags))) + pattern = self._caps_unnamed_any + return list(filter(None, re.split(pattern, text, **kwds))) def wrap(self, text, width=None, **kwargs): """ From 0484c46c3393f7ea5d388df2ca32ba807e501ca3 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 8 Nov 2015 12:28:24 -0800 Subject: [PATCH 342/459] appease quantifiedcode --- .checkignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .checkignore diff --git a/.checkignore b/.checkignore new file mode 100644 index 00000000..5c9c5361 --- /dev/null +++ b/.checkignore @@ -0,0 +1 @@ +blessed/tests From 1ec23b9d180a90b3780d5c63773932813482fdbb Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 8 Nov 2015 13:27:03 -0800 Subject: [PATCH 343/459] allow 'iter_parse' from "import *" phrase --- blessed/sequences.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blessed/sequences.py b/blessed/sequences.py index bed5ef43..399ca108 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -13,7 +13,7 @@ import wcwidth import six -__all__ = ('Sequence', 'SequenceTextWrapper', 'measure_length') +__all__ = ('Sequence', 'SequenceTextWrapper', 'iter_parse', 'measure_length') class Termcap(object): From e63b8566eaf44a1b1394af45d9517dfc6a52f0ff Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 8 Nov 2015 13:49:30 -0800 Subject: [PATCH 344/459] bugfix line wrapping behaviour in multiline strings --- blessed/terminal.py | 2 +- blessed/tests/test_wrap.py | 26 ++++++++++++++++++++++++++ docs/history.rst | 3 +++ version.json | 2 +- 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/blessed/terminal.py b/blessed/terminal.py index 89631fa3..08d8ff29 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -858,7 +858,7 @@ def wrap(self, text, width=None, **kwargs): for line in text.splitlines(): lines.extend( (_linewrap for _linewrap in SequenceTextWrapper( - width=width, term=self, **kwargs).wrap(text)) + width=width, term=self, **kwargs).wrap(line)) if line.strip() else (u'',)) return lines diff --git a/blessed/tests/test_wrap.py b/blessed/tests/test_wrap.py index 1026b68d..e0458bd0 100644 --- a/blessed/tests/test_wrap.py +++ b/blessed/tests/test_wrap.py @@ -111,3 +111,29 @@ def child(width, pgraph, kwargs): child(width=many_columns, kwargs=kwargs, pgraph=u' Z! a bc defghij klmnopqrstuvw<<>>xyz012345678900 '*2) child(width=many_columns, kwargs=kwargs, pgraph=u'a bb ccc') + + +def test_multiline(): + """Test that text wrapping matches internal extra options.""" + + @as_subprocess + def child(): + # build a test paragraph, along with a very colorful version + term = TestTerminal() + given_string = ('\n' + (32 * 'A') + '\n' + + (32 * 'B') + '\n' + + (32 * 'C') + '\n\n') + expected = [ + '', + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + 'AA', + 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB', + 'BB', + 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC', + 'CC', + '', + ] + result = term.wrap(given_string, width=30) + assert expected == result + + child() diff --git a/docs/history.rst b/docs/history.rst index 9f9b88d1..f6548d9f 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -1,5 +1,8 @@ Version History =============== +1.14 + * bugfix: term.wrap misbehaved for lines including newlines, :ghissue:`74`. + 1.13 * enhancement: :meth:`~.Terminal.split_seqs` introduced, and 4x cost reduction in related sequence-aware functions, :ghissue:`29`. diff --git a/version.json b/version.json index 10f769c2..eca36dd0 100644 --- a/version.json +++ b/version.json @@ -1 +1 @@ -{"version": "1.13.0"} +{"version": "1.14.0"} From 78f4cf9f0e01e43ef2d5092853e34f51431b9a55 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 8 Nov 2015 13:56:08 -0800 Subject: [PATCH 345/459] gramfix --- docs/history.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/history.rst b/docs/history.rst index f6548d9f..63f18fcb 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -1,7 +1,7 @@ Version History =============== 1.14 - * bugfix: term.wrap misbehaved for lines including newlines, :ghissue:`74`. + * bugfix: term.wrap misbehaved for text containing newlines, :ghissue:`74`. 1.13 * enhancement: :meth:`~.Terminal.split_seqs` introduced, and 4x cost From 491e6b31bec08ed3e6a7efebf6346eb4f929e9e7 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 8 Nov 2015 14:02:13 -0800 Subject: [PATCH 346/459] doesn't work, del --- .checkignore | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .checkignore diff --git a/.checkignore b/.checkignore deleted file mode 100644 index 5c9c5361..00000000 --- a/.checkignore +++ /dev/null @@ -1 +0,0 @@ -blessed/tests From 080e1b24f8d51c7bc78f5ceadde6c4261b95f96a Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 8 Nov 2015 18:59:52 -0800 Subject: [PATCH 347/459] bugfix lint exclude 'start-args' => 'star-args' --- .landscape.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.landscape.yml b/.landscape.yml index 5db2905c..cf70c768 100644 --- a/.landscape.yml +++ b/.landscape.yml @@ -72,7 +72,7 @@ pylint: disable: - protected-access - too-few-public-methods - - start-args + - star-args pyroma: # checks setup.py From 92ae84629a46807999a12faa97167169f4df2495 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 8 Nov 2015 19:02:33 -0800 Subject: [PATCH 348/459] bugfix .. warning sphinx indentation --- blessed/terminal.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/blessed/terminal.py b/blessed/terminal.py index 08d8ff29..d2af6c98 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -495,10 +495,10 @@ def get_location(self, timeout=None): >>> term = Terminal() >>> term.move(*term.get_location())) - .. warning:: You might first test that a terminal is capable of - informing you of its location, while using a timeout, before - later calling. When a timeout is specified, always ensure the - return value is conditionally checked for ``(-1, -1)``. + .. warning:: You might first test that a terminal is capable of + informing you of its location, while using a timeout, before + later calling. When a timeout is specified, always ensure the + return value is conditionally checked for ``(-1, -1)``. """ # Local lines attached by termios and remote login protocols such as # ssh and telnet both provide a means to determine the window From 39268f7681910c4eefb2a088a3a92ea5c14767ac Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 8 Nov 2015 19:04:11 -0800 Subject: [PATCH 349/459] sphinx link to Terminal.wrap method --- docs/history.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/history.rst b/docs/history.rst index 63f18fcb..24067cc1 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -1,7 +1,8 @@ Version History =============== 1.14 - * bugfix: term.wrap misbehaved for text containing newlines, :ghissue:`74`. + * bugfix: :meth:`~.Terminal.wrap` misbehaved for text containing newlines, + :ghissue:`74`. 1.13 * enhancement: :meth:`~.Terminal.split_seqs` introduced, and 4x cost From 9a305e813aa4b753a0b027e8e910d15c4f645eb7 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 29 Nov 2015 22:59:55 -0800 Subject: [PATCH 350/459] "Return a context manager" => "Context manager" --- blessed/terminal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blessed/terminal.py b/blessed/terminal.py index d2af6c98..425b12cd 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -432,7 +432,7 @@ def _height_and_width(self): @contextlib.contextmanager def location(self, x=None, y=None): """ - Return a context manager for temporarily moving the cursor. + Context manager for temporarily moving the cursor. Move the cursor to a certain position on entry, let you print stuff there, then return the cursor to its original position:: @@ -1039,7 +1039,7 @@ def raw(self): @contextlib.contextmanager def keypad(self): r""" - Return a context manager that enables directional keypad input. + Context manager that enables directional keypad input. On entrying, this puts the terminal into "keyboard_transmit" mode by emitting the keypad_xmit (smkx) capability. On exit, it emits From c4aa93f533afd804f50d746f90545d3130576554 Mon Sep 17 00:00:00 2001 From: David Butler Date: Mon, 21 Dec 2015 14:50:43 -0600 Subject: [PATCH 351/459] Fixing https://github.com/jquast/blessed/issues/84 --- blessed/sequences.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/blessed/sequences.py b/blessed/sequences.py index 399ca108..faac2ed2 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -326,19 +326,20 @@ def length(self): # we require ur"" for the docstring, but it is not supported by pep257 # tool: https://github.com/GreenSteam/pep257/issues/116 - length.__doc__ += ( - u"""For example: - - >>> from blessed import Terminal - >>> from blessed.sequences import Sequence - >>> term = Terminal() - >>> Sequence(term.clear + term.red(u'コンニチハ'), term).length() - 10 - - .. note:: Although accounted for, strings containing sequences such as - ``term.clear`` will not give accurate returns, it is not - considered lengthy (a length of 0). - """) + if length.__doc__ is not None: + length.__doc__ += ( + u"""For example: + + >>> from blessed import Terminal + >>> from blessed.sequences import Sequence + >>> term = Terminal() + >>> Sequence(term.clear + term.red(u'コンニチハ'), term).length() + 10 + + .. note:: Although accounted for, strings containing sequences such as + ``term.clear`` will not give accurate returns, it is not + considered lengthy (a length of 0). + """) def strip(self, chars=None): """ From bbde6119d3a182b00b1cb4cc6a94c41f5263441c Mon Sep 17 00:00:00 2001 From: David Butler Date: Mon, 21 Dec 2015 15:00:38 -0600 Subject: [PATCH 352/459] Fixing code quality issue, line length --- blessed/sequences.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blessed/sequences.py b/blessed/sequences.py index faac2ed2..7dfaf976 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -336,8 +336,8 @@ def length(self): >>> Sequence(term.clear + term.red(u'コンニチハ'), term).length() 10 - .. note:: Although accounted for, strings containing sequences such as - ``term.clear`` will not give accurate returns, it is not + .. note:: Although accounted for, strings containing sequences such + as ``term.clear`` will not give accurate returns, it is not considered lengthy (a length of 0). """) From 09e332af78d843631ead9d024a7c5b65b3461992 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Mon, 21 Dec 2015 13:48:37 -0800 Subject: [PATCH 353/459] update static analysis exclusions --- .landscape.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.landscape.yml b/.landscape.yml index cf70c768..8c4ccb78 100644 --- a/.landscape.yml +++ b/.landscape.yml @@ -20,16 +20,14 @@ dodgy: pyflakes: # preferring 'frosted' instead (a fork of) - run: false - -frosted: - # static analysis, pyflakes improved? - # run: true disable: # Terminal imported but unused (false) - 'E101' +frosted: + run: false + mccabe: # complexity checking. We know of only one offense of the # default of 10, which is Terminal.inkey() at exactly 10. @@ -69,10 +67,14 @@ pylint: # 'fd' is a common shorthand term for file descriptor (as int). good-names: _,ks,fd + disable: - protected-access - too-few-public-methods - star-args + - wrong-import-order + - wrong-import-position + - ungrouped-imports pyroma: # checks setup.py From dd4dc985ee265202c6de1341b959b15d2f03e758 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Mon, 21 Dec 2015 13:48:48 -0800 Subject: [PATCH 354/459] satisfy static analysis about order of equality --- bin/keymatrix.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/keymatrix.py b/bin/keymatrix.py index eb83673f..ea69d937 100755 --- a/bin/keymatrix.py +++ b/bin/keymatrix.py @@ -85,7 +85,7 @@ def add_score(score, pts, level): """Add points to score, determine and return new score and level.""" lvl_multiplier = 10 score += pts - if 0 == (score % (pts * lvl_multiplier)): + if (score % (pts * lvl_multiplier)) == 0: level += 1 return score, level @@ -109,7 +109,7 @@ def main(): dirty = True if (inp.is_sequence and inp.name in gameboard and - 0 == gameboard[inp.name]['hit']): + gameboard[inp.name]['hit'] == 0): gameboard[inp.name]['hit'] = 1 score, level = add_score(score, 100, level) elif inp and not inp.is_sequence and 128 <= ord(inp) <= 255: From d98460b290abc79f17b9e8a6896e2e14cb5e00a4 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Mon, 21 Dec 2015 13:49:09 -0800 Subject: [PATCH 355/459] use flake8, not frosted --- requirements-analysis.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-analysis.txt b/requirements-analysis.txt index 492c109e..f36f1409 100644 --- a/requirements-analysis.txt +++ b/requirements-analysis.txt @@ -1,3 +1,3 @@ -prospector[with_frosted,with_pyroma] +prospector[with_pyroma] restructuredtext_lint doc8 From d660ca49532aac4ae28fac5e02c80f24bfeb6c50 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Mon, 21 Dec 2015 13:49:23 -0800 Subject: [PATCH 356/459] use py3.5 for static analysis, bump ver --- tox.ini | 2 +- version.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 6b632f67..4820f2fc 100644 --- a/tox.ini +++ b/tox.ini @@ -37,7 +37,7 @@ commands = python {toxinidir}/bin/display-sighandlers.py python {toxinidir}/bin/display-maxcanon.py [testenv:sa] -basepython = python2.7 +basepython = python3.5 deps = -rrequirements-analysis.txt -rrequirements-about.txt commands = python -m compileall -fq {toxinidir}/blessed diff --git a/version.json b/version.json index eca36dd0..5867d71c 100644 --- a/version.json +++ b/version.json @@ -1 +1 @@ -{"version": "1.14.0"} +{"version": "1.14.1"} From ed6b2174541401a9a56cc4e33a16f9d50be91267 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Mon, 21 Dec 2015 13:49:37 -0800 Subject: [PATCH 357/459] fix length() docstring indentation --- blessed/sequences.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/blessed/sequences.py b/blessed/sequences.py index 7dfaf976..7c6e2da0 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -324,21 +324,22 @@ def length(self): return sum(clip(wcwidth.wcwidth(w_char)) for w_char in self.strip_seqs()) - # we require ur"" for the docstring, but it is not supported by pep257 - # tool: https://github.com/GreenSteam/pep257/issues/116 + # we require ur"" for the docstring, but it is not supported by all + # python versions. if length.__doc__ is not None: length.__doc__ += ( - u"""For example: + u""" + For example: - >>> from blessed import Terminal - >>> from blessed.sequences import Sequence - >>> term = Terminal() - >>> Sequence(term.clear + term.red(u'コンニチハ'), term).length() - 10 + >>> from blessed import Terminal + >>> from blessed.sequences import Sequence + >>> term = Terminal() + >>> Sequence(term.clear + term.red(u'コンニチハ'), term).length() + 10 .. note:: Although accounted for, strings containing sequences such - as ``term.clear`` will not give accurate returns, it is not - considered lengthy (a length of 0). + as ``term.clear`` will not give accurate returns, it is not + considered lengthy (a length of 0). """) def strip(self, chars=None): From dd0b7237ebb934d918e7d8b62db9464193761650 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Mon, 21 Dec 2015 13:49:50 -0800 Subject: [PATCH 358/459] satisfy static analysis --- blessed/terminal.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/blessed/terminal.py b/blessed/terminal.py index 425b12cd..1cda21a0 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -36,6 +36,7 @@ InterruptedError except NameError: # alias py2 exception to py3 + # pylint: disable=redefined-builtin InterruptedError = select.error try: @@ -416,8 +417,6 @@ def _height_and_width(self): """ for fd in (self._init_descriptor, sys.__stdout__): - # pylint: disable=pointless-except - # Except doesn't do anything try: if fd is not None: return self._winsize(fd) From 66030e7f5589dcb9a0ad28f970c7a05e84d8c94b Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Mon, 21 Dec 2015 13:53:50 -0800 Subject: [PATCH 359/459] include .coveragerc --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 5a2ca9ef..f6fd7f2a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,5 +2,6 @@ include docs/*.rst include LICENSE include version.json include *.txt +include .coveragerc include tox.ini include blessed/tests/wall.ans From 5d14dbe87bea268e9084a7547700a7b9002bad75 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Mon, 21 Dec 2015 13:59:27 -0800 Subject: [PATCH 360/459] note about TypeError fix --- docs/history.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/history.rst b/docs/history.rst index 24067cc1..0cf34d3e 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -3,6 +3,8 @@ Version History 1.14 * bugfix: :meth:`~.Terminal.wrap` misbehaved for text containing newlines, :ghissue:`74`. + * bugfix: TypeError when using ``PYTHONOPTIMIZE=2`` environment variable, + :ghissue:`84`. 1.13 * enhancement: :meth:`~.Terminal.split_seqs` introduced, and 4x cost From 34216d1bc6c22397416fa721a9a0957f672a9ff1 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Mon, 21 Dec 2015 14:01:26 -0800 Subject: [PATCH 361/459] line too long after indentation --- blessed/sequences.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/blessed/sequences.py b/blessed/sequences.py index 7c6e2da0..cad01796 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -331,11 +331,12 @@ def length(self): u""" For example: - >>> from blessed import Terminal - >>> from blessed.sequences import Sequence - >>> term = Terminal() - >>> Sequence(term.clear + term.red(u'コンニチハ'), term).length() - 10 + >>> from blessed import Terminal + >>> from blessed.sequences import Sequence + >>> term = Terminal() + >>> msg = term.clear + term.red(u'コンニチハ'), term + >>> Sequence(msg).length() + 10 .. note:: Although accounted for, strings containing sequences such as ``term.clear`` will not give accurate returns, it is not From c4b99eadb011305029e9bfebaf811ccf11a37071 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Mon, 21 Dec 2015 14:02:58 -0800 Subject: [PATCH 362/459] bugfix hexidecimal display values under 10 --- bin/display-fpathconf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/display-fpathconf.py b/bin/display-fpathconf.py index 81229482..84ced9c8 100755 --- a/bin/display-fpathconf.py +++ b/bin/display-fpathconf.py @@ -43,7 +43,7 @@ def display_fpathconf(): try: value = os.fpathconf(fd, name) if name == 'PC_VDISABLE': - value = r'\x{0:2x}'.format(value) + value = r'\x{0:02x}'.format(value) except OSError as err: value = 'OSErrno {0.errno}'.format(err) From 09b51db2b3a1eee4c7bd1c459bd21ea2858350f1 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Mon, 21 Dec 2015 14:09:52 -0800 Subject: [PATCH 363/459] italicize *Blessed* in intro --- docs/intro.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/intro.rst b/docs/intro.rst index 8eb2d3e3..ddd957d4 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -130,7 +130,7 @@ The same program with *Blessed* is simply:: Requirements ------------ -Blessed is tested with Python 2.7, 3.4, and 3.5 on Debian Linux, Mac, and +*Blessed* is tested with Python 2.7, 3.4, and 3.5 on Debian Linux, Mac, and FreeBSD. Further Documentation @@ -155,12 +155,12 @@ in the `issue tracker`_ with a well-formed question. License ------- -Blessed is under the MIT License. See the LICENSE file. +*Blessed* is under the MIT License. See the LICENSE file. Forked ------ -Blessed is a fork of `blessings `_. +*Blessed* is a fork of `blessings `_. Changes since 1.7 have all been proposed but unaccepted upstream. Furthermore, a project in the node.js language of the `same name From 9a4ee62394dea203f516d15d996c76257694b348 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Mon, 21 Dec 2015 14:31:14 -0800 Subject: [PATCH 364/459] add missing test assertions! --- blessed/tests/test_formatters.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blessed/tests/test_formatters.py b/blessed/tests/test_formatters.py index 01762d33..660e9209 100644 --- a/blessed/tests/test_formatters.py +++ b/blessed/tests/test_formatters.py @@ -382,7 +382,7 @@ def test_pickled_parameterizing_string(monkeypatch): for proto_num in range(pickle.HIGHEST_PROTOCOL): assert pstr == pickle.loads(pickle.dumps(pstr, protocol=proto_num)) w.send(pstr) - r.recv() == pstr + assert r.recv() == pstr # exercise picklability of FormattingString # -- the return value of calling ParameterizingString @@ -390,7 +390,7 @@ def test_pickled_parameterizing_string(monkeypatch): for proto_num in range(pickle.HIGHEST_PROTOCOL): assert zero == pickle.loads(pickle.dumps(zero, protocol=proto_num)) w.send(zero) - r.recv() == zero + assert r.recv() == zero def test_tparm_returns_null(monkeypatch): From 13e215a797aaae7a533fa9357264839d355d4d6b Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 6 Mar 2016 08:47:04 -0800 Subject: [PATCH 365/459] spellfix horizontal/vertical, change phrasing --- CONTRIBUTING.rst | 3 +-- docs/overview.rst | 12 ++++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index fd54bd79..5aa648c2 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -15,8 +15,7 @@ Prepare a developer environment. Then, from the blessed code folder:: pip install --editable . Any changes made in this project folder are then made available to the python -interpreter as the 'blessed' package regardless of the current working -directory. +interpreter as the 'blessed' package from any working directory. Running Tests ~~~~~~~~~~~~~ diff --git a/docs/overview.rst b/docs/overview.rst index b319294f..2da7c47b 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -278,12 +278,12 @@ this:: term = Terminal() print(term.move(10, 1) + 'Hi, mom!') -``move`` - Position the cursor, parameter in form of *(y, x)* -``move_x`` - Position the cursor at given horizontal column. -``move_y`` - Position the cursor at given vertical column. +``move(y, x)`` + Position cursor at given **y**, **x**. +``move_x(x)`` + Position cursor at column **x**. +``move_y(y)`` + Position cursor at row **y**. One-Notch Movement ~~~~~~~~~~~~~~~~~~ From 0427875c9a642803a56fc44c62ff06742f42824c Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Tue, 7 Feb 2017 20:15:11 -0800 Subject: [PATCH 366/459] reference rubout, '\b \b' --- bin/editor.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bin/editor.py b/bin/editor.py index 979b4e41..877e2787 100755 --- a/bin/editor.py +++ b/bin/editor.py @@ -79,6 +79,11 @@ def readline(term, width=20): echo(inp) elif inp.code in (term.KEY_BACKSPACE, term.KEY_DELETE): text = text[:-1] + # https://utcc.utoronto.ca/~cks/space/blog/unix/HowUnixBackspaces + # + # "When you hit backspace, the kernel tty line discipline rubs out your previous + # character by printing (in the simple case) Ctrl-H, a space, and then another + # Ctrl-H." echo(u'\b \b') return text From 17f2f6dcd78a011cff9212d5b3b50bfb5b509629 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Tue, 7 Feb 2017 20:43:15 -0800 Subject: [PATCH 367/459] docfix "raw" mode, more comprehendible --- blessed/terminal.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/blessed/terminal.py b/blessed/terminal.py index 1cda21a0..7413beb2 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -1002,17 +1002,16 @@ def raw(self): r""" A context manager for :func:`tty.setraw`. - Raw mode differs from :meth:`cbreak` mode in that input and output - processing of characters is disabled, in similar in that they both - allow each keystroke to be read immediately after it is pressed. + Raw mode, like :meth:`cbreak` mode, allows each keystroke to be read + immediately after it is pressed. It differs from :meth:`cbreak` in + that *input and output processing is disabled. - For input, the interrupt, quit, suspend, and flow control characters - are received as their raw control character values rather than - generating a signal. + Interrupt, quit, suspend, and flow control characters are received as + their raw control character values rather than generating a signal. - For output, the newline ``chr(10)`` is not sufficient enough to return - the carriage, requiring ``chr(13)`` printed explicitly by your - program:: + Because output processing is not done, the newline ``'\n'`` is not + enough, you must also print carriage return to ensure that the cursor + is returned to the first column:: with term.raw(): print("printing in raw mode", end="\r\n") From b95b9f1d243d95ea4194d067dd7cfb65f815d150 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Tue, 7 Feb 2017 20:47:31 -0800 Subject: [PATCH 368/459] we can recommend raw mode, with caveats described --- docs/overview.rst | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/overview.rst b/docs/overview.rst index 2da7c47b..4a87660e 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -490,7 +490,21 @@ The mode entered using :meth:`~.Terminal.cbreak` is called character-processing (interrupt and flow control characters are unaffected), making characters typed by the user immediately available to the program. -:meth:`~.Terminal.raw` is similar to cbreak, but not recommended. +raw +~~~ + +:meth:`~.Terminal.raw` is similar to cbreak, except that control-C and +other keystrokes are "ignored", and received as their keystroke value +rather than interrupting the program with signals. + +Output processing is also disabled, you must print phrases with carriage +return after newline. Without raw mode:: + + print("hello, world.") + +With raw mode:: + + print("hello, world.", endl="\r\n") inkey ~~~~~ From f249a2ce6096bfa3f222704ba6385a9d2c43d635 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 24 Mar 2017 14:13:14 -0700 Subject: [PATCH 369/459] Detect \x1b[0K and \x1b[2K for measuring width, define __version__ (#96) - Detect \x1b[0K and \x1b[2K for measuring width - define __version__ - new tool for bumping version number consistantly - bump for release 1.14.2 --- blessed/__init__.py | 1 + blessed/_capabilities.py | 4 ++++ blessed/terminal.py | 24 ++++++++++++++--------- docs/history.rst | 4 ++++ tools/bump-version.py | 41 ++++++++++++++++++++++++++++++++++++++++ version.json | 2 +- 6 files changed, 66 insertions(+), 10 deletions(-) create mode 100755 tools/bump-version.py diff --git a/blessed/__init__.py b/blessed/__init__.py index 88954e18..cf5fbb9b 100644 --- a/blessed/__init__.py +++ b/blessed/__init__.py @@ -16,3 +16,4 @@ 'support due to http://bugs.python.org/issue10570.') __all__ = ('Terminal',) +__version__ = '1.14.2' diff --git a/blessed/_capabilities.py b/blessed/_capabilities.py index a4c6d05e..c6f96c9f 100644 --- a/blessed/_capabilities.py +++ b/blessed/_capabilities.py @@ -142,6 +142,10 @@ 'sgr0': ('sgr0', re.escape('\x1b') + r'\[m'), 'backspace': ('', re.escape('\b')), 'ascii_tab': ('', re.escape('\t')), + 'clr_eol': ('', re.escape('\x1b[K')), + 'clr_eol0': ('', re.escape('\x1b[0K')), + 'clr_bol': ('', re.escape('\x1b[1K')), + 'clr_eosK': ('', re.escape('\x1b[2K')), } CAPABILITIES_CAUSE_MOVEMENT = ( diff --git a/blessed/terminal.py b/blessed/terminal.py index 7413beb2..fc9c8af7 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -222,9 +222,8 @@ def __init__(self, kind=None, stream=None, force_styling=False): ' returned for the remainder of this process.' % ( self._kind, _CUR_TERM,)) - # initialize capabilities database + # initialize capabilities and terminal keycodes database self.__init__capabilities() - self.__init__keycodes() def __init__capabilities(self): @@ -274,17 +273,24 @@ def __init__keycodes(self): # Build database of sequence <=> KEY_NAME. self._keymap = get_keyboard_sequences(self) + # build set of prefixes of sequences self._keymap_prefixes = get_leading_prefixes(self._keymap) + # keyboard stream buffer self._keyboard_buf = collections.deque() + if self._keyboard_fd is not None: + # set input encoding and initialize incremental decoder locale.setlocale(locale.LC_ALL, '') self._encoding = locale.getpreferredencoding() or 'ascii' + try: self._keyboard_decoder = codecs.getincrementaldecoder( self._encoding)() + except LookupError as err: + # encoding is illegal or unsupported, use 'ascii' warnings.warn('LookupError: {0}, fallback to ASCII for ' 'keyboard.'.format(err)) self._encoding = 'ascii' @@ -1002,20 +1008,20 @@ def raw(self): r""" A context manager for :func:`tty.setraw`. - Raw mode, like :meth:`cbreak` mode, allows each keystroke to be read - immediately after it is pressed. It differs from :meth:`cbreak` in - that *input and output processing is disabled. + Although both :meth:`break` and :meth:`raw` modes allow each keystroke + to be read immediately after it is pressed, Raw mode disables processing + of input and output. - Interrupt, quit, suspend, and flow control characters are received as - their raw control character values rather than generating a signal. + In cbreak mode, special input characters such as ``^C`` or ``^S`` are + interpreted by the terminal driver and excluded from the stdin stream. + In raw mode these values are receive by the :meth:`inkey` method. Because output processing is not done, the newline ``'\n'`` is not enough, you must also print carriage return to ensure that the cursor is returned to the first column:: with term.raw(): - print("printing in raw mode", end="\r\n") - + print("printing in raw mode", end="\r\n") """ if HAS_TTY and self._keyboard_fd is not None: # Save current terminal mode: diff --git a/docs/history.rst b/docs/history.rst index 0cf34d3e..e593377d 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -5,6 +5,10 @@ Version History :ghissue:`74`. * bugfix: TypeError when using ``PYTHONOPTIMIZE=2`` environment variable, :ghissue:`84`. + * bugfix: define ``blessed.__version__`` value, + :ghissue:`92`. + * bugfix: detect sequences ``\x1b[0K`` and ``\x1b2K``, + :ghissue:`95`. 1.13 * enhancement: :meth:`~.Terminal.split_seqs` introduced, and 4x cost diff --git a/tools/bump-version.py b/tools/bump-version.py new file mode 100755 index 00000000..f3f35978 --- /dev/null +++ b/tools/bump-version.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +import json +import sys +import os + +json_version = os.path.join( + os.path.dirname(__file__), os.path.pardir, 'version.json') + +init_file = os.path.join( + os.path.dirname(__file__), os.path.pardir, 'blessed', '__init__.py') + +def main(bump_arg): + assert bump_arg in ('--minor', '--major', '--release'), bump_arg + + with open(json_version, 'r') as fin: + data = json.load(fin) + + release, major, minor = map(int, data['version'].split('.')) + release = release + 1 if bump_arg == '--release' else release + major = major + 1 if bump_arg == '--major' else major + minor = minor + 1 if bump_arg == '--minor' else minor + new_version = '.'.join(map(str, [release, major, minor])) + new_data = {'version': new_version} + + with open(json_version, 'w') as fout: + json.dump(new_data, fout) + + with open(init_file, 'r') as fin: + file_contents = fin.readlines() + + new_contents = [] + for line in file_contents: + if line.startswith('__version__ = '): + line = '__version__ = {!r}\n'.format(new_version) + new_contents.append(line) + + with open(init_file, 'w') as fout: + fout.writelines(new_contents) + +if __name__ == '__main__': + main(sys.argv[1]) diff --git a/version.json b/version.json index 5867d71c..00a1292b 100644 --- a/version.json +++ b/version.json @@ -1 +1 @@ -{"version": "1.14.1"} +{"version": "1.14.2"} \ No newline at end of file From 106bcead0dc508dfdb37ce6d117a1fda8d7099c8 Mon Sep 17 00:00:00 2001 From: Duncan Lock Date: Tue, 6 Jun 2017 14:57:52 -0700 Subject: [PATCH 370/459] Update intro.rst to enable syntax highlighting Use: ``` .. code-block:: python ``` instead of `::` for code blocks, to enable syntax highlighting on github. --- docs/intro.rst | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/intro.rst b/docs/intro.rst index ddd957d4..4135c2ba 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -27,7 +27,9 @@ Introduction Blessed is a thin, practical wrapper around terminal capabilities in Python. -Coding with *Blessed* looks like this... :: +Coding with *Blessed* looks like this... + +.. code-block:: python from blessed import Terminal @@ -82,7 +84,9 @@ Before And After ---------------- With the built-in curses_ module, this is how you would typically -print some underlined text at the bottom of the screen:: +print some underlined text at the bottom of the screen: + +.. code-block:: python from curses import tigetstr, setupterm, tparm from fcntl import ioctl @@ -119,7 +123,9 @@ print some underlined text at the bottom of the screen:: # Restore cursor position. print(rc) -The same program with *Blessed* is simply:: +The same program with *Blessed* is simply: + +.. code-block:: python from blessed import Terminal From ca59bbcd5eac2ad8f873fe3e51d163e9b0bbdc8b Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Wed, 23 May 2018 11:15:27 -0400 Subject: [PATCH 371/459] Add support for Python 3.7 --- .travis.yml | 6 +++++- blessed/sequences.py | 4 ++-- blessed/tests/test_core.py | 3 ++- setup.py | 3 +++ tox.ini | 14 ++++++++++---- 5 files changed, 22 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index ecb95fbf..ccc2438c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,11 @@ matrix: - python: 3.4 env: TOXENV=py34 TEST_QUICK=1 COVERAGE_ID=travis-ci - python: 3.5 - env: TOXENV=py35 COVERAGE_ID=travis-ci + env: TOXENV=py35 TEST_QUICK=1 COVERAGE_ID=travis-ci + - python: 3.6 + env: TOXENV=py36 COVERAGE_ID=travis-ci + - python: 3.7-dev + env: TOXENV=py37 COVERAGE_ID=travis-ci install: - pip install tox script: diff --git a/blessed/sequences.py b/blessed/sequences.py index cad01796..6bff1b55 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -129,9 +129,9 @@ def build(cls, name, capability, attribute, nparams=0, return cls(name, pattern, attribute) if match_grouped: - pattern = re.sub(r'(\d+)', _numeric_regex, _outp) + pattern = re.sub(r'(\d+)', lambda x: _numeric_regex, _outp) else: - pattern = re.sub(r'\d+', _numeric_regex, _outp) + pattern = re.sub(r'\d+', lambda x: _numeric_regex, _outp) return cls(name, pattern, attribute) diff --git a/blessed/tests/test_core.py b/blessed/tests/test_core.py index 046088fa..bda924d4 100644 --- a/blessed/tests/test_core.py +++ b/blessed/tests/test_core.py @@ -479,7 +479,8 @@ def test_termcap_repr(): given_ttype='vt220' given_capname = 'cursor_up' - expected = [r"", + expected = [r"", + r"", r""] @as_subprocess diff --git a/setup.py b/setup.py index 7c10f629..c7c50f74 100755 --- a/setup.py +++ b/setup.py @@ -59,6 +59,9 @@ def _get_long_description(fname): 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', 'Topic :: Software Development :: Libraries', 'Topic :: Software Development :: User Interfaces', 'Topic :: Terminals' diff --git a/tox.ini b/tox.ini index 4820f2fc..4b519e18 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = about, sa, sphinx, py{26,27,34,35} +envlist = about, sa, sphinx, py{26,27,34,35,36,37} skip_missing_interpreters = true [testenv] @@ -30,14 +30,14 @@ commands = coveralls [testenv:about] deps = -rrequirements-about.txt -basepython = python3.5 +basepython = python3.6 commands = python {toxinidir}/bin/display-sighandlers.py python {toxinidir}/bin/display-terminalinfo.py python {toxinidir}/bin/display-fpathconf.py python {toxinidir}/bin/display-maxcanon.py [testenv:sa] -basepython = python3.5 +basepython = python3.6 deps = -rrequirements-analysis.txt -rrequirements-about.txt commands = python -m compileall -fq {toxinidir}/blessed @@ -49,7 +49,7 @@ commands = python -m compileall -fq {toxinidir}/blessed [testenv:sphinx] whitelist_externals = echo -basepython = python3.5 +basepython = python3.6 deps = -rrequirements-docs.txt commands = {envbindir}/sphinx-build -v -W \ -d {toxinidir}/docs/_build/doctrees \ @@ -57,6 +57,12 @@ commands = {envbindir}/sphinx-build -v -W \ {toxinidir}/docs/_build/html echo "--> open docs/_build/html/index.html for review." +[testenv:py35] +# there is not much difference of py34 vs. 35 in blessed +# library; prefer testing integration against py35, and +# just do a 'quick' on py34, if exists. +setenv = TEST_QUICK=1 + [testenv:py34] # there is not much difference of py34 vs. 35 in blessed # library; prefer testing integration against py35, and From 042573d460612bf810556a3229d4170ea93358d6 Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Wed, 23 May 2018 11:27:49 -0400 Subject: [PATCH 372/459] Workaround for https://github.com/travis-ci/travis-ci/issues/8363 --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index ccc2438c..6fa927a0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,9 @@ matrix: env: TOXENV=py36 COVERAGE_ID=travis-ci - python: 3.7-dev env: TOXENV=py37 COVERAGE_ID=travis-ci +before_install: + # work around https://github.com/travis-ci/travis-ci/issues/8363 + - pyenv global system 3.6 install: - pip install tox script: From 279b4276bcdc234844960cc62440a8e09b89cba5 Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Wed, 23 May 2018 11:43:54 -0400 Subject: [PATCH 373/459] Pass TRAVIS envvar to tox --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 4b519e18..bf6013ad 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ skip_missing_interpreters = true [testenv] whitelist_externals = cp setenv = PYTHONIOENCODING=UTF8 -passenv = TEST_QUICK TEST_FULL +passenv = TEST_QUICK TEST_FULL TRAVIS deps = -rrequirements-tests.txt commands = {envbindir}/py.test {posargs:\ --strict --verbose --verbose --color=yes \ From 42ee7d6fab9b663288dca7fc9948d6ae7fc7f074 Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Wed, 23 May 2018 14:42:58 -0400 Subject: [PATCH 374/459] Fix issues with static analysis --- .landscape.yml | 5 ++++- bin/display-maxcanon.py | 2 +- bin/editor.py | 6 +++--- bin/on_resize.py | 2 +- bin/progress_bar.py | 2 +- bin/worms.py | 4 ++-- blessed/formatters.py | 22 +++++++++++----------- blessed/keyboard.py | 10 +++------- blessed/sequences.py | 7 ++++--- blessed/terminal.py | 16 +++++++++------- requirements-analysis.txt | 1 + tox.ini | 1 + 12 files changed, 41 insertions(+), 37 deletions(-) diff --git a/.landscape.yml b/.landscape.yml index 8c4ccb78..30a05c1f 100644 --- a/.landscape.yml +++ b/.landscape.yml @@ -45,7 +45,10 @@ pep257: - 'D203' # 1 blank line required after class docstring - 'D204' - + # Multi-line docstring summary should start at the first line + - 'D212' + # First line should be in imperative mood + - 'D401' pep8: # style checking diff --git a/bin/display-maxcanon.py b/bin/display-maxcanon.py index 1df8a7e4..2839202f 100755 --- a/bin/display-maxcanon.py +++ b/bin/display-maxcanon.py @@ -1,6 +1,6 @@ #!/usr/bin/env python """ -This tool uses pexpect to test expected Canonical mode length. +A tool which uses pexpect to test expected Canonical mode length. All systems use the value of MAX_CANON which can be found using fpathconf(3) value PC_MAX_CANON -- with the exception of Linux diff --git a/bin/editor.py b/bin/editor.py index 877e2787..4b1c36e0 100755 --- a/bin/editor.py +++ b/bin/editor.py @@ -81,9 +81,9 @@ def readline(term, width=20): text = text[:-1] # https://utcc.utoronto.ca/~cks/space/blog/unix/HowUnixBackspaces # - # "When you hit backspace, the kernel tty line discipline rubs out your previous - # character by printing (in the simple case) Ctrl-H, a space, and then another - # Ctrl-H." + # "When you hit backspace, the kernel tty line discipline rubs out + # your previous character by printing (in the simple case) + # Ctrl-H, a space, and then another Ctrl-H." echo(u'\b \b') return text diff --git a/bin/on_resize.py b/bin/on_resize.py index 7a483376..e77bb453 100755 --- a/bin/on_resize.py +++ b/bin/on_resize.py @@ -1,6 +1,6 @@ #!/usr/bin/env python """ -This is an example application for the 'blessed' Terminal library for python. +Example application for the 'blessed' Terminal library for python. Window size changes are caught by the 'on_resize' function using a traditional signal handler. Meanwhile, blocking keyboard input is displayed to stdout. diff --git a/bin/progress_bar.py b/bin/progress_bar.py index 037f1ad2..6a2ebfba 100755 --- a/bin/progress_bar.py +++ b/bin/progress_bar.py @@ -1,6 +1,6 @@ #!/usr/bin/env python """ -This is an example application for the 'blessed' Terminal library for python. +Example application for the 'blessed' Terminal library for python. This isn't a real progress bar, just a sample "animated prompt" of sorts that demonstrates the separate move_x() and move_y() functions, made diff --git a/bin/worms.py b/bin/worms.py index 907667ec..f1353c77 100755 --- a/bin/worms.py +++ b/bin/worms.py @@ -1,6 +1,6 @@ #!/usr/bin/env python """ -This is an example application for the 'blessed' Terminal library for python. +Example application for the 'blessed' Terminal library for python. It is also an experiment in functional programming. """ @@ -25,7 +25,7 @@ import sys def echo(text): - """python 2 version of print(end='', flush=True).""" + """Python 2 version of print(end='', flush=True).""" sys.stdout.write(u'{0}'.format(text)) sys.stdout.flush() diff --git a/blessed/formatters.py b/blessed/formatters.py index 66020738..5484bb25 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -1,4 +1,4 @@ -"""This sub-module provides sequence-formatting functions.""" +"""Sub-module providing sequence-formatting functions.""" # standard imports import curses @@ -59,10 +59,10 @@ def __new__(cls, *args): :arg normal: terminating sequence for this capability (optional). :arg name: name of this terminal capability (optional). """ - assert len(args) and len(args) < 4, args + assert args and len(args) < 4, args new = six.text_type.__new__(cls, args[0]) - new._normal = len(args) > 1 and args[1] or u'' - new._name = len(args) > 2 and args[2] or u'' + new._normal = args[1] if len(args) > 1 else u'' + new._name = args[2] if len(args) > 2 else u'' return new def __call__(self, *args): @@ -84,7 +84,7 @@ def __call__(self, *args): except TypeError as err: # If the first non-int (i.e. incorrect) arg was a string, suggest # something intelligent: - if len(args) and isinstance(args[0], six.string_types): + if args and isinstance(args[0], six.string_types): raise TypeError( "A native or nonexistent capability template, %r received" " invalid argument %r: %s. You probably misspelled a" @@ -135,13 +135,13 @@ def __new__(cls, *args): :arg normal: terminating sequence for this capability (optional). :arg name: name of this terminal capability (optional). """ - assert len(args) and len(args) < 4, args + assert args and len(args) < 4, args assert isinstance(args[0], tuple), args[0] assert callable(args[0][1]), args[0][1] new = six.text_type.__new__(cls, args[0][0]) new._fmt_args = args[0][1] - new._normal = len(args) > 1 and args[1] or u'' - new._name = len(args) > 2 and args[2] or u'' + new._normal = args[1] if len(args) > 1 else u'' + new._name = args[2] if len(args) > 2 else u'' return new def __call__(self, *args): @@ -226,7 +226,7 @@ def __new__(cls, *args): """ assert 1 <= len(args) <= 2, args new = six.text_type.__new__(cls, args[0]) - new._normal = len(args) > 1 and args[1] or u'' + new._normal = args[1] if len(args) > 1 else u'' return new def __call__(self, *args): @@ -246,7 +246,7 @@ def __call__(self, *args): expected_types=six.string_types, )) postfix = u'' - if len(self) and self._normal: + if self and self._normal: postfix = self._normal _refresh = self._normal + self args = [_refresh.join(ucs_part.split(self._normal)) @@ -281,7 +281,7 @@ def __call__(self, *args): the first arg, acting in place of :class:`FormattingString` without any attributes. """ - if len(args) == 0 or isinstance(args[0], int): + if not args or isinstance(args[0], int): # As a NullCallableString, even when provided with a parameter, # such as t.color(5), we must also still be callable, fe: # diff --git a/blessed/keyboard.py b/blessed/keyboard.py index 81b82573..bbf43265 100644 --- a/blessed/keyboard.py +++ b/blessed/keyboard.py @@ -1,4 +1,4 @@ -"""This sub-module provides 'keyboard awareness'.""" +"""Sub-module providing 'keyboard awareness'.""" # std imports import curses.has_key @@ -52,8 +52,7 @@ def is_sequence(self): def __repr__(self): """Docstring overwritten.""" - return (self._name is None and - six.text_type.__repr__(self) or + return (six.text_type.__repr__(self) if self._name is None else self._name) __repr__.__doc__ = six.text_type.__doc__ @@ -246,10 +245,7 @@ class method :meth:`~.Terminal.kbhit` and similar functions. :returns: time remaining as float. If no time is remaining, then the integer ``0`` is returned. """ - if timeout is not None: - if timeout == 0: - return 0 - return max(0, timeout - (time.time() - stime)) + return max(0, timeout - (time.time() - stime)) if timeout else timeout def _read_until(term, pattern, timeout): diff --git a/blessed/sequences.py b/blessed/sequences.py index 6bff1b55..e2aef824 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -1,5 +1,5 @@ # encoding: utf-8 -"""This module provides 'sequence awareness'.""" +"""Module providing 'sequence awareness'.""" # std imports import functools import textwrap @@ -136,7 +136,7 @@ def build(cls, name, capability, attribute, nparams=0, class SequenceTextWrapper(textwrap.TextWrapper): - """This docstring overridden.""" + """Docstring overridden.""" def __init__(self, width, term, **kwargs): """ @@ -195,7 +195,8 @@ def _wrap_chunks(self, chunks): return lines def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width): - """Sequence-aware :meth:`textwrap.TextWrapper._handle_long_word`. + """ + Sequence-aware :meth:`textwrap.TextWrapper._handle_long_word`. This simply ensures that word boundaries are not broken mid-sequence, as standard python textwrap would incorrectly determine the length diff --git a/blessed/terminal.py b/blessed/terminal.py index fc9c8af7..84ea99bf 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -1,5 +1,5 @@ # encoding: utf-8 -"""This module contains :class:`Terminal`, the primary API entry point.""" +"""Module containing :class:`Terminal`, the primary API entry point.""" # pylint: disable=too-many-lines # Too many lines in module (1027/1000) import codecs @@ -75,6 +75,9 @@ ) +_CUR_TERM = None # See comments at end of file + + class Terminal(object): """ An abstraction for color, style, positioning, and input in the terminal. @@ -191,9 +194,8 @@ def __init__(self, kind=None, stream=None, force_styling=False): self._normal = None # cache normal attr, preventing recursive lookups # The descriptor to direct terminal initialization sequences to. - self._init_descriptor = (stream_fd is None and - sys.__stdout__.fileno() or - stream_fd) + self._init_descriptor = (sys.__stdout__.fileno() if stream_fd is None + else stream_fd) self._kind = kind or os.environ.get('TERM', 'unknown') if self.does_styling: @@ -1009,8 +1011,8 @@ def raw(self): A context manager for :func:`tty.setraw`. Although both :meth:`break` and :meth:`raw` modes allow each keystroke - to be read immediately after it is pressed, Raw mode disables processing - of input and output. + to be read immediately after it is pressed, Raw mode disables + processing of input and output. In cbreak mode, special input characters such as ``^C`` or ``^S`` are interpreted by the terminal driver and excluded from the stdin stream. @@ -1177,6 +1179,7 @@ class WINSZ(collections.namedtuple('WINSZ', ( _BUF = '\x00' * struct.calcsize(_FMT) +#: _CUR_TERM = None #: From libcurses/doc/ncurses-intro.html (ESR, Thomas Dickey, et. al):: #: #: "After the call to setupterm(), the global variable cur_term is set to @@ -1196,4 +1199,3 @@ class WINSZ(collections.namedtuple('WINSZ', ( #: Therefore, the :attr:`Terminal.kind` of each :class:`Terminal` is #: essentially a singleton. This global variable reflects that, and a warning #: is emitted if somebody expects otherwise. -_CUR_TERM = None diff --git a/requirements-analysis.txt b/requirements-analysis.txt index f36f1409..1f5e69b4 100644 --- a/requirements-analysis.txt +++ b/requirements-analysis.txt @@ -1,3 +1,4 @@ prospector[with_pyroma] restructuredtext_lint doc8 +Pygments diff --git a/tox.ini b/tox.ini index bf6013ad..f68268f6 100644 --- a/tox.ini +++ b/tox.ini @@ -43,6 +43,7 @@ deps = -rrequirements-analysis.txt commands = python -m compileall -fq {toxinidir}/blessed {envbindir}/prospector \ --die-on-tool-error \ + --no-external-config \ {toxinidir} {envbindir}/rst-lint README.rst {envbindir}/doc8 --ignore-path docs/_build --ignore D000 docs From 465f384edd6d2ca84b0325face95b9eac4dd56e6 Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Wed, 23 May 2018 15:00:10 -0400 Subject: [PATCH 375/459] Temporarily disable display-maxcanon.py in tox --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index f68268f6..42838f58 100644 --- a/tox.ini +++ b/tox.ini @@ -34,7 +34,8 @@ basepython = python3.6 commands = python {toxinidir}/bin/display-sighandlers.py python {toxinidir}/bin/display-terminalinfo.py python {toxinidir}/bin/display-fpathconf.py - python {toxinidir}/bin/display-maxcanon.py + # Temporarily disable until limits are added to MAX_CANON logic + # python {toxinidir}/bin/display-maxcanon.py [testenv:sa] basepython = python3.6 From f59e53607b51ec866bd9c13e1673873a774eed27 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Tue, 19 Jun 2018 21:58:45 -0700 Subject: [PATCH 376/459] remove timing tests for folks timing tests can only be tested using integration tests such as these, but debian wishes to run all of our tests, there are not any formal contracts to prevent people from running certain tests, such as explicit definition of test groups. --- blessed/tests/test_keyboard.py | 1124 ++++++++++++++++---------------- 1 file changed, 562 insertions(+), 562 deletions(-) diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index 9df528d6..dd351969 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -5,7 +5,7 @@ import tempfile import signal import curses -import time +#import time import math import tty # NOQA import pty @@ -35,89 +35,89 @@ unichr = chr -@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, - reason="TEST_QUICK specified") -def test_kbhit_interrupted(): - "kbhit() should not be interrupted with a signal handler." - pid, master_fd = pty.fork() - if pid == 0: - cov = init_subproc_coverage('test_kbhit_interrupted') - - # child pauses, writes semaphore and begins awaiting input - global got_sigwinch - got_sigwinch = False - - def on_resize(sig, action): - global got_sigwinch - got_sigwinch = True - - term = TestTerminal() - signal.signal(signal.SIGWINCH, on_resize) - read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) - os.write(sys.__stdout__.fileno(), SEMAPHORE) - with term.raw(): - assert term.inkey(timeout=1.05) == u'' - os.write(sys.__stdout__.fileno(), b'complete') - assert got_sigwinch - if cov is not None: - cov.stop() - cov.save() - os._exit(0) - - with echo_off(master_fd): - os.write(master_fd, SEND_SEMAPHORE) - read_until_semaphore(master_fd) - stime = time.time() - os.kill(pid, signal.SIGWINCH) - output = read_until_eof(master_fd) - - pid, status = os.waitpid(pid, 0) - assert output == u'complete' - assert os.WEXITSTATUS(status) == 0 - assert math.floor(time.time() - stime) == 1.0 - - -@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, - reason="TEST_QUICK specified") -def test_kbhit_interrupted_nonetype(): - "kbhit() should also allow interruption with timeout of None." - pid, master_fd = pty.fork() - if pid == 0: - cov = init_subproc_coverage('test_kbhit_interrupted_nonetype') - - # child pauses, writes semaphore and begins awaiting input - global got_sigwinch - got_sigwinch = False - - def on_resize(sig, action): - global got_sigwinch - got_sigwinch = True - - term = TestTerminal() - signal.signal(signal.SIGWINCH, on_resize) - read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) - os.write(sys.__stdout__.fileno(), SEMAPHORE) - with term.raw(): - term.inkey(timeout=1) - os.write(sys.__stdout__.fileno(), b'complete') - assert got_sigwinch - if cov is not None: - cov.stop() - cov.save() - os._exit(0) - - with echo_off(master_fd): - os.write(master_fd, SEND_SEMAPHORE) - read_until_semaphore(master_fd) - stime = time.time() - time.sleep(0.05) - os.kill(pid, signal.SIGWINCH) - output = read_until_eof(master_fd) - - pid, status = os.waitpid(pid, 0) - assert output == u'complete' - assert os.WEXITSTATUS(status) == 0 - assert math.floor(time.time() - stime) == 1.0 +#@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, +# reason="TEST_QUICK specified") +#def test_kbhit_interrupted(): +# "kbhit() should not be interrupted with a signal handler." +# pid, master_fd = pty.fork() +# if pid == 0: +# cov = init_subproc_coverage('test_kbhit_interrupted') +# +# # child pauses, writes semaphore and begins awaiting input +# global got_sigwinch +# got_sigwinch = False +# +# def on_resize(sig, action): +# global got_sigwinch +# got_sigwinch = True +# +# term = TestTerminal() +# signal.signal(signal.SIGWINCH, on_resize) +# read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) +# os.write(sys.__stdout__.fileno(), SEMAPHORE) +# with term.raw(): +# assert term.inkey(timeout=1.05) == u'' +# os.write(sys.__stdout__.fileno(), b'complete') +# assert got_sigwinch +# if cov is not None: +# cov.stop() +# cov.save() +# os._exit(0) +# +# with echo_off(master_fd): +# os.write(master_fd, SEND_SEMAPHORE) +# read_until_semaphore(master_fd) +# stime = time.time() +# os.kill(pid, signal.SIGWINCH) +# output = read_until_eof(master_fd) +# +# pid, status = os.waitpid(pid, 0) +# assert output == u'complete' +# assert os.WEXITSTATUS(status) == 0 +# assert math.floor(time.time() - stime) == 1.0 +# +# +#@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, +# reason="TEST_QUICK specified") +#def test_kbhit_interrupted_nonetype(): +# "kbhit() should also allow interruption with timeout of None." +# pid, master_fd = pty.fork() +# if pid == 0: +# cov = init_subproc_coverage('test_kbhit_interrupted_nonetype') +# +# # child pauses, writes semaphore and begins awaiting input +# global got_sigwinch +# got_sigwinch = False +# +# def on_resize(sig, action): +# global got_sigwinch +# got_sigwinch = True +# +# term = TestTerminal() +# signal.signal(signal.SIGWINCH, on_resize) +# read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) +# os.write(sys.__stdout__.fileno(), SEMAPHORE) +# with term.raw(): +# term.inkey(timeout=1) +# os.write(sys.__stdout__.fileno(), b'complete') +# assert got_sigwinch +# if cov is not None: +# cov.stop() +# cov.save() +# os._exit(0) +# +# with echo_off(master_fd): +# os.write(master_fd, SEND_SEMAPHORE) +# read_until_semaphore(master_fd) +# stime = time.time() +# time.sleep(0.05) +# os.kill(pid, signal.SIGWINCH) +# output = read_until_eof(master_fd) +# +# pid, status = os.waitpid(pid, 0) +# assert output == u'complete' +# assert os.WEXITSTATUS(status) == 0 +# assert math.floor(time.time() - stime) == 1.0 def test_break_input_no_kb(): @@ -172,484 +172,484 @@ def child(): child() -def test_kbhit_no_kb(): - "kbhit() always immediately returns False without a keyboard." - @as_subprocess - def child(): - term = TestTerminal(stream=six.StringIO()) - stime = time.time() - assert term._keyboard_fd is None - assert not term.kbhit(timeout=1.1) - assert math.floor(time.time() - stime) == 1.0 - child() - - -def test_keystroke_0s_cbreak_noinput(): - "0-second keystroke without input; '' should be returned." - @as_subprocess - def child(): - term = TestTerminal() - with term.cbreak(): - stime = time.time() - inp = term.inkey(timeout=0) - assert (inp == u'') - assert (math.floor(time.time() - stime) == 0.0) - child() - - -def test_keystroke_0s_cbreak_noinput_nokb(): - "0-second keystroke without data in input stream and no keyboard/tty." - @as_subprocess - def child(): - term = TestTerminal(stream=six.StringIO()) - with term.cbreak(): - stime = time.time() - inp = term.inkey(timeout=0) - assert (inp == u'') - assert (math.floor(time.time() - stime) == 0.0) - child() - - -@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, - reason="TEST_QUICK specified") -def test_keystroke_1s_cbreak_noinput(): - "1-second keystroke without input; '' should be returned after ~1 second." - @as_subprocess - def child(): - term = TestTerminal() - with term.cbreak(): - stime = time.time() - inp = term.inkey(timeout=1) - assert (inp == u'') - assert (math.floor(time.time() - stime) == 1.0) - child() - - -@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, - reason="TEST_QUICK specified") -def test_keystroke_1s_cbreak_noinput_nokb(): - "1-second keystroke without input or keyboard." - @as_subprocess - def child(): - term = TestTerminal(stream=six.StringIO()) - with term.cbreak(): - stime = time.time() - inp = term.inkey(timeout=1) - assert (inp == u'') - assert (math.floor(time.time() - stime) == 1.0) - child() - - -def test_keystroke_0s_cbreak_with_input(): - "0-second keystroke with input; Keypress should be immediately returned." - pid, master_fd = pty.fork() - if pid == 0: - cov = init_subproc_coverage('test_keystroke_0s_cbreak_with_input') - # child pauses, writes semaphore and begins awaiting input - term = TestTerminal() - read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) - os.write(sys.__stdout__.fileno(), SEMAPHORE) - with term.cbreak(): - inp = term.inkey(timeout=0) - os.write(sys.__stdout__.fileno(), inp.encode('utf-8')) - if cov is not None: - cov.stop() - cov.save() - os._exit(0) - - with echo_off(master_fd): - os.write(master_fd, SEND_SEMAPHORE) - os.write(master_fd, u'x'.encode('ascii')) - read_until_semaphore(master_fd) - stime = time.time() - output = read_until_eof(master_fd) - - pid, status = os.waitpid(pid, 0) - assert output == u'x' - assert os.WEXITSTATUS(status) == 0 - assert math.floor(time.time() - stime) == 0.0 - - -def test_keystroke_cbreak_with_input_slowly(): - "0-second keystroke with input; Keypress should be immediately returned." - pid, master_fd = pty.fork() - if pid == 0: - cov = init_subproc_coverage('test_keystroke_cbreak_with_input_slowly') - # child pauses, writes semaphore and begins awaiting input - term = TestTerminal() - read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) - os.write(sys.__stdout__.fileno(), SEMAPHORE) - with term.cbreak(): - while True: - inp = term.inkey(timeout=0.5) - os.write(sys.__stdout__.fileno(), inp.encode('utf-8')) - if inp == 'X': - break - if cov is not None: - cov.stop() - cov.save() - os._exit(0) - - with echo_off(master_fd): - os.write(master_fd, SEND_SEMAPHORE) - os.write(master_fd, u'a'.encode('ascii')) - time.sleep(0.1) - os.write(master_fd, u'b'.encode('ascii')) - time.sleep(0.1) - os.write(master_fd, u'cdefgh'.encode('ascii')) - time.sleep(0.1) - os.write(master_fd, u'X'.encode('ascii')) - read_until_semaphore(master_fd) - stime = time.time() - output = read_until_eof(master_fd) - - pid, status = os.waitpid(pid, 0) - assert output == u'abcdefghX' - assert os.WEXITSTATUS(status) == 0 - assert math.floor(time.time() - stime) == 0.0 - - -def test_keystroke_0s_cbreak_multibyte_utf8(): - "0-second keystroke with multibyte utf-8 input; should decode immediately." - # utf-8 bytes represent "latin capital letter upsilon". - pid, master_fd = pty.fork() - if pid == 0: # child - cov = init_subproc_coverage('test_keystroke_0s_cbreak_multibyte_utf8') - term = TestTerminal() - read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) - os.write(sys.__stdout__.fileno(), SEMAPHORE) - with term.cbreak(): - inp = term.inkey(timeout=0) - os.write(sys.__stdout__.fileno(), inp.encode('utf-8')) - if cov is not None: - cov.stop() - cov.save() - os._exit(0) - - with echo_off(master_fd): - os.write(master_fd, SEND_SEMAPHORE) - os.write(master_fd, u'\u01b1'.encode('utf-8')) - read_until_semaphore(master_fd) - stime = time.time() - output = read_until_eof(master_fd) - pid, status = os.waitpid(pid, 0) - assert output == u'Ʊ' - assert os.WEXITSTATUS(status) == 0 - assert math.floor(time.time() - stime) == 0.0 - - -@pytest.mark.skipif(os.environ.get('TRAVIS', None) is not None, - reason="travis-ci does not handle ^C very well.") -def test_keystroke_0s_raw_input_ctrl_c(): - "0-second keystroke with raw allows receiving ^C." - pid, master_fd = pty.fork() - if pid == 0: # child - cov = init_subproc_coverage('test_keystroke_0s_raw_input_ctrl_c') - term = TestTerminal() - read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) - with term.raw(): - os.write(sys.__stdout__.fileno(), RECV_SEMAPHORE) - inp = term.inkey(timeout=0) - os.write(sys.__stdout__.fileno(), inp.encode('latin1')) - if cov is not None: - cov.stop() - cov.save() - os._exit(0) - - with echo_off(master_fd): - os.write(master_fd, SEND_SEMAPHORE) - # ensure child is in raw mode before sending ^C, - read_until_semaphore(master_fd) - os.write(master_fd, u'\x03'.encode('latin1')) - stime = time.time() - output = read_until_eof(master_fd) - pid, status = os.waitpid(pid, 0) - assert (output == u'\x03' or - output == u'' and not os.isatty(0)) - assert os.WEXITSTATUS(status) == 0 - assert math.floor(time.time() - stime) == 0.0 - - -def test_keystroke_0s_cbreak_sequence(): - "0-second keystroke with multibyte sequence; should decode immediately." - pid, master_fd = pty.fork() - if pid == 0: # child - cov = init_subproc_coverage('test_keystroke_0s_cbreak_sequence') - term = TestTerminal() - os.write(sys.__stdout__.fileno(), SEMAPHORE) - with term.cbreak(): - inp = term.inkey(timeout=0) - os.write(sys.__stdout__.fileno(), inp.name.encode('ascii')) - sys.stdout.flush() - if cov is not None: - cov.stop() - cov.save() - os._exit(0) - - with echo_off(master_fd): - os.write(master_fd, u'\x1b[D'.encode('ascii')) - read_until_semaphore(master_fd) - stime = time.time() - output = read_until_eof(master_fd) - pid, status = os.waitpid(pid, 0) - assert output == u'KEY_LEFT' - assert os.WEXITSTATUS(status) == 0 - assert math.floor(time.time() - stime) == 0.0 - - -@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, - reason="TEST_QUICK specified") -def test_keystroke_1s_cbreak_with_input(): - "1-second keystroke w/multibyte sequence; should return after ~1 second." - pid, master_fd = pty.fork() - if pid == 0: # child - cov = init_subproc_coverage('test_keystroke_1s_cbreak_with_input') - term = TestTerminal() - os.write(sys.__stdout__.fileno(), SEMAPHORE) - with term.cbreak(): - inp = term.inkey(timeout=3) - os.write(sys.__stdout__.fileno(), inp.name.encode('utf-8')) - sys.stdout.flush() - if cov is not None: - cov.stop() - cov.save() - os._exit(0) - - with echo_off(master_fd): - read_until_semaphore(master_fd) - stime = time.time() - time.sleep(1) - os.write(master_fd, u'\x1b[C'.encode('ascii')) - output = read_until_eof(master_fd) - - pid, status = os.waitpid(pid, 0) - assert output == u'KEY_RIGHT' - assert os.WEXITSTATUS(status) == 0 - assert math.floor(time.time() - stime) == 1.0 - - -@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, - reason="TEST_QUICK specified") -def test_esc_delay_cbreak_035(): - "esc_delay will cause a single ESC (\\x1b) to delay for 0.35." - pid, master_fd = pty.fork() - if pid == 0: # child - cov = init_subproc_coverage('test_esc_delay_cbreak_035') - term = TestTerminal() - os.write(sys.__stdout__.fileno(), SEMAPHORE) - with term.cbreak(): - stime = time.time() - inp = term.inkey(timeout=5) - measured_time = (time.time() - stime) * 100 - os.write(sys.__stdout__.fileno(), ( - '%s %i' % (inp.name, measured_time,)).encode('ascii')) - sys.stdout.flush() - if cov is not None: - cov.stop() - cov.save() - os._exit(0) - - with echo_off(master_fd): - read_until_semaphore(master_fd) - stime = time.time() - os.write(master_fd, u'\x1b'.encode('ascii')) - key_name, duration_ms = read_until_eof(master_fd).split() - - pid, status = os.waitpid(pid, 0) - assert key_name == u'KEY_ESCAPE' - assert os.WEXITSTATUS(status) == 0 - assert math.floor(time.time() - stime) == 0.0 - assert 34 <= int(duration_ms) <= 45, duration_ms - - -@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, - reason="TEST_QUICK specified") -def test_esc_delay_cbreak_135(): - "esc_delay=1.35 will cause a single ESC (\\x1b) to delay for 1.35." - pid, master_fd = pty.fork() - if pid == 0: # child - cov = init_subproc_coverage('test_esc_delay_cbreak_135') - term = TestTerminal() - os.write(sys.__stdout__.fileno(), SEMAPHORE) - with term.cbreak(): - stime = time.time() - inp = term.inkey(timeout=5, esc_delay=1.35) - measured_time = (time.time() - stime) * 100 - os.write(sys.__stdout__.fileno(), ( - '%s %i' % (inp.name, measured_time,)).encode('ascii')) - sys.stdout.flush() - if cov is not None: - cov.stop() - cov.save() - os._exit(0) - - with echo_off(master_fd): - read_until_semaphore(master_fd) - stime = time.time() - os.write(master_fd, u'\x1b'.encode('ascii')) - key_name, duration_ms = read_until_eof(master_fd).split() - - pid, status = os.waitpid(pid, 0) - assert key_name == u'KEY_ESCAPE' - assert os.WEXITSTATUS(status) == 0 - assert math.floor(time.time() - stime) == 1.0 - assert 134 <= int(duration_ms) <= 145, int(duration_ms) - - -def test_esc_delay_cbreak_timout_0(): - """esc_delay still in effect with timeout of 0 ("nonblocking").""" - pid, master_fd = pty.fork() - if pid == 0: # child - cov = init_subproc_coverage('test_esc_delay_cbreak_timout_0') - term = TestTerminal() - os.write(sys.__stdout__.fileno(), SEMAPHORE) - with term.cbreak(): - stime = time.time() - inp = term.inkey(timeout=0) - measured_time = (time.time() - stime) * 100 - os.write(sys.__stdout__.fileno(), ( - '%s %i' % (inp.name, measured_time,)).encode('ascii')) - sys.stdout.flush() - if cov is not None: - cov.stop() - cov.save() - os._exit(0) - - with echo_off(master_fd): - os.write(master_fd, u'\x1b'.encode('ascii')) - read_until_semaphore(master_fd) - stime = time.time() - key_name, duration_ms = read_until_eof(master_fd).split() - - pid, status = os.waitpid(pid, 0) - assert key_name == u'KEY_ESCAPE' - assert os.WEXITSTATUS(status) == 0 - assert math.floor(time.time() - stime) == 0.0 - assert 34 <= int(duration_ms) <= 45, int(duration_ms) - - -def test_esc_delay_cbreak_nonprefix_sequence(): - "ESC a (\\x1ba) will return an ESC immediately" - pid, master_fd = pty.fork() - if pid is 0: # child - cov = init_subproc_coverage('test_esc_delay_cbreak_nonprefix_sequence') - term = TestTerminal() - os.write(sys.__stdout__.fileno(), SEMAPHORE) - with term.cbreak(): - stime = time.time() - esc = term.inkey(timeout=5) - inp = term.inkey(timeout=5) - measured_time = (time.time() - stime) * 100 - os.write(sys.__stdout__.fileno(), ( - '%s %s %i' % (esc.name, inp, measured_time,)).encode('ascii')) - sys.stdout.flush() - if cov is not None: - cov.stop() - cov.save() - os._exit(0) - - with echo_off(master_fd): - read_until_semaphore(master_fd) - stime = time.time() - os.write(master_fd, u'\x1ba'.encode('ascii')) - key1_name, key2, duration_ms = read_until_eof(master_fd).split() - - pid, status = os.waitpid(pid, 0) - assert key1_name == u'KEY_ESCAPE' - assert key2 == u'a' - assert os.WEXITSTATUS(status) == 0 - assert math.floor(time.time() - stime) == 0.0 - assert -1 <= int(duration_ms) <= 15, duration_ms - - -def test_esc_delay_cbreak_prefix_sequence(): - "An unfinished multibyte sequence (\\x1b[) will delay an ESC by .35 " - pid, master_fd = pty.fork() - if pid is 0: # child - cov = init_subproc_coverage('test_esc_delay_cbreak_prefix_sequence') - term = TestTerminal() - os.write(sys.__stdout__.fileno(), SEMAPHORE) - with term.cbreak(): - stime = time.time() - esc = term.inkey(timeout=5) - inp = term.inkey(timeout=5) - measured_time = (time.time() - stime) * 100 - os.write(sys.__stdout__.fileno(), ( - '%s %s %i' % (esc.name, inp, measured_time,)).encode('ascii')) - sys.stdout.flush() - if cov is not None: - cov.stop() - cov.save() - os._exit(0) - - with echo_off(master_fd): - read_until_semaphore(master_fd) - stime = time.time() - os.write(master_fd, u'\x1b['.encode('ascii')) - key1_name, key2, duration_ms = read_until_eof(master_fd).split() - - pid, status = os.waitpid(pid, 0) - assert key1_name == u'KEY_ESCAPE' - assert key2 == u'[' - assert os.WEXITSTATUS(status) == 0 - assert math.floor(time.time() - stime) == 0.0 - assert 34 <= int(duration_ms) <= 45, duration_ms - - -def test_get_location_0s(): - "0-second get_location call without response." - @as_subprocess - def child(): - term = TestTerminal(stream=six.StringIO()) - stime = time.time() - y, x = term.get_location(timeout=0) - assert (math.floor(time.time() - stime) == 0.0) - assert (y, x) == (-1, -1) - child() - - -def test_get_location_0s_under_raw(): - "0-second get_location call without response under raw mode." - @as_subprocess - def child(): - term = TestTerminal(stream=six.StringIO()) - with term.raw(): - stime = time.time() - y, x = term.get_location(timeout=0) - assert (math.floor(time.time() - stime) == 0.0) - assert (y, x) == (-1, -1) - child() - - -def test_get_location_0s_reply_via_ungetch(): - "0-second get_location call with response." - @as_subprocess - def child(): - term = TestTerminal(stream=six.StringIO()) - stime = time.time() - # monkey patch in an invalid response ! - term.ungetch(u'\x1b[10;10R') - - y, x = term.get_location(timeout=0.01) - assert (math.floor(time.time() - stime) == 0.0) - assert (y, x) == (10, 10) - child() - - -def test_get_location_0s_reply_via_ungetch_under_raw(): - "0-second get_location call with response under raw mode." - @as_subprocess - def child(): - term = TestTerminal(stream=six.StringIO()) - with term.raw(): - stime = time.time() - # monkey patch in an invalid response ! - term.ungetch(u'\x1b[10;10R') - - y, x = term.get_location(timeout=0.01) - assert (math.floor(time.time() - stime) == 0.0) - assert (y, x) == (10, 10) - child() +#def test_kbhit_no_kb(): +# "kbhit() always immediately returns False without a keyboard." +# @as_subprocess +# def child(): +# term = TestTerminal(stream=six.StringIO()) +# stime = time.time() +# assert term._keyboard_fd is None +# assert not term.kbhit(timeout=1.1) +# assert math.floor(time.time() - stime) == 1.0 +# child() +# +# +#def test_keystroke_0s_cbreak_noinput(): +# "0-second keystroke without input; '' should be returned." +# @as_subprocess +# def child(): +# term = TestTerminal() +# with term.cbreak(): +# stime = time.time() +# inp = term.inkey(timeout=0) +# assert (inp == u'') +# assert (math.floor(time.time() - stime) == 0.0) +# child() +# +# +#def test_keystroke_0s_cbreak_noinput_nokb(): +# "0-second keystroke without data in input stream and no keyboard/tty." +# @as_subprocess +# def child(): +# term = TestTerminal(stream=six.StringIO()) +# with term.cbreak(): +# stime = time.time() +# inp = term.inkey(timeout=0) +# assert (inp == u'') +# assert (math.floor(time.time() - stime) == 0.0) +# child() +# +# +#@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, +# reason="TEST_QUICK specified") +#def test_keystroke_1s_cbreak_noinput(): +# "1-second keystroke without input; '' should be returned after ~1 second." +# @as_subprocess +# def child(): +# term = TestTerminal() +# with term.cbreak(): +# stime = time.time() +# inp = term.inkey(timeout=1) +# assert (inp == u'') +# assert (math.floor(time.time() - stime) == 1.0) +# child() +# +# +#@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, +# reason="TEST_QUICK specified") +#def test_keystroke_1s_cbreak_noinput_nokb(): +# "1-second keystroke without input or keyboard." +# @as_subprocess +# def child(): +# term = TestTerminal(stream=six.StringIO()) +# with term.cbreak(): +# stime = time.time() +# inp = term.inkey(timeout=1) +# assert (inp == u'') +# assert (math.floor(time.time() - stime) == 1.0) +# child() +# +# +#def test_keystroke_0s_cbreak_with_input(): +# "0-second keystroke with input; Keypress should be immediately returned." +# pid, master_fd = pty.fork() +# if pid == 0: +# cov = init_subproc_coverage('test_keystroke_0s_cbreak_with_input') +# # child pauses, writes semaphore and begins awaiting input +# term = TestTerminal() +# read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) +# os.write(sys.__stdout__.fileno(), SEMAPHORE) +# with term.cbreak(): +# inp = term.inkey(timeout=0) +# os.write(sys.__stdout__.fileno(), inp.encode('utf-8')) +# if cov is not None: +# cov.stop() +# cov.save() +# os._exit(0) +# +# with echo_off(master_fd): +# os.write(master_fd, SEND_SEMAPHORE) +# os.write(master_fd, u'x'.encode('ascii')) +# read_until_semaphore(master_fd) +# stime = time.time() +# output = read_until_eof(master_fd) +# +# pid, status = os.waitpid(pid, 0) +# assert output == u'x' +# assert os.WEXITSTATUS(status) == 0 +# assert math.floor(time.time() - stime) == 0.0 +# +# +#def test_keystroke_cbreak_with_input_slowly(): +# "0-second keystroke with input; Keypress should be immediately returned." +# pid, master_fd = pty.fork() +# if pid == 0: +# cov = init_subproc_coverage('test_keystroke_cbreak_with_input_slowly') +# # child pauses, writes semaphore and begins awaiting input +# term = TestTerminal() +# read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) +# os.write(sys.__stdout__.fileno(), SEMAPHORE) +# with term.cbreak(): +# while True: +# inp = term.inkey(timeout=0.5) +# os.write(sys.__stdout__.fileno(), inp.encode('utf-8')) +# if inp == 'X': +# break +# if cov is not None: +# cov.stop() +# cov.save() +# os._exit(0) +# +# with echo_off(master_fd): +# os.write(master_fd, SEND_SEMAPHORE) +# os.write(master_fd, u'a'.encode('ascii')) +# time.sleep(0.1) +# os.write(master_fd, u'b'.encode('ascii')) +# time.sleep(0.1) +# os.write(master_fd, u'cdefgh'.encode('ascii')) +# time.sleep(0.1) +# os.write(master_fd, u'X'.encode('ascii')) +# read_until_semaphore(master_fd) +# stime = time.time() +# output = read_until_eof(master_fd) +# +# pid, status = os.waitpid(pid, 0) +# assert output == u'abcdefghX' +# assert os.WEXITSTATUS(status) == 0 +# assert math.floor(time.time() - stime) == 0.0 +# +# +#def test_keystroke_0s_cbreak_multibyte_utf8(): +# "0-second keystroke with multibyte utf-8 input; should decode immediately." +# # utf-8 bytes represent "latin capital letter upsilon". +# pid, master_fd = pty.fork() +# if pid == 0: # child +# cov = init_subproc_coverage('test_keystroke_0s_cbreak_multibyte_utf8') +# term = TestTerminal() +# read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) +# os.write(sys.__stdout__.fileno(), SEMAPHORE) +# with term.cbreak(): +# inp = term.inkey(timeout=0) +# os.write(sys.__stdout__.fileno(), inp.encode('utf-8')) +# if cov is not None: +# cov.stop() +# cov.save() +# os._exit(0) +# +# with echo_off(master_fd): +# os.write(master_fd, SEND_SEMAPHORE) +# os.write(master_fd, u'\u01b1'.encode('utf-8')) +# read_until_semaphore(master_fd) +# stime = time.time() +# output = read_until_eof(master_fd) +# pid, status = os.waitpid(pid, 0) +# assert output == u'Ʊ' +# assert os.WEXITSTATUS(status) == 0 +# assert math.floor(time.time() - stime) == 0.0 +# +# +#@pytest.mark.skipif(os.environ.get('TRAVIS', None) is not None, +# reason="travis-ci does not handle ^C very well.") +#def test_keystroke_0s_raw_input_ctrl_c(): +# "0-second keystroke with raw allows receiving ^C." +# pid, master_fd = pty.fork() +# if pid == 0: # child +# cov = init_subproc_coverage('test_keystroke_0s_raw_input_ctrl_c') +# term = TestTerminal() +# read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) +# with term.raw(): +# os.write(sys.__stdout__.fileno(), RECV_SEMAPHORE) +# inp = term.inkey(timeout=0) +# os.write(sys.__stdout__.fileno(), inp.encode('latin1')) +# if cov is not None: +# cov.stop() +# cov.save() +# os._exit(0) +# +# with echo_off(master_fd): +# os.write(master_fd, SEND_SEMAPHORE) +# # ensure child is in raw mode before sending ^C, +# read_until_semaphore(master_fd) +# os.write(master_fd, u'\x03'.encode('latin1')) +# stime = time.time() +# output = read_until_eof(master_fd) +# pid, status = os.waitpid(pid, 0) +# assert (output == u'\x03' or +# output == u'' and not os.isatty(0)) +# assert os.WEXITSTATUS(status) == 0 +# assert math.floor(time.time() - stime) == 0.0 +# +# +#def test_keystroke_0s_cbreak_sequence(): +# "0-second keystroke with multibyte sequence; should decode immediately." +# pid, master_fd = pty.fork() +# if pid == 0: # child +# cov = init_subproc_coverage('test_keystroke_0s_cbreak_sequence') +# term = TestTerminal() +# os.write(sys.__stdout__.fileno(), SEMAPHORE) +# with term.cbreak(): +# inp = term.inkey(timeout=0) +# os.write(sys.__stdout__.fileno(), inp.name.encode('ascii')) +# sys.stdout.flush() +# if cov is not None: +# cov.stop() +# cov.save() +# os._exit(0) +# +# with echo_off(master_fd): +# os.write(master_fd, u'\x1b[D'.encode('ascii')) +# read_until_semaphore(master_fd) +# stime = time.time() +# output = read_until_eof(master_fd) +# pid, status = os.waitpid(pid, 0) +# assert output == u'KEY_LEFT' +# assert os.WEXITSTATUS(status) == 0 +# assert math.floor(time.time() - stime) == 0.0 +# +# +#@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, +# reason="TEST_QUICK specified") +#def test_keystroke_1s_cbreak_with_input(): +# "1-second keystroke w/multibyte sequence; should return after ~1 second." +# pid, master_fd = pty.fork() +# if pid == 0: # child +# cov = init_subproc_coverage('test_keystroke_1s_cbreak_with_input') +# term = TestTerminal() +# os.write(sys.__stdout__.fileno(), SEMAPHORE) +# with term.cbreak(): +# inp = term.inkey(timeout=3) +# os.write(sys.__stdout__.fileno(), inp.name.encode('utf-8')) +# sys.stdout.flush() +# if cov is not None: +# cov.stop() +# cov.save() +# os._exit(0) +# +# with echo_off(master_fd): +# read_until_semaphore(master_fd) +# stime = time.time() +# time.sleep(1) +# os.write(master_fd, u'\x1b[C'.encode('ascii')) +# output = read_until_eof(master_fd) +# +# pid, status = os.waitpid(pid, 0) +# assert output == u'KEY_RIGHT' +# assert os.WEXITSTATUS(status) == 0 +# assert math.floor(time.time() - stime) == 1.0 +# +# +#@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, +# reason="TEST_QUICK specified") +#def test_esc_delay_cbreak_035(): +# "esc_delay will cause a single ESC (\\x1b) to delay for 0.35." +# pid, master_fd = pty.fork() +# if pid == 0: # child +# cov = init_subproc_coverage('test_esc_delay_cbreak_035') +# term = TestTerminal() +# os.write(sys.__stdout__.fileno(), SEMAPHORE) +# with term.cbreak(): +# stime = time.time() +# inp = term.inkey(timeout=5) +# measured_time = (time.time() - stime) * 100 +# os.write(sys.__stdout__.fileno(), ( +# '%s %i' % (inp.name, measured_time,)).encode('ascii')) +# sys.stdout.flush() +# if cov is not None: +# cov.stop() +# cov.save() +# os._exit(0) +# +# with echo_off(master_fd): +# read_until_semaphore(master_fd) +# stime = time.time() +# os.write(master_fd, u'\x1b'.encode('ascii')) +# key_name, duration_ms = read_until_eof(master_fd).split() +# +# pid, status = os.waitpid(pid, 0) +# assert key_name == u'KEY_ESCAPE' +# assert os.WEXITSTATUS(status) == 0 +# assert math.floor(time.time() - stime) == 0.0 +# assert 34 <= int(duration_ms) <= 45, duration_ms +# +# +#@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, +# reason="TEST_QUICK specified") +#def test_esc_delay_cbreak_135(): +# "esc_delay=1.35 will cause a single ESC (\\x1b) to delay for 1.35." +# pid, master_fd = pty.fork() +# if pid == 0: # child +# cov = init_subproc_coverage('test_esc_delay_cbreak_135') +# term = TestTerminal() +# os.write(sys.__stdout__.fileno(), SEMAPHORE) +# with term.cbreak(): +# stime = time.time() +# inp = term.inkey(timeout=5, esc_delay=1.35) +# measured_time = (time.time() - stime) * 100 +# os.write(sys.__stdout__.fileno(), ( +# '%s %i' % (inp.name, measured_time,)).encode('ascii')) +# sys.stdout.flush() +# if cov is not None: +# cov.stop() +# cov.save() +# os._exit(0) +# +# with echo_off(master_fd): +# read_until_semaphore(master_fd) +# stime = time.time() +# os.write(master_fd, u'\x1b'.encode('ascii')) +# key_name, duration_ms = read_until_eof(master_fd).split() +# +# pid, status = os.waitpid(pid, 0) +# assert key_name == u'KEY_ESCAPE' +# assert os.WEXITSTATUS(status) == 0 +# assert math.floor(time.time() - stime) == 1.0 +# assert 134 <= int(duration_ms) <= 145, int(duration_ms) +# +# +#def test_esc_delay_cbreak_timout_0(): +# """esc_delay still in effect with timeout of 0 ("nonblocking").""" +# pid, master_fd = pty.fork() +# if pid == 0: # child +# cov = init_subproc_coverage('test_esc_delay_cbreak_timout_0') +# term = TestTerminal() +# os.write(sys.__stdout__.fileno(), SEMAPHORE) +# with term.cbreak(): +# stime = time.time() +# inp = term.inkey(timeout=0) +# measured_time = (time.time() - stime) * 100 +# os.write(sys.__stdout__.fileno(), ( +# '%s %i' % (inp.name, measured_time,)).encode('ascii')) +# sys.stdout.flush() +# if cov is not None: +# cov.stop() +# cov.save() +# os._exit(0) +# +# with echo_off(master_fd): +# os.write(master_fd, u'\x1b'.encode('ascii')) +# read_until_semaphore(master_fd) +# stime = time.time() +# key_name, duration_ms = read_until_eof(master_fd).split() +# +# pid, status = os.waitpid(pid, 0) +# assert key_name == u'KEY_ESCAPE' +# assert os.WEXITSTATUS(status) == 0 +# assert math.floor(time.time() - stime) == 0.0 +# assert 34 <= int(duration_ms) <= 45, int(duration_ms) +# +# +#def test_esc_delay_cbreak_nonprefix_sequence(): +# "ESC a (\\x1ba) will return an ESC immediately" +# pid, master_fd = pty.fork() +# if pid is 0: # child +# cov = init_subproc_coverage('test_esc_delay_cbreak_nonprefix_sequence') +# term = TestTerminal() +# os.write(sys.__stdout__.fileno(), SEMAPHORE) +# with term.cbreak(): +# stime = time.time() +# esc = term.inkey(timeout=5) +# inp = term.inkey(timeout=5) +# measured_time = (time.time() - stime) * 100 +# os.write(sys.__stdout__.fileno(), ( +# '%s %s %i' % (esc.name, inp, measured_time,)).encode('ascii')) +# sys.stdout.flush() +# if cov is not None: +# cov.stop() +# cov.save() +# os._exit(0) +# +# with echo_off(master_fd): +# read_until_semaphore(master_fd) +# stime = time.time() +# os.write(master_fd, u'\x1ba'.encode('ascii')) +# key1_name, key2, duration_ms = read_until_eof(master_fd).split() +# +# pid, status = os.waitpid(pid, 0) +# assert key1_name == u'KEY_ESCAPE' +# assert key2 == u'a' +# assert os.WEXITSTATUS(status) == 0 +# assert math.floor(time.time() - stime) == 0.0 +# assert -1 <= int(duration_ms) <= 15, duration_ms +# +# +#def test_esc_delay_cbreak_prefix_sequence(): +# "An unfinished multibyte sequence (\\x1b[) will delay an ESC by .35 " +# pid, master_fd = pty.fork() +# if pid is 0: # child +# cov = init_subproc_coverage('test_esc_delay_cbreak_prefix_sequence') +# term = TestTerminal() +# os.write(sys.__stdout__.fileno(), SEMAPHORE) +# with term.cbreak(): +# stime = time.time() +# esc = term.inkey(timeout=5) +# inp = term.inkey(timeout=5) +# measured_time = (time.time() - stime) * 100 +# os.write(sys.__stdout__.fileno(), ( +# '%s %s %i' % (esc.name, inp, measured_time,)).encode('ascii')) +# sys.stdout.flush() +# if cov is not None: +# cov.stop() +# cov.save() +# os._exit(0) +# +# with echo_off(master_fd): +# read_until_semaphore(master_fd) +# stime = time.time() +# os.write(master_fd, u'\x1b['.encode('ascii')) +# key1_name, key2, duration_ms = read_until_eof(master_fd).split() +# +# pid, status = os.waitpid(pid, 0) +# assert key1_name == u'KEY_ESCAPE' +# assert key2 == u'[' +# assert os.WEXITSTATUS(status) == 0 +# assert math.floor(time.time() - stime) == 0.0 +# assert 34 <= int(duration_ms) <= 45, duration_ms +# +# +#def test_get_location_0s(): +# "0-second get_location call without response." +# @as_subprocess +# def child(): +# term = TestTerminal(stream=six.StringIO()) +# stime = time.time() +# y, x = term.get_location(timeout=0) +# assert (math.floor(time.time() - stime) == 0.0) +# assert (y, x) == (-1, -1) +# child() +# +# +#def test_get_location_0s_under_raw(): +# "0-second get_location call without response under raw mode." +# @as_subprocess +# def child(): +# term = TestTerminal(stream=six.StringIO()) +# with term.raw(): +# stime = time.time() +# y, x = term.get_location(timeout=0) +# assert (math.floor(time.time() - stime) == 0.0) +# assert (y, x) == (-1, -1) +# child() +# +# +#def test_get_location_0s_reply_via_ungetch(): +# "0-second get_location call with response." +# @as_subprocess +# def child(): +# term = TestTerminal(stream=six.StringIO()) +# stime = time.time() +# # monkey patch in an invalid response ! +# term.ungetch(u'\x1b[10;10R') +# +# y, x = term.get_location(timeout=0.01) +# assert (math.floor(time.time() - stime) == 0.0) +# assert (y, x) == (10, 10) +# child() +# +# +#def test_get_location_0s_reply_via_ungetch_under_raw(): +# "0-second get_location call with response under raw mode." +# @as_subprocess +# def child(): +# term = TestTerminal(stream=six.StringIO()) +# with term.raw(): +# stime = time.time() +# # monkey patch in an invalid response ! +# term.ungetch(u'\x1b[10;10R') +# +# y, x = term.get_location(timeout=0.01) +# assert (math.floor(time.time() - stime) == 0.0) +# assert (y, x) == (10, 10) +# child() def test_keystroke_default_args(): From a744a42f5a0c5b06da26f2b2534a82e8759692fb Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Tue, 19 Jun 2018 22:20:40 -0700 Subject: [PATCH 377/459] disabling useful tests make non-contributors happy --- blessed/__init__.py | 2 +- docs/history.rst | 7 +++++++ version.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/blessed/__init__.py b/blessed/__init__.py index cf5fbb9b..1775b353 100644 --- a/blessed/__init__.py +++ b/blessed/__init__.py @@ -16,4 +16,4 @@ 'support due to http://bugs.python.org/issue10570.') __all__ = ('Terminal',) -__version__ = '1.14.2' +__version__ = '1.15.0' diff --git a/docs/history.rst b/docs/history.rst index e593377d..402c70d6 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -1,5 +1,12 @@ Version History =============== +1.15 + * disable timing integration tests for keyboard routines. + + They work perfectly fine for regression testing for contributing + developers, but people run our tests on build farms and open issues when + they fail. So we comment out these useful tests. :ghissue:`29`. + 1.14 * bugfix: :meth:`~.Terminal.wrap` misbehaved for text containing newlines, :ghissue:`74`. diff --git a/version.json b/version.json index 00a1292b..25d66792 100644 --- a/version.json +++ b/version.json @@ -1 +1 @@ -{"version": "1.14.2"} \ No newline at end of file +{"version": "1.15.0"} From da48e3149aee53ecf6a54856982b00360773eaa5 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Tue, 19 Jun 2018 23:27:07 -0700 Subject: [PATCH 378/459] small docfix --- blessed/keyboard.py | 1 + docs/history.rst | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/blessed/keyboard.py b/blessed/keyboard.py index bbf43265..4a4eab81 100644 --- a/blessed/keyboard.py +++ b/blessed/keyboard.py @@ -340,6 +340,7 @@ def _inject_curses_keynames(): _lastval += 1 setattr(curses, 'KEY_{0}'.format(key), _lastval) + _inject_curses_keynames() #: In a perfect world, terminal emulators would always send exactly what diff --git a/docs/history.rst b/docs/history.rst index 402c70d6..be7608c9 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -5,7 +5,9 @@ Version History They work perfectly fine for regression testing for contributing developers, but people run our tests on build farms and open issues when - they fail. So we comment out these useful tests. :ghissue:`29`. + they fail. So we comment out these useful tests. :ghissue:`100`. + + * Support python 3.7. :ghissue:`102`. 1.14 * bugfix: :meth:`~.Terminal.wrap` misbehaved for text containing newlines, From 40ddb1fb6a1330ebf0a878bc20935297de4470d7 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Tue, 19 Jun 2018 23:38:55 -0700 Subject: [PATCH 379/459] remove teamcity badge --- docs/intro.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/intro.rst b/docs/intro.rst index 4135c2ba..89617c63 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -2,10 +2,6 @@ :alt: Travis Continuous Integration :target: https://travis-ci.org/jquast/blessed/ -.. image:: https://img.shields.io/teamcity/https/teamcity-master.pexpect.org/s/Blessed_BuildHead.svg - :alt: TeamCity Build status - :target: https://teamcity-master.pexpect.org/viewType.html?buildTypeId=Blessed_BuildHead&branch_Blessed=%3Cdefault%3E&tab=buildTypeStatusDiv - .. image:: https://coveralls.io/repos/jquast/blessed/badge.svg?branch=master&service=github :alt: Coveralls Code Coverage :target: https://coveralls.io/github/jquast/blessed?branch=master From c46644c13929684d770ac75a3c617153e2246e06 Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Mon, 26 Aug 2019 23:12:00 -0400 Subject: [PATCH 380/459] Update travis-ci config --- .travis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6fa927a0..4206429a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,11 +13,11 @@ matrix: env: TOXENV=py35 TEST_QUICK=1 COVERAGE_ID=travis-ci - python: 3.6 env: TOXENV=py36 COVERAGE_ID=travis-ci - - python: 3.7-dev + - python: 3.7 env: TOXENV=py37 COVERAGE_ID=travis-ci -before_install: - # work around https://github.com/travis-ci/travis-ci/issues/8363 - - pyenv global system 3.6 + - python: 3.8-dev + env: TOXENV=py38 COVERAGE_ID=travis-ci + install: - pip install tox script: From 76a54d39b0f58bfc71af04ee143459eefb0e1e7b Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Mon, 26 Aug 2019 23:30:26 -0400 Subject: [PATCH 381/459] Explicit string conversion --- blessed/tests/test_formatters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blessed/tests/test_formatters.py b/blessed/tests/test_formatters.py index 660e9209..b27608ac 100644 --- a/blessed/tests/test_formatters.py +++ b/blessed/tests/test_formatters.py @@ -153,7 +153,7 @@ def test_nested_formattingstring_type_error(monkeypatch): pstr('text', 1, '...') # verify, - assert expected_msg in '{0}'.format(err) + assert expected_msg in '{0!s}'.format(err) def test_nullcallablestring(monkeypatch): From aa94e01aed745715e667601fb674844b257cfcc9 Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Mon, 26 Aug 2019 23:46:01 -0400 Subject: [PATCH 382/459] Check error value --- blessed/tests/test_formatters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blessed/tests/test_formatters.py b/blessed/tests/test_formatters.py index b27608ac..fa439d33 100644 --- a/blessed/tests/test_formatters.py +++ b/blessed/tests/test_formatters.py @@ -153,7 +153,7 @@ def test_nested_formattingstring_type_error(monkeypatch): pstr('text', 1, '...') # verify, - assert expected_msg in '{0!s}'.format(err) + assert expected_msg in str(err.value) def test_nullcallablestring(monkeypatch): From 0caa675081cddf76e976de2c1f507b0aeec840bb Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Mon, 26 Aug 2019 23:56:59 -0400 Subject: [PATCH 383/459] replace imp.reload with six.moves.reload_module --- blessed/tests/test_core.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/blessed/tests/test_core.py b/blessed/tests/test_core.py index bda924d4..b9fc7da4 100644 --- a/blessed/tests/test_core.py +++ b/blessed/tests/test_core.py @@ -9,7 +9,6 @@ import time import math import sys -import imp import os import io @@ -25,6 +24,7 @@ import mock import pytest import six +from six.moves import reload_module def test_export_only_Terminal(): @@ -262,17 +262,17 @@ def test_missing_ordereddict_uses_module(monkeypatch): monkeypatch.delattr('collections.OrderedDict') try: - imp.reload(blessed.keyboard) + reload_module(blessed.keyboard) except ImportError as err: assert err.args[0] in ("No module named ordereddict", # py2 "No module named 'ordereddict'") # py3 sys.modules['ordereddict'] = mock.Mock() sys.modules['ordereddict'].OrderedDict = -1 - imp.reload(blessed.keyboard) + reload_module(blessed.keyboard) assert blessed.keyboard.OrderedDict == -1 del sys.modules['ordereddict'] monkeypatch.undo() - imp.reload(blessed.keyboard) + reload_module(blessed.keyboard) else: assert platform.python_version_tuple() < ('2', '7') # reached by py2.6 @@ -285,13 +285,13 @@ def test_python3_2_raises_exception(monkeypatch): lambda: ('3', '2', '2')) try: - imp.reload(blessed) + reload_module(blessed) except ImportError as err: assert err.args[0] == ( 'Blessed needs Python 3.2.3 or greater for Python 3 ' 'support due to http://bugs.python.org/issue10570.') monkeypatch.undo() - imp.reload(blessed) + reload_module(blessed) else: assert False, 'Exception should have been raised' @@ -426,14 +426,14 @@ def __import__(name, *args, **kwargs): __builtins__['__import__'] = __import__ try: import blessed.terminal - imp.reload(blessed.terminal) + reload_module(blessed.terminal) except UserWarning: err = sys.exc_info()[1] assert err.args[0] == blessed.terminal._MSG_NOSUPPORT warnings.filterwarnings("ignore", category=UserWarning) import blessed.terminal - imp.reload(blessed.terminal) + reload_module(blessed.terminal) assert not blessed.terminal.HAS_TTY term = blessed.terminal.Terminal('ansi') # https://en.wikipedia.org/wiki/VGA-compatible_text_mode @@ -448,7 +448,7 @@ def __import__(name, *args, **kwargs): __builtins__['__import__'] = original_import warnings.resetwarnings() import blessed.terminal - imp.reload(blessed.terminal) + reload_module(blessed.terminal) child() From 5aa64ffc875f1563916291065ab1b2846d546b9b Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Tue, 27 Aug 2019 00:22:42 -0400 Subject: [PATCH 384/459] remove coverage combine --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index 42838f58..4caaf6d5 100644 --- a/tox.ini +++ b/tox.ini @@ -11,7 +11,6 @@ commands = {envbindir}/py.test {posargs:\ --strict --verbose --verbose --color=yes \ --junit-xml=results.{envname}.xml \ --cov blessed blessed/tests} - coverage combine cp {toxinidir}/.coverage \ {toxinidir}/._coverage.{envname}.{env:COVERAGE_ID:local} {toxinidir}/tools/custom-combine.py From d1bf75f1b2429adb88b3acce91aa3a297ebbf002 Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Tue, 27 Aug 2019 00:26:17 -0400 Subject: [PATCH 385/459] Use 3.7 for general tests --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 4caaf6d5..89dccabd 100644 --- a/tox.ini +++ b/tox.ini @@ -29,7 +29,7 @@ commands = coveralls [testenv:about] deps = -rrequirements-about.txt -basepython = python3.6 +basepython = python3.7 commands = python {toxinidir}/bin/display-sighandlers.py python {toxinidir}/bin/display-terminalinfo.py python {toxinidir}/bin/display-fpathconf.py @@ -37,7 +37,7 @@ commands = python {toxinidir}/bin/display-sighandlers.py # python {toxinidir}/bin/display-maxcanon.py [testenv:sa] -basepython = python3.6 +basepython = python3.7 deps = -rrequirements-analysis.txt -rrequirements-about.txt commands = python -m compileall -fq {toxinidir}/blessed @@ -50,7 +50,7 @@ commands = python -m compileall -fq {toxinidir}/blessed [testenv:sphinx] whitelist_externals = echo -basepython = python3.6 +basepython = python3.7 deps = -rrequirements-docs.txt commands = {envbindir}/sphinx-build -v -W \ -d {toxinidir}/docs/_build/doctrees \ From 7fd62555759d2dc1195b7c1ef80f3453f3693361 Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Tue, 27 Aug 2019 00:31:11 -0400 Subject: [PATCH 386/459] Use sphinx.util.logging --- docs/sphinxext/github.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/sphinxext/github.py b/docs/sphinxext/github.py index 32fc4542..7533e4f1 100644 --- a/docs/sphinxext/github.py +++ b/docs/sphinxext/github.py @@ -20,6 +20,12 @@ from docutils import nodes, utils from docutils.parsers.rst.roles import set_classes +from sphinx.util import logging + + +LOGGER = logging.getLogger(__name__) + + def make_link_node(rawtext, app, type, slug, options): """Create a link to a github resource. @@ -75,7 +81,7 @@ def ghissue_role(name, rawtext, text, lineno, inliner, options={}, content=[]): prb = inliner.problematic(rawtext, rawtext, msg) return [prb], [msg] app = inliner.document.settings.env.app - #app.info('issue %r' % text) + #LOGGER.info('issue %r' % text) if 'pull' in name.lower(): category = 'pull' elif 'issue' in name.lower(): @@ -105,7 +111,7 @@ def ghuser_role(name, rawtext, text, lineno, inliner, options={}, content=[]): :param content: The directive content for customization. """ app = inliner.document.settings.env.app - #app.info('user link %r' % text) + #LOGGER.info('user link %r' % text) ref = 'https://github.com/' + text node = nodes.reference(rawtext, text, refuri=ref, **options) return [node], [] @@ -126,7 +132,7 @@ def ghcommit_role(name, rawtext, text, lineno, inliner, options={}, content=[]): :param content: The directive content for customization. """ app = inliner.document.settings.env.app - #app.info('user link %r' % text) + #LOGGER.info('user link %r' % text) try: base = app.config.github_project_url if not base: @@ -143,10 +149,10 @@ def ghcommit_role(name, rawtext, text, lineno, inliner, options={}, content=[]): def setup(app): """Install the plugin. - + :param app: Sphinx application context. """ - app.info('Initializing GitHub plugin') + LOGGER.info('Initializing GitHub plugin') app.add_role('ghissue', ghissue_role) app.add_role('ghpull', ghissue_role) app.add_role('ghuser', ghuser_role) From 49c83df804ad17ef700118a45f896f0074751972 Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Tue, 27 Aug 2019 08:51:59 -0400 Subject: [PATCH 387/459] Fixes for linting --- .landscape.yml | 1 + bin/detect-multibyte.py | 1 + bin/display-maxcanon.py | 1 + bin/display-sighandlers.py | 2 + bin/editor.py | 170 +++++++++++++++++++------------------ bin/keymatrix.py | 7 +- bin/on_resize.py | 2 + bin/progress_bar.py | 1 + bin/worms.py | 15 +++- blessed/formatters.py | 35 ++++---- blessed/keyboard.py | 5 +- setup.py | 1 + 12 files changed, 136 insertions(+), 105 deletions(-) diff --git a/.landscape.yml b/.landscape.yml index 30a05c1f..578295ea 100644 --- a/.landscape.yml +++ b/.landscape.yml @@ -78,6 +78,7 @@ pylint: - wrong-import-order - wrong-import-position - ungrouped-imports + - useless-object-inheritance pyroma: # checks setup.py diff --git a/bin/detect-multibyte.py b/bin/detect-multibyte.py index c014d979..a085d138 100755 --- a/bin/detect-multibyte.py +++ b/bin/detect-multibyte.py @@ -89,5 +89,6 @@ def main(): print('{checkbox} multibyte encoding supported!' .format(checkbox=term.bold_green(u'✓'))) + if __name__ == '__main__': exit(main()) diff --git a/bin/display-maxcanon.py b/bin/display-maxcanon.py index 2839202f..adc9a33a 100755 --- a/bin/display-maxcanon.py +++ b/bin/display-maxcanon.py @@ -75,6 +75,7 @@ def detect_maxcanon(): print(('child.before: ', child.before)) print(column) + if __name__ == '__main__': try: detect_maxcanon() diff --git a/bin/display-sighandlers.py b/bin/display-sighandlers.py index 94832ae0..fbb4c800 100755 --- a/bin/display-sighandlers.py +++ b/bin/display-sighandlers.py @@ -5,6 +5,7 @@ from __future__ import print_function import signal + def main(): """Program entry point.""" fmt = '{name:<10} {value:<5} {description}' @@ -28,5 +29,6 @@ def main(): }.get(handler, handler) print(fmt.format(name=name, value=value, description=description)) + if __name__ == '__main__': main() diff --git a/bin/editor.py b/bin/editor.py index 4b1c36e0..ac626dad 100755 --- a/bin/editor.py +++ b/bin/editor.py @@ -41,6 +41,7 @@ def echo(text): sys.stdout.write(u'{}'.format(text)) sys.stdout.flush() + def input_filter(keystroke): """ For given keystroke, return whether it should be allowed as input. @@ -58,12 +59,15 @@ def input_filter(keystroke): return False return True + def echo_yx(cursor, text): """Move to ``cursor`` and display ``text``.""" echo(cursor.term.move(cursor.y, cursor.x) + text) + Cursor = collections.namedtuple('Cursor', ('y', 'x', 'term')) + def readline(term, width=20): """A rudimentary readline implementation.""" text = u'' @@ -120,8 +124,7 @@ def redraw(term, screen, start=None, end=None): term=term)) lastcol, lastrow = -1, -1 for row, col in sorted(screen): - if (row >= start.y and row <= end.y and - col >= start.x and col <= end.x): + if start.y <= row <= end.y and start.x <= col <= end.x: if col >= term.width or row >= term.height: # out of bounds continue @@ -132,87 +135,89 @@ def redraw(term, screen, start=None, end=None): # just write past last one echo(screen[row, col]) + def main(): """Program entry point.""" - above = lambda csr, n: ( - Cursor(y=max(0, csr.y - n), - x=csr.x, - term=csr.term)) - - below = lambda csr, n: ( - Cursor(y=min(csr.term.height - 1, csr.y + n), - x=csr.x, - term=csr.term)) - - right_of = lambda csr, n: ( - Cursor(y=csr.y, - x=min(csr.term.width - 1, csr.x + n), - term=csr.term)) - - left_of = lambda csr, n: ( - Cursor(y=csr.y, - x=max(0, csr.x - n), - term=csr.term)) - - home = lambda csr: ( - Cursor(y=csr.y, - x=0, - term=csr.term)) - - end = lambda csr: ( - Cursor(y=csr.y, - x=csr.term.width - 1, - term=csr.term)) - - bottom = lambda csr: ( - Cursor(y=csr.term.height - 1, - x=csr.x, - term=csr.term)) - - center = lambda csr: Cursor( - csr.term.height // 2, - csr.term.width // 2, - csr.term) - - lookup_move = lambda inp_code, csr, term: { - # arrows, including angled directionals - csr.term.KEY_END: below(left_of(csr, 1), 1), - csr.term.KEY_KP_1: below(left_of(csr, 1), 1), - - csr.term.KEY_DOWN: below(csr, 1), - csr.term.KEY_KP_2: below(csr, 1), - - csr.term.KEY_PGDOWN: below(right_of(csr, 1), 1), - csr.term.KEY_LR: below(right_of(csr, 1), 1), - csr.term.KEY_KP_3: below(right_of(csr, 1), 1), - - csr.term.KEY_LEFT: left_of(csr, 1), - csr.term.KEY_KP_4: left_of(csr, 1), - - csr.term.KEY_CENTER: center(csr), - csr.term.KEY_KP_5: center(csr), - - csr.term.KEY_RIGHT: right_of(csr, 1), - csr.term.KEY_KP_6: right_of(csr, 1), - - csr.term.KEY_HOME: above(left_of(csr, 1), 1), - csr.term.KEY_KP_7: above(left_of(csr, 1), 1), - - csr.term.KEY_UP: above(csr, 1), - csr.term.KEY_KP_8: above(csr, 1), - - csr.term.KEY_PGUP: above(right_of(csr, 1), 1), - csr.term.KEY_KP_9: above(right_of(csr, 1), 1), - - # shift + arrows - csr.term.KEY_SLEFT: left_of(csr, 10), - csr.term.KEY_SRIGHT: right_of(csr, 10), - csr.term.KEY_SDOWN: below(csr, 10), - csr.term.KEY_SUP: above(csr, 10), - - # carriage return - csr.term.KEY_ENTER: home(below(csr, 1)), - }.get(inp_code, csr) + def above(csr, offset): + return Cursor(y=max(0, csr.y - offset), + x=csr.x, + term=csr.term) + + def below(csr, offset): + return Cursor(y=min(csr.term.height - 1, csr.y + offset), + x=csr.x, + term=csr.term) + + def right_of(csr, offset): + return Cursor(y=csr.y, + x=min(csr.term.width - 1, csr.x + offset), + term=csr.term) + + def left_of(csr, offset): + return Cursor(y=csr.y, + x=max(0, csr.x - offset), + term=csr.term) + + def home(csr): + return Cursor(y=csr.y, + x=0, + term=csr.term) + + def end(csr): + return Cursor(y=csr.y, + x=csr.term.width - 1, + term=csr.term) + + def bottom(csr): + return Cursor(y=csr.term.height - 1, + x=csr.x, + term=csr.term) + + def center(csr): + return Cursor(csr.term.height // 2, + csr.term.width // 2, + csr.term) + + def lookup_move(inp_code, csr): + return { + # arrows, including angled directionals + csr.term.KEY_END: below(left_of(csr, 1), 1), + csr.term.KEY_KP_1: below(left_of(csr, 1), 1), + + csr.term.KEY_DOWN: below(csr, 1), + csr.term.KEY_KP_2: below(csr, 1), + + csr.term.KEY_PGDOWN: below(right_of(csr, 1), 1), + csr.term.KEY_LR: below(right_of(csr, 1), 1), + csr.term.KEY_KP_3: below(right_of(csr, 1), 1), + + csr.term.KEY_LEFT: left_of(csr, 1), + csr.term.KEY_KP_4: left_of(csr, 1), + + csr.term.KEY_CENTER: center(csr), + csr.term.KEY_KP_5: center(csr), + + csr.term.KEY_RIGHT: right_of(csr, 1), + csr.term.KEY_KP_6: right_of(csr, 1), + + csr.term.KEY_HOME: above(left_of(csr, 1), 1), + csr.term.KEY_KP_7: above(left_of(csr, 1), 1), + + csr.term.KEY_UP: above(csr, 1), + csr.term.KEY_KP_8: above(csr, 1), + + csr.term.KEY_PGUP: above(right_of(csr, 1), 1), + csr.term.KEY_KP_9: above(right_of(csr, 1), 1), + + # shift + arrows + csr.term.KEY_SLEFT: left_of(csr, 10), + csr.term.KEY_SRIGHT: right_of(csr, 10), + csr.term.KEY_SDOWN: below(csr, 10), + csr.term.KEY_SUP: above(csr, 10), + + # carriage return + csr.term.KEY_ENTER: home(below(csr, 1)), + }.get(inp_code, csr) term = Terminal() csr = Cursor(0, 0, term) @@ -248,7 +253,7 @@ def main(): redraw(term=term, screen=screen) else: - n_csr = lookup_move(inp.code, csr, term) + n_csr = lookup_move(inp.code, csr) if n_csr != csr: # erase old cursor, @@ -264,5 +269,6 @@ def main(): n_csr = home(below(csr, 1)) csr = n_csr + if __name__ == '__main__': main() diff --git a/bin/keymatrix.py b/bin/keymatrix.py index ea69d937..9496a959 100755 --- a/bin/keymatrix.py +++ b/bin/keymatrix.py @@ -26,6 +26,7 @@ def echo(text): sys.stdout.write(u'{}'.format(text)) sys.stdout.flush() + def refresh(term, board, level, score, inps): """Refresh the game screen.""" echo(term.home + term.clear) @@ -41,8 +42,7 @@ def refresh(term, board, level, score, inps): keycode, term.normal))) bottom = max(bottom, attr['row']) - echo(term.move(term.height, 0) - + 'level: %s score: %s' % (level, score,)) + echo(term.move(term.height, 0) + 'level: %s score: %s' % (level, score,)) if bottom >= (term.height - 5): sys.stderr.write( ('\n' * (term.height // 2)) + @@ -61,6 +61,7 @@ def refresh(term, board, level, score, inps): inp.name)) echo(term.clear_eol) + def build_gameboard(term): """Build the gameboard layout.""" column, row = 0, 0 @@ -81,6 +82,7 @@ def build_gameboard(term): column += len(keycode) + (spacing * 2) return board + def add_score(score, pts, level): """Add points to score, determine and return new score and level.""" lvl_multiplier = 10 @@ -140,5 +142,6 @@ def main(): ) term.inkey() + if __name__ == '__main__': main() diff --git a/bin/on_resize.py b/bin/on_resize.py index e77bb453..a5cabfe7 100755 --- a/bin/on_resize.py +++ b/bin/on_resize.py @@ -12,6 +12,7 @@ from blessed import Terminal + def main(): """Program entry point.""" term = Terminal() @@ -41,5 +42,6 @@ def on_resize(*args): inp = term.inkey() print(repr(inp)) + if __name__ == '__main__': main() diff --git a/bin/progress_bar.py b/bin/progress_bar.py index 6a2ebfba..16522c58 100755 --- a/bin/progress_bar.py +++ b/bin/progress_bar.py @@ -43,5 +43,6 @@ def main(): inp = term.inkey(0.04) print() + if __name__ == '__main__': main() diff --git a/bin/worms.py b/bin/worms.py index f1353c77..32b56ff5 100755 --- a/bin/worms.py +++ b/bin/worms.py @@ -47,6 +47,7 @@ def echo(text): UP = (-1, 0) DOWN = (1, 0) + def left_of(segment, term): """Return Location left-of given segment.""" # pylint: disable=unused-argument @@ -54,11 +55,13 @@ def left_of(segment, term): return Location(y=segment.y, x=max(0, segment.x - 1)) + def right_of(segment, term): """Return Location right-of given segment.""" return Location(y=segment.y, x=min(term.width - 1, segment.x + 1)) + def above(segment, term): """Return Location above given segment.""" # pylint: disable=unused-argument @@ -67,12 +70,14 @@ def above(segment, term): y=max(0, segment.y - 1), x=segment.x) + def below(segment, term): """Return Location below given segment.""" return Location( y=min(term.height - 1, segment.y + 1), x=segment.x) + def next_bearing(term, inp_code, bearing): """ Return direction function for new bearing by inp_code. @@ -99,6 +104,7 @@ def change_bearing(f_mov, segment, term): f_mov(segment, term).y - segment.y, f_mov(segment, term).x - segment.x) + def bearing_flipped(dir1, dir2): """ direction-flipped check. @@ -107,32 +113,38 @@ def bearing_flipped(dir1, dir2): """ return (0, 0) == (dir1.y + dir2.y, dir1.x + dir2.x) + def hit_any(loc, segments): """Return True if `loc' matches any (y, x) coordinates within segments.""" # `segments' -- a list composing a worm. return loc in segments + def hit_vany(locations, segments): """Return True if any locations are found within any segments.""" return any(hit_any(loc, segments) for loc in locations) + def hit(src, dst): """Return True if segments are same position (hit detection).""" return src.x == dst.x and src.y == dst.y + def next_wormlength(nibble, head, worm_length): """Return new worm_length if current nibble is hit.""" if hit(head, nibble.location): return worm_length + nibble.value return worm_length + def next_speed(nibble, head, speed, modifier): """Return new speed if current nibble is hit.""" if hit(head, nibble.location): return speed * modifier return speed + def head_glyph(direction): """Return character for worm head depending on horiz/vert orientation.""" if direction in (left_of, right_of): @@ -151,7 +163,7 @@ def next_nibble(term, nibble, head, worm): loc, val = nibble.location, nibble.value while hit_vany([head] + worm, nibble_locations(loc, val)): loc = Location(x=randrange(1, term.width - 1), - y=randrange(1, term.height - 1)) + y=randrange(1, term.height - 1)) val = nibble.value + 1 return Nibble(loc, val) @@ -262,5 +274,6 @@ def main(): echo(u''.join((term.move(term.height - 1, 1), term.normal))) echo(u''.join((u'\r\n', u'score: {}'.format(score), u'\r\n'))) + if __name__ == '__main__': main() diff --git a/blessed/formatters.py b/blessed/formatters.py index 5484bb25..a7322feb 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -14,9 +14,9 @@ def _make_colors(): """ derivatives = ('on', 'bright', 'on_bright',) colors = set('black red green yellow blue magenta cyan white'.split()) - return set(['_'.join((_deravitive, _color)) - for _deravitive in derivatives - for _color in colors]) | colors + return set('_'.join((_deravitive, _color)) + for _deravitive in derivatives + for _color in colors) | colors def _make_compoundables(colors): @@ -403,17 +403,18 @@ def resolve_attribute(term, attr): if all(fmt in COMPOUNDABLES for fmt in formatters): resolution = (resolve_attribute(term, fmt) for fmt in formatters) return FormattingString(u''.join(resolution), term.normal) - else: - # otherwise, this is our end-game: given a sequence such as 'csr' - # (change scrolling region), return a ParameterizingString instance, - # that when called, performs and returns the final string after curses - # capability lookup is performed. - tparm_capseq = resolve_capability(term, attr) - if not tparm_capseq: - # and, for special terminals, such as 'screen', provide a Proxy - # ParameterizingString for attributes they do not claim to support, - # but actually do! (such as 'hpa' and 'vpa'). - proxy = get_proxy_string(term, term._sugar.get(attr, attr)) - if proxy is not None: - return proxy - return ParameterizingString(tparm_capseq, term.normal, attr) + + # otherwise, this is our end-game: given a sequence such as 'csr' + # (change scrolling region), return a ParameterizingString instance, + # that when called, performs and returns the final string after curses + # capability lookup is performed. + tparm_capseq = resolve_capability(term, attr) + if not tparm_capseq: + # and, for special terminals, such as 'screen', provide a Proxy + # ParameterizingString for attributes they do not claim to support, + # but actually do! (such as 'hpa' and 'vpa'). + proxy = get_proxy_string(term, term._sugar.get(attr, attr)) + if proxy is not None: + return proxy + + return ParameterizingString(tparm_capseq, term.normal, attr) diff --git a/blessed/keyboard.py b/blessed/keyboard.py index 4a4eab81..baa9a5eb 100644 --- a/blessed/keyboard.py +++ b/blessed/keyboard.py @@ -79,9 +79,8 @@ def get_curses_keycodes(): """ _keynames = [attr for attr in dir(curses) if attr.startswith('KEY_')] - return dict( - [(keyname, getattr(curses, keyname)) - for keyname in _keynames]) + return dict((keyname, getattr(curses, keyname)) + for keyname in _keynames) def get_keyboard_codes(): diff --git a/setup.py b/setup.py index c7c50f74..53c73970 100755 --- a/setup.py +++ b/setup.py @@ -25,6 +25,7 @@ def _get_long_description(fname): import codecs return codecs.open(fname, 'r', 'utf8').read() + HERE = os.path.dirname(__file__) setuptools.setup( From 9579f0e789262dfef88b916f0cc23787623eaf16 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 22 Sep 2019 18:35:45 -0700 Subject: [PATCH 388/459] earmark 1.1.15 for patch release --- docs/history.rst | 2 ++ version.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/history.rst b/docs/history.rst index be7608c9..2b612a2d 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -9,6 +9,8 @@ Version History * Support python 3.7. :ghissue:`102`. + * Various fixes to test automation :ghissue:`108` + 1.14 * bugfix: :meth:`~.Terminal.wrap` misbehaved for text containing newlines, :ghissue:`74`. diff --git a/version.json b/version.json index 25d66792..aee6dd1e 100644 --- a/version.json +++ b/version.json @@ -1 +1 @@ -{"version": "1.15.0"} +{"version": "1.15.1"} From bd47e2018d3cf537f24363031f4c842e7038e640 Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Mon, 23 Sep 2019 12:30:13 -0400 Subject: [PATCH 389/459] Add support for Windows --- blessed/__init__.py | 5 +- blessed/formatters.py | 8 +- blessed/keyboard.py | 13 ++- blessed/terminal.py | 43 +++++---- blessed/tests/test_keyboard.py | 3 +- blessed/win_terminal.py | 157 +++++++++++++++++++++++++++++++++ setup.py | 6 ++ 7 files changed, 211 insertions(+), 24 deletions(-) create mode 100644 blessed/win_terminal.py diff --git a/blessed/__init__.py b/blessed/__init__.py index 1775b353..30fcfe0f 100644 --- a/blessed/__init__.py +++ b/blessed/__init__.py @@ -7,7 +7,10 @@ import platform as _platform # local -from blessed.terminal import Terminal +if _platform.system() == 'Windows': + from blessed.win_terminal import Terminal +else: + from blessed.terminal import Terminal if ('3', '0', '0') <= _platform.python_version_tuple() < ('3', '2', '2+'): # Good till 3.2.10 diff --git a/blessed/formatters.py b/blessed/formatters.py index a7322feb..998edefd 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -1,10 +1,16 @@ """Sub-module providing sequence-formatting functions.""" # standard imports -import curses +import platform # 3rd-party import six +# curses +if platform.system() == 'Windows': + import jinxed as curses # pylint: disable=import-error +else: + import curses + def _make_colors(): """ diff --git a/blessed/keyboard.py b/blessed/keyboard.py index baa9a5eb..82345d18 100644 --- a/blessed/keyboard.py +++ b/blessed/keyboard.py @@ -1,8 +1,7 @@ """Sub-module providing 'keyboard awareness'.""" # std imports -import curses.has_key -import curses +import platform import time import re @@ -18,6 +17,15 @@ # Unable to import 'ordereddict' from ordereddict import OrderedDict +# curses +if platform.system() == 'Windows': + # pylint: disable=import-error + import jinxed as curses + from jinxed.has_key import _capability_names as capability_names +else: + import curses + from curses.has_key import _capability_names as capability_names + class Keystroke(six.text_type): """ @@ -170,7 +178,6 @@ def get_keyboard_sequences(term): # of a kermit or avatar terminal, for example, remains unchanged # in its byte sequence values even when represented by unicode. # - capability_names = curses.has_key._capability_names sequence_map = dict(( (seq.decode('latin1'), val) for (seq, val) in ( diff --git a/blessed/terminal.py b/blessed/terminal.py index 84ea99bf..8394bc2e 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -5,11 +5,11 @@ import codecs import collections import contextlib -import curses import functools import io import locale import os +import platform import select import struct import sys @@ -17,21 +17,6 @@ import warnings import re -try: - import termios - import fcntl - import tty - HAS_TTY = True -except ImportError: - _TTY_METHODS = ('setraw', 'cbreak', 'kbhit', 'height', 'width') - _MSG_NOSUPPORT = ( - "One or more of the modules: 'termios', 'fcntl', and 'tty' " - "are not found on your platform '{0}'. The following methods " - "of Terminal are dummy/no-op unless a deriving class overrides " - "them: {1}".format(sys.platform.lower(), ', '.join(_TTY_METHODS))) - warnings.warn(_MSG_NOSUPPORT) - HAS_TTY = False - try: InterruptedError except NameError: @@ -74,6 +59,26 @@ _time_left, ) +if platform.system() == 'Windows': + import jinxed as curses # pylint: disable=import-error + HAS_TTY = True +else: + import curses + + try: + import termios + import fcntl + import tty + HAS_TTY = True + except ImportError: + _TTY_METHODS = ('setraw', 'cbreak', 'kbhit', 'height', 'width') + _MSG_NOSUPPORT = ( + "One or more of the modules: 'termios', 'fcntl', and 'tty' " + "are not found on your platform '{0}'. The following methods " + "of Terminal are dummy/no-op unless a deriving class overrides " + "them: {1}".format(sys.platform.lower(), ', '.join(_TTY_METHODS))) + warnings.warn(_MSG_NOSUPPORT) + HAS_TTY = False _CUR_TERM = None # See comments at end of file @@ -196,7 +201,11 @@ def __init__(self, kind=None, stream=None, force_styling=False): # The descriptor to direct terminal initialization sequences to. self._init_descriptor = (sys.__stdout__.fileno() if stream_fd is None else stream_fd) - self._kind = kind or os.environ.get('TERM', 'unknown') + + if platform.system() == 'Windows': + self._kind = kind or curses.get_term(self._init_descriptor) + else: + self._kind = kind or os.environ.get('TERM', 'unknown') if self.does_styling: # Initialize curses (call setupterm). diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index dd351969..fbc75fd3 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -748,7 +748,6 @@ def child(kind): def test_get_keyboard_sequence(monkeypatch): "Test keyboard.get_keyboard_sequence. " - import curses.has_key import blessed.keyboard (KEY_SMALL, KEY_LARGE, KEY_MIXIN) = range(3) @@ -765,7 +764,7 @@ def test_get_keyboard_sequence(monkeypatch): lambda cap: {CAP_SMALL: SEQ_SMALL, CAP_LARGE: SEQ_LARGE}[cap]) - monkeypatch.setattr(curses.has_key, '_capability_names', + monkeypatch.setattr(blessed.keyboard, 'capability_names', dict(((KEY_SMALL, CAP_SMALL,), (KEY_LARGE, CAP_LARGE,)))) diff --git a/blessed/win_terminal.py b/blessed/win_terminal.py new file mode 100644 index 00000000..58c51a62 --- /dev/null +++ b/blessed/win_terminal.py @@ -0,0 +1,157 @@ +# encoding: utf-8 +"""Module containing Windows version of :class:`Terminal`.""" + +from __future__ import absolute_import + +import contextlib +import msvcrt # pylint: disable=import-error +import time + +import jinxed.win32 as win32 # pylint: disable=import-error + +from .terminal import WINSZ, Terminal as _Terminal + + +class Terminal(_Terminal): + """Windows subclass of :class:`Terminal`.""" + + def getch(self): + r""" + Read, decode, and return the next byte from the keyboard stream. + + :rtype: unicode + :returns: a single unicode character, or ``u''`` if a multi-byte + sequence has not yet been fully received. + + For versions of Windows 10.0.10586 and later, the console is expected + to be in ENABLE_VIRTUAL_TERMINAL_INPUT mode and the default method is + called. + + For older versions of Windows, msvcrt.getwch() is used. If the received + character is ``\x00`` or ``\xe0``, the next character is + automatically retrieved. + """ + if win32.VTMODE_SUPPORTED: + return super(Terminal, self).getch() + + rtn = msvcrt.getwch() + if rtn in ('\x00', '\xe0'): + rtn += msvcrt.getwch() + return rtn + + def kbhit(self, timeout=None, **_kwargs): + """ + Return whether a keypress has been detected on the keyboard. + + This method is used by :meth:`inkey` to determine if a byte may + be read using :meth:`getch` without blocking. This is implemented + by wrapping msvcrt.kbhit() in a timeout. + + :arg float timeout: When ``timeout`` is 0, this call is + non-blocking, otherwise blocking indefinitely until keypress + is detected when None (default). When ``timeout`` is a + positive number, returns after ``timeout`` seconds have + elapsed (float). + :rtype: bool + :returns: True if a keypress is awaiting to be read on the keyboard + attached to this terminal. + """ + end = time.time() + (timeout or 0) + while True: + + if msvcrt.kbhit(): + return True + + if timeout is not None and end < time.time(): + break + + return False + + @staticmethod + def _winsize(fd): + """ + Return named tuple describing size of the terminal by ``fd``. + + :arg int fd: file descriptor queries for its window size. + :rtype: WINSZ + + WINSZ is a :class:`collections.namedtuple` instance, whose structure + directly maps to the return value of the :const:`termios.TIOCGWINSZ` + ioctl return value. The return parameters are: + + - ``ws_row``: width of terminal by its number of character cells. + - ``ws_col``: height of terminal by its number of character cells. + - ``ws_xpixel``: width of terminal by pixels (not accurate). + - ``ws_ypixel``: height of terminal by pixels (not accurate). + """ + window = win32.get_terminal_size(fd) + return WINSZ(ws_row=window.lines, ws_col=window.columns, + ws_xpixel=0, ws_ypixel=0) + + @contextlib.contextmanager + def cbreak(self): + """ + Allow each keystroke to be read immediately after it is pressed. + + This is a context manager for ``jinxed.w32.setcbreak()``. + + .. note:: You must explicitly print any user input you would like + displayed. If you provide any kind of editing, you must handle + backspace and other line-editing control functions in this mode + as well! + + **Normally**, characters received from the keyboard cannot be read + by Python until the *Return* key is pressed. Also known as *cooked* or + *canonical input* mode, it allows the tty driver to provide + line-editing before shuttling the input to your program and is the + (implicit) default terminal mode set by most unix shells before + executing programs. + """ + if self._keyboard_fd is not None: + + filehandle = msvcrt.get_osfhandle(self._keyboard_fd) + + # Save current terminal mode: + save_mode = win32.get_console_mode(filehandle) + save_line_buffered = self._line_buffered + win32.setcbreak(filehandle) + try: + self._line_buffered = False + yield + finally: + win32.set_console_mode(filehandle, save_mode) + self._line_buffered = save_line_buffered + + else: + yield + + @contextlib.contextmanager + def raw(self): + """ + A context manager for ``jinxed.w32.setcbreak()``. + + Although both :meth:`break` and :meth:`raw` modes allow each keystroke + to be read immediately after it is pressed, Raw mode disables + processing of input and output. + + In cbreak mode, special input characters such as ``^C`` are + interpreted by the terminal driver and excluded from the stdin stream. + In raw mode these values are receive by the :meth:`inkey` method. + """ + if self._keyboard_fd is not None: + + filehandle = msvcrt.get_osfhandle(self._keyboard_fd) + + # Save current terminal mode: + save_mode = win32.get_console_mode(filehandle) + save_line_buffered = self._line_buffered + win32.setraw(filehandle) + try: + self._line_buffered = False + yield + finally: + win32.set_console_mode(filehandle, save_mode) + self._line_buffered = save_line_buffered + + else: + yield diff --git a/setup.py b/setup.py index 53c73970..60b06a65 100755 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ #!/usr/bin/env python """Distutils setup script.""" import os +import platform import setuptools @@ -13,6 +14,10 @@ def _get_install_requires(fname): if sys.version_info < (2, 7): result.append('ordereddict==1.1') + # Windows requires jinxed + if platform.system() == 'Windows': + result.append('jinxed>=0.5.4') + return result @@ -53,6 +58,7 @@ def _get_long_description(fname): 'Environment :: Console :: Curses', 'License :: OSI Approved :: MIT License', 'Operating System :: POSIX', + 'Operating System :: Microsoft :: Windows', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', From 4af65799e2efc1f7b4b216b9c752c9faafde621f Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Tue, 1 Oct 2019 08:55:30 -0400 Subject: [PATCH 390/459] Catch additional exceptions for Win size --- blessed/terminal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blessed/terminal.py b/blessed/terminal.py index 8394bc2e..b4dfb046 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -437,7 +437,7 @@ def _height_and_width(self): try: if fd is not None: return self._winsize(fd) - except IOError: + except (IOError, OSError, ValueError): pass return WINSZ(ws_row=int(os.getenv('LINES', '25')), From 0dade641519d8bb9bc9c41be1f94c3c4753b8db7 Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Wed, 16 Oct 2019 12:41:38 -0400 Subject: [PATCH 391/459] Use PIP 508 dependencies --- requirements.txt | 4 ++++ setup.py | 10 ---------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/requirements.txt b/requirements.txt index 10f88fa1..c1fa4fc4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,6 @@ wcwidth>=0.1.4 six>=1.9.0 +# support python2.6 by using backport of 'orderedict' +ordereddict==1.1; python_version < "2.7" +# Windows requires jinxed +jinxed>=0.5.4; platform_system == "Windows" \ No newline at end of file diff --git a/setup.py b/setup.py index 60b06a65..b76c20a7 100755 --- a/setup.py +++ b/setup.py @@ -1,23 +1,13 @@ #!/usr/bin/env python """Distutils setup script.""" import os -import platform import setuptools def _get_install_requires(fname): - import sys result = [req_line.strip() for req_line in open(fname) if req_line.strip() and not req_line.startswith('#')] - # support python2.6 by using backport of 'orderedict' - if sys.version_info < (2, 7): - result.append('ordereddict==1.1') - - # Windows requires jinxed - if platform.system() == 'Windows': - result.append('jinxed>=0.5.4') - return result From c71ce0786f8e801250c33072124dfcd439e1c88b Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Thu, 17 Oct 2019 19:18:39 -0700 Subject: [PATCH 392/459] v1.16 for release Plus a tiny demo i found in my bin folder ?? --- bin/strip.py | 16 ++++++++++++++++ docs/history.rst | 3 +++ version.json | 2 +- 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100755 bin/strip.py diff --git a/bin/strip.py b/bin/strip.py new file mode 100755 index 00000000..9a47e7fb --- /dev/null +++ b/bin/strip.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +"""Example scrip that strips input of terminal sequences.""" +import sys + +import blessed + + +def main(): + """Program entry point.""" + term = blessed.Terminal() + for line in sys.stdin: + print(term.strip_seqs(line)) + + +if __name__ == '__main__': + main() diff --git a/docs/history.rst b/docs/history.rst index 2b612a2d..4dbf3078 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -1,5 +1,8 @@ Version History =============== +1.16 + * Windows support?! :ghissue:`110` + 1.15 * disable timing integration tests for keyboard routines. diff --git a/version.json b/version.json index aee6dd1e..77e713b6 100644 --- a/version.json +++ b/version.json @@ -1 +1 @@ -{"version": "1.15.1"} +{"version": "1.16.0"} From 4d86380955d2c69bf823ec01733739ecabbe867e Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Thu, 17 Oct 2019 19:59:11 -0700 Subject: [PATCH 393/459] More notes about windows support --- docs/further.rst | 7 +++---- docs/history.rst | 2 +- docs/intro.rst | 14 ++++---------- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/docs/further.rst b/docs/further.rst index f6f14c5a..313ae483 100644 --- a/docs/further.rst +++ b/docs/further.rst @@ -76,10 +76,9 @@ Here are some recommended readings to help you along: The TTY driver is a great introduction to Kernel and Systems programming, because familiar components may be discovered and experimented with. It is - available on all operating systems (except windows), and because of its - critical nature, examples of efficient file I/O, character buffers (often - implemented as "ring buffers") and even fine-grained kernel locking can be - found. + available on all operating systems, and because of its critical nature, examples of + efficient file I/O, character buffers (often implemented as "ring buffers") and even + fine-grained kernel locking can be found. - `Thomas E. Dickey `_ has been maintaining `xterm `_, as well as a diff --git a/docs/history.rst b/docs/history.rst index 4dbf3078..8f1a813d 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -1,7 +1,7 @@ Version History =============== 1.16 - * Windows support?! :ghissue:`110` + * Windows support?! :ghissue:`110` by :ghuser:`avylove`. 1.15 * disable timing integration tests for keyboard routines. diff --git a/docs/intro.rst b/docs/intro.rst index 89617c63..6066e0a2 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -68,14 +68,6 @@ Brief Overview * Allows the printable length of strings containing sequences to be determined. -Blessed **does not** provide... - -* Windows command prompt support. A PDCurses_ build of python for windows - provides only partial support at this time -- there are plans to merge with - the ansi_ module in concert with colorama_ to resolve this. `Patches - welcome `_! - - Before And After ---------------- @@ -132,8 +124,10 @@ The same program with *Blessed* is simply: Requirements ------------ -*Blessed* is tested with Python 2.7, 3.4, and 3.5 on Debian Linux, Mac, and -FreeBSD. +*Blessed* is tested with Python 2.7, 3.4, 3.5, 3.6, and 3.7 on Linux, Mac, and +FreeBSD. Windows support was just added in October 2019, thanks to kind +contributions from :ghuser:`avylove`, give it a try, and please report any +strange issues! Further Documentation --------------------- From 5bd9dbba6cf4150c623492f4894b9d72610d6ecd Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Thu, 17 Oct 2019 20:06:00 -0700 Subject: [PATCH 394/459] doctouch --- docs/pains.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/pains.rst b/docs/pains.rst index 00e6d1ad..629a67ed 100644 --- a/docs/pains.rst +++ b/docs/pains.rst @@ -19,7 +19,7 @@ A recent phenomenon of users is to customize their base 16 colors to provide getting LCD displays of colorspaces that achieve close approximation to the original video terminals. Some find these values uncomfortably intense: in their original CRT form, their contrast and brightness was lowered by hardware -dials, whereas today's LCD's typically display well only near full intensity. +dials, whereas today's LCD's typically display colors well near full intensity. Though we may not *detect* the colorspace of the remote terminal, **we can**: @@ -33,10 +33,10 @@ Though we may not *detect* the colorspace of the remote terminal, **we can**: .. note:: It has become popular to use dynamic system-wide color palette adjustments - in software such as `f.lux`_, which adjust the system-wide "Color Profile" - of the entire graphics display depending on the time of day. One might - assume that ``term.blue("text")`` may be **completely** invisible to such - users during the night! + in software such as `f.lux`_, "Dark Mode", "Night Mode", and others, + which adjust the system-wide "Color Profile" of the entire graphics display + depending on the time of day. One might assume that ``term.blue("text")`` + may become **completely** invisible to such users during the night! Where is brown, purple, or grey? From 021900c890af63f259514f1902a6aac72219ab0e Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Thu, 17 Oct 2019 20:42:02 -0700 Subject: [PATCH 395/459] Can't use :ghuser: in README grr --- docs/intro.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/intro.rst b/docs/intro.rst index 6066e0a2..e4186069 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -125,9 +125,8 @@ Requirements ------------ *Blessed* is tested with Python 2.7, 3.4, 3.5, 3.6, and 3.7 on Linux, Mac, and -FreeBSD. Windows support was just added in October 2019, thanks to kind -contributions from :ghuser:`avylove`, give it a try, and please report any -strange issues! +FreeBSD. Windows support was just added in October 2019, give it a try, and +please report any strange issues! Further Documentation --------------------- From 513d6b4643b63e1cb0ea6512dfc776aa65f4be78 Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Mon, 21 Oct 2019 12:15:53 -0400 Subject: [PATCH 396/459] Better handling of unsupported terminals --- blessed/terminal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blessed/terminal.py b/blessed/terminal.py index b4dfb046..d0753ca0 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -437,7 +437,7 @@ def _height_and_width(self): try: if fd is not None: return self._winsize(fd) - except (IOError, OSError, ValueError): + except (IOError, OSError, ValueError, TypeError): pass return WINSZ(ws_row=int(os.getenv('LINES', '25')), From f4c8434847ec1939cb306676d5ae320940dab958 Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Mon, 21 Oct 2019 12:25:07 -0400 Subject: [PATCH 397/459] Don't set locale for Python 2 on Windows --- blessed/terminal.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/blessed/terminal.py b/blessed/terminal.py index d0753ca0..dab9a786 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -293,7 +293,11 @@ def __init__keycodes(self): if self._keyboard_fd is not None: # set input encoding and initialize incremental decoder - locale.setlocale(locale.LC_ALL, '') + if platform.system() == 'Windows' and sys.version_info[0] < 3: + # Default for setlocale() has side effects for Python 2 on Windows + pass + else: + locale.setlocale(locale.LC_ALL, '') self._encoding = locale.getpreferredencoding() or 'ascii' try: From 2d23d0504b573183d9af040e85addd4e5afc1aea Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Mon, 21 Oct 2019 12:26:50 -0400 Subject: [PATCH 398/459] Add .vscode to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 943eebde..d1dc4d34 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ htmlcov .coveralls.yml .DS_Store .*.sw? +.vscode From 5b2da155c25262857338f63ea9fde9a47aaaa9f7 Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Mon, 21 Oct 2019 12:36:51 -0400 Subject: [PATCH 399/459] Minor formatting for linting --- blessed/terminal.py | 2 +- docs/further.rst | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/blessed/terminal.py b/blessed/terminal.py index dab9a786..931488f4 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -294,7 +294,7 @@ def __init__keycodes(self): if self._keyboard_fd is not None: # set input encoding and initialize incremental decoder if platform.system() == 'Windows' and sys.version_info[0] < 3: - # Default for setlocale() has side effects for Python 2 on Windows + # Default for setlocale() has side effects for PY2 on Windows pass else: locale.setlocale(locale.LC_ALL, '') diff --git a/docs/further.rst b/docs/further.rst index 313ae483..9e93d078 100644 --- a/docs/further.rst +++ b/docs/further.rst @@ -76,9 +76,9 @@ Here are some recommended readings to help you along: The TTY driver is a great introduction to Kernel and Systems programming, because familiar components may be discovered and experimented with. It is - available on all operating systems, and because of its critical nature, examples of - efficient file I/O, character buffers (often implemented as "ring buffers") and even - fine-grained kernel locking can be found. + available on all operating systems, and because of its critical nature, + examples of efficient file I/O, character buffers (often implemented as + "ring buffers") and even fine-grained kernel locking can be found. - `Thomas E. Dickey `_ has been maintaining `xterm `_, as well as a From 0af50bff41fa2fae13f3a3516537630f7caded22 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Tue, 22 Oct 2019 07:01:15 -0700 Subject: [PATCH 400/459] bump for minor release --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 77e713b6..4d76db6e 100644 --- a/version.json +++ b/version.json @@ -1 +1 @@ -{"version": "1.16.0"} +{"version": "1.16.1"} From a7f86ebc2aa5095bbfc279edd183be6860ef844c Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Mon, 9 Dec 2019 07:18:25 -0500 Subject: [PATCH 401/459] Add color dictionaries and translator --- blessed/colors.py | 1108 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1108 insertions(+) create mode 100644 blessed/colors.py diff --git a/blessed/colors.py b/blessed/colors.py new file mode 100644 index 00000000..5c7bd54a --- /dev/null +++ b/blessed/colors.py @@ -0,0 +1,1108 @@ +""" +XTerm color mappings to X11 names. + +The XTerm values come from the following source files: +- https://github.com/ThomasDickey/xterm-snapshots/blob/master/256colres.h +- https://github.com/ThomasDickey/xterm-snapshots/blob/master/88colres.h +- https://github.com/ThomasDickey/xterm-snapshots/blob/master/XTerm-col.ad + + +The X11 color names are found in: +- https://github.com/freedesktop/xorg-rgb/blob/master/rgb.txt + +""" +# pylint: disable=too-many-lines + +from math import sqrt + + +RGB_256 = {0: (0, 0, 0), + 1: (205, 0, 0), + 2: (0, 205, 0), + 3: (205, 205, 0), + 4: (0, 0, 238), + 5: (205, 0, 205), + 6: (0, 205, 205), + 7: (229, 229, 229), + 8: (127, 127, 127), + 9: (255, 0, 0), + 10: (0, 255, 0), + 11: (255, 255, 0), + 12: (92, 92, 255), + 13: (255, 0, 255), + 14: (0, 255, 255), + 15: (255, 255, 255), + 16: (0, 0, 0), + 17: (0, 0, 95), + 18: (0, 0, 135), + 19: (0, 0, 175), + 20: (0, 0, 215), + 21: (0, 0, 255), + 22: (0, 95, 0), + 23: (0, 95, 95), + 24: (0, 95, 135), + 25: (0, 95, 175), + 26: (0, 95, 215), + 27: (0, 95, 255), + 28: (0, 135, 0), + 29: (0, 135, 95), + 30: (0, 135, 135), + 31: (0, 135, 175), + 32: (0, 135, 215), + 33: (0, 135, 255), + 34: (0, 175, 0), + 35: (0, 175, 95), + 36: (0, 175, 135), + 37: (0, 175, 175), + 38: (0, 175, 215), + 39: (0, 175, 255), + 40: (0, 215, 0), + 41: (0, 215, 95), + 42: (0, 215, 135), + 43: (0, 215, 175), + 44: (0, 215, 215), + 45: (0, 215, 255), + 46: (0, 255, 0), + 47: (0, 255, 95), + 48: (0, 255, 135), + 49: (0, 255, 175), + 50: (0, 255, 215), + 51: (0, 255, 255), + 52: (95, 0, 0), + 53: (95, 0, 95), + 54: (95, 0, 135), + 55: (95, 0, 175), + 56: (95, 0, 215), + 57: (95, 0, 255), + 58: (95, 95, 0), + 59: (95, 95, 95), + 60: (95, 95, 135), + 61: (95, 95, 175), + 62: (95, 95, 215), + 63: (95, 95, 255), + 64: (95, 135, 0), + 65: (95, 135, 95), + 66: (95, 135, 135), + 67: (95, 135, 175), + 68: (95, 135, 215), + 69: (95, 135, 255), + 70: (95, 175, 0), + 71: (95, 175, 95), + 72: (95, 175, 135), + 73: (95, 175, 175), + 74: (95, 175, 215), + 75: (95, 175, 255), + 76: (95, 215, 0), + 77: (95, 215, 95), + 78: (95, 215, 135), + 79: (95, 215, 175), + 80: (95, 215, 215), + 81: (95, 215, 255), + 82: (95, 255, 0), + 83: (95, 255, 95), + 84: (95, 255, 135), + 85: (95, 255, 175), + 86: (95, 255, 215), + 87: (95, 255, 255), + 88: (135, 0, 0), + 89: (135, 0, 95), + 90: (135, 0, 135), + 91: (135, 0, 175), + 92: (135, 0, 215), + 93: (135, 0, 255), + 94: (135, 95, 0), + 95: (135, 95, 95), + 96: (135, 95, 135), + 97: (135, 95, 175), + 98: (135, 95, 215), + 99: (135, 95, 255), + 100: (135, 135, 0), + 101: (135, 135, 95), + 102: (135, 135, 135), + 103: (135, 135, 175), + 104: (135, 135, 215), + 105: (135, 135, 255), + 106: (135, 175, 0), + 107: (135, 175, 95), + 108: (135, 175, 135), + 109: (135, 175, 175), + 110: (135, 175, 215), + 111: (135, 175, 255), + 112: (135, 215, 0), + 113: (135, 215, 95), + 114: (135, 215, 135), + 115: (135, 215, 175), + 116: (135, 215, 215), + 117: (135, 215, 255), + 118: (135, 255, 0), + 119: (135, 255, 95), + 120: (135, 255, 135), + 121: (135, 255, 175), + 122: (135, 255, 215), + 123: (135, 255, 255), + 124: (175, 0, 0), + 125: (175, 0, 95), + 126: (175, 0, 135), + 127: (175, 0, 175), + 128: (175, 0, 215), + 129: (175, 0, 255), + 130: (175, 95, 0), + 131: (175, 95, 95), + 132: (175, 95, 135), + 133: (175, 95, 175), + 134: (175, 95, 215), + 135: (175, 95, 255), + 136: (175, 135, 0), + 137: (175, 135, 95), + 138: (175, 135, 135), + 139: (175, 135, 175), + 140: (175, 135, 215), + 141: (175, 135, 255), + 142: (175, 175, 0), + 143: (175, 175, 95), + 144: (175, 175, 135), + 145: (175, 175, 175), + 146: (175, 175, 215), + 147: (175, 175, 255), + 148: (175, 215, 0), + 149: (175, 215, 95), + 150: (175, 215, 135), + 151: (175, 215, 175), + 152: (175, 215, 215), + 153: (175, 215, 255), + 154: (175, 255, 0), + 155: (175, 255, 95), + 156: (175, 255, 135), + 157: (175, 255, 175), + 158: (175, 255, 215), + 159: (175, 255, 255), + 160: (215, 0, 0), + 161: (215, 0, 95), + 162: (215, 0, 135), + 163: (215, 0, 175), + 164: (215, 0, 215), + 165: (215, 0, 255), + 166: (215, 95, 0), + 167: (215, 95, 95), + 168: (215, 95, 135), + 169: (215, 95, 175), + 170: (215, 95, 215), + 171: (215, 95, 255), + 172: (215, 135, 0), + 173: (215, 135, 95), + 174: (215, 135, 135), + 175: (215, 135, 175), + 176: (215, 135, 215), + 177: (215, 135, 255), + 178: (215, 175, 0), + 179: (215, 175, 95), + 180: (215, 175, 135), + 181: (215, 175, 175), + 182: (215, 175, 215), + 183: (215, 175, 255), + 184: (215, 215, 0), + 185: (215, 215, 95), + 186: (215, 215, 135), + 187: (215, 215, 175), + 188: (215, 215, 215), + 189: (215, 215, 255), + 190: (215, 255, 0), + 191: (215, 255, 95), + 192: (215, 255, 135), + 193: (215, 255, 175), + 194: (215, 255, 215), + 195: (215, 255, 255), + 196: (255, 0, 0), + 197: (255, 0, 95), + 198: (255, 0, 135), + 199: (255, 0, 175), + 200: (255, 0, 215), + 201: (255, 0, 255), + 202: (255, 95, 0), + 203: (255, 95, 95), + 204: (255, 95, 135), + 205: (255, 95, 175), + 206: (255, 95, 215), + 207: (255, 95, 255), + 208: (255, 135, 0), + 209: (255, 135, 95), + 210: (255, 135, 135), + 211: (255, 135, 175), + 212: (255, 135, 215), + 213: (255, 135, 255), + 214: (255, 175, 0), + 215: (255, 175, 95), + 216: (255, 175, 135), + 217: (255, 175, 175), + 218: (255, 175, 215), + 219: (255, 175, 255), + 220: (255, 215, 0), + 221: (255, 215, 95), + 222: (255, 215, 135), + 223: (255, 215, 175), + 224: (255, 215, 215), + 225: (255, 215, 255), + 226: (255, 255, 0), + 227: (255, 255, 95), + 228: (255, 255, 135), + 229: (255, 255, 175), + 230: (255, 255, 215), + 231: (255, 255, 255), + 232: (8, 8, 8), + 233: (18, 18, 18), + 234: (28, 28, 28), + 235: (38, 38, 38), + 236: (48, 48, 48), + 237: (58, 58, 58), + 238: (68, 68, 68), + 239: (78, 78, 78), + 240: (88, 88, 88), + 241: (98, 98, 98), + 242: (108, 108, 108), + 243: (118, 118, 118), + 244: (128, 128, 128), + 245: (138, 138, 138), + 246: (148, 148, 148), + 247: (158, 158, 158), + 248: (168, 168, 168), + 249: (178, 178, 178), + 250: (188, 188, 188), + 251: (198, 198, 198), + 252: (208, 208, 208), + 253: (218, 218, 218), + 254: (228, 228, 228), + 255: (238, 238, 238)} + +RGB_16 = dict((index, RGB_256[index]) for index in range(16)) +RGB_8 = dict((index, RGB_256[index]) for index in range(8)) + +RGB_TO_256 = {} +for key, value in sorted(RGB_256.items()): + RGB_TO_256.setdefault(value, key) + +RGB_TO_16 = {} +for key, value in sorted(RGB_16.items()): + RGB_TO_16.setdefault(value, key) + +RGB_TO_8 = {} +for key, value in sorted(RGB_8.items()): + RGB_TO_8.setdefault(value, key) + +RGB_88 = {0: (0, 0, 0), + 1: (205, 0, 0), + 2: (0, 205, 0), + 3: (205, 205, 0), + 4: (0, 0, 238), + 5: (205, 0, 205), + 6: (0, 205, 205), + 7: (229, 229, 229), + 8: (127, 127, 127), + 9: (255, 0, 0), + 10: (0, 255, 0), + 11: (255, 255, 0), + 12: (92, 92, 255), + 13: (255, 0, 255), + 14: (0, 255, 255), + 15: (255, 255, 255), + 16: (0, 0, 0), + 17: (0, 0, 139), + 18: (0, 0, 205), + 19: (0, 0, 255), + 20: (0, 139, 0), + 21: (0, 139, 139), + 22: (0, 139, 205), + 23: (0, 139, 255), + 24: (0, 205, 0), + 25: (0, 205, 139), + 26: (0, 205, 205), + 27: (0, 205, 255), + 28: (0, 255, 0), + 29: (0, 255, 139), + 30: (0, 255, 205), + 31: (0, 255, 255), + 32: (139, 0, 0), + 33: (139, 0, 139), + 34: (139, 0, 205), + 35: (139, 0, 255), + 36: (139, 139, 0), + 37: (139, 139, 139), + 38: (139, 139, 205), + 39: (139, 139, 255), + 40: (139, 205, 0), + 41: (139, 205, 139), + 42: (139, 205, 205), + 43: (139, 205, 255), + 44: (139, 255, 0), + 45: (139, 255, 139), + 46: (139, 255, 205), + 47: (139, 255, 255), + 48: (205, 0, 0), + 49: (205, 0, 139), + 50: (205, 0, 205), + 51: (205, 0, 255), + 52: (205, 139, 0), + 53: (205, 139, 139), + 54: (205, 139, 205), + 55: (205, 139, 255), + 56: (205, 205, 0), + 57: (205, 205, 139), + 58: (205, 205, 205), + 59: (205, 205, 255), + 60: (205, 255, 0), + 61: (205, 255, 139), + 62: (205, 255, 205), + 63: (205, 255, 255), + 64: (255, 0, 0), + 65: (255, 0, 139), + 66: (255, 0, 205), + 67: (255, 0, 255), + 68: (255, 139, 0), + 69: (255, 139, 139), + 70: (255, 139, 205), + 71: (255, 139, 255), + 72: (255, 205, 0), + 73: (255, 205, 139), + 74: (255, 205, 205), + 75: (255, 205, 255), + 76: (255, 255, 0), + 77: (255, 255, 139), + 78: (255, 255, 205), + 79: (255, 255, 255), + 80: (46, 46, 46), + 81: (92, 92, 92), + 82: (115, 115, 115), + 83: (139, 139, 139), + 84: (162, 162, 162), + 85: (185, 185, 185), + 86: (208, 208, 208), + 87: (231, 231, 231)} + +RGB_TO_88 = {} +for key, value in sorted(RGB_88.items()): + RGB_TO_88.setdefault(value, key) + +X11_NAMES = {'aliceblue': (240, 248, 255), + 'antiquewhite': (250, 235, 215), + 'antiquewhite1': (255, 239, 219), + 'antiquewhite2': (238, 223, 204), + 'antiquewhite3': (205, 192, 176), + 'antiquewhite4': (139, 131, 120), + 'aqua': (0, 255, 255), + 'aquamarine': (127, 255, 212), + 'aquamarine1': (127, 255, 212), + 'aquamarine2': (118, 238, 198), + 'aquamarine3': (102, 205, 170), + 'aquamarine4': (69, 139, 116), + 'azure': (240, 255, 255), + 'azure1': (240, 255, 255), + 'azure2': (224, 238, 238), + 'azure3': (193, 205, 205), + 'azure4': (131, 139, 139), + 'beige': (245, 245, 220), + 'bisque': (255, 228, 196), + 'bisque1': (255, 228, 196), + 'bisque2': (238, 213, 183), + 'bisque3': (205, 183, 158), + 'bisque4': (139, 125, 107), + 'black': (0, 0, 0), + 'blanchedalmond': (255, 235, 205), + 'blue': (0, 0, 255), + 'blue1': (0, 0, 255), + 'blue2': (0, 0, 238), + 'blue3': (0, 0, 205), + 'blue4': (0, 0, 139), + 'blueviolet': (138, 43, 226), + 'brown': (165, 42, 42), + 'brown1': (255, 64, 64), + 'brown2': (238, 59, 59), + 'brown3': (205, 51, 51), + 'brown4': (139, 35, 35), + 'burlywood': (222, 184, 135), + 'burlywood1': (255, 211, 155), + 'burlywood2': (238, 197, 145), + 'burlywood3': (205, 170, 125), + 'burlywood4': (139, 115, 85), + 'cadetblue': (95, 158, 160), + 'cadetblue1': (152, 245, 255), + 'cadetblue2': (142, 229, 238), + 'cadetblue3': (122, 197, 205), + 'cadetblue4': (83, 134, 139), + 'chartreuse': (127, 255, 0), + 'chartreuse1': (127, 255, 0), + 'chartreuse2': (118, 238, 0), + 'chartreuse3': (102, 205, 0), + 'chartreuse4': (69, 139, 0), + 'chocolate': (210, 105, 30), + 'chocolate1': (255, 127, 36), + 'chocolate2': (238, 118, 33), + 'chocolate3': (205, 102, 29), + 'chocolate4': (139, 69, 19), + 'coral': (255, 127, 80), + 'coral1': (255, 114, 86), + 'coral2': (238, 106, 80), + 'coral3': (205, 91, 69), + 'coral4': (139, 62, 47), + 'cornflowerblue': (100, 149, 237), + 'cornsilk': (255, 248, 220), + 'cornsilk1': (255, 248, 220), + 'cornsilk2': (238, 232, 205), + 'cornsilk3': (205, 200, 177), + 'cornsilk4': (139, 136, 120), + 'crimson': (220, 20, 60), + 'cyan': (0, 255, 255), + 'cyan1': (0, 255, 255), + 'cyan2': (0, 238, 238), + 'cyan3': (0, 205, 205), + 'cyan4': (0, 139, 139), + 'darkblue': (0, 0, 139), + 'darkcyan': (0, 139, 139), + 'darkgoldenrod': (184, 134, 11), + 'darkgoldenrod1': (255, 185, 15), + 'darkgoldenrod2': (238, 173, 14), + 'darkgoldenrod3': (205, 149, 12), + 'darkgoldenrod4': (139, 101, 8), + 'darkgray': (169, 169, 169), + 'darkgreen': (0, 100, 0), + 'darkgrey': (169, 169, 169), + 'darkkhaki': (189, 183, 107), + 'darkmagenta': (139, 0, 139), + 'darkolivegreen': (85, 107, 47), + 'darkolivegreen1': (202, 255, 112), + 'darkolivegreen2': (188, 238, 104), + 'darkolivegreen3': (162, 205, 90), + 'darkolivegreen4': (110, 139, 61), + 'darkorange': (255, 140, 0), + 'darkorange1': (255, 127, 0), + 'darkorange2': (238, 118, 0), + 'darkorange3': (205, 102, 0), + 'darkorange4': (139, 69, 0), + 'darkorchid': (153, 50, 204), + 'darkorchid1': (191, 62, 255), + 'darkorchid2': (178, 58, 238), + 'darkorchid3': (154, 50, 205), + 'darkorchid4': (104, 34, 139), + 'darkred': (139, 0, 0), + 'darksalmon': (233, 150, 122), + 'darkseagreen': (143, 188, 143), + 'darkseagreen1': (193, 255, 193), + 'darkseagreen2': (180, 238, 180), + 'darkseagreen3': (155, 205, 155), + 'darkseagreen4': (105, 139, 105), + 'darkslateblue': (72, 61, 139), + 'darkslategray': (47, 79, 79), + 'darkslategray1': (151, 255, 255), + 'darkslategray2': (141, 238, 238), + 'darkslategray3': (121, 205, 205), + 'darkslategray4': (82, 139, 139), + 'darkslategrey': (47, 79, 79), + 'darkturquoise': (0, 206, 209), + 'darkviolet': (148, 0, 211), + 'deeppink': (255, 20, 147), + 'deeppink1': (255, 20, 147), + 'deeppink2': (238, 18, 137), + 'deeppink3': (205, 16, 118), + 'deeppink4': (139, 10, 80), + 'deepskyblue': (0, 191, 255), + 'deepskyblue1': (0, 191, 255), + 'deepskyblue2': (0, 178, 238), + 'deepskyblue3': (0, 154, 205), + 'deepskyblue4': (0, 104, 139), + 'dimgray': (105, 105, 105), + 'dimgrey': (105, 105, 105), + 'dodgerblue': (30, 144, 255), + 'dodgerblue1': (30, 144, 255), + 'dodgerblue2': (28, 134, 238), + 'dodgerblue3': (24, 116, 205), + 'dodgerblue4': (16, 78, 139), + 'firebrick': (178, 34, 34), + 'firebrick1': (255, 48, 48), + 'firebrick2': (238, 44, 44), + 'firebrick3': (205, 38, 38), + 'firebrick4': (139, 26, 26), + 'floralwhite': (255, 250, 240), + 'forestgreen': (34, 139, 34), + 'fuchsia': (255, 0, 255), + 'gainsboro': (220, 220, 220), + 'ghostwhite': (248, 248, 255), + 'gold': (255, 215, 0), + 'gold1': (255, 215, 0), + 'gold2': (238, 201, 0), + 'gold3': (205, 173, 0), + 'gold4': (139, 117, 0), + 'goldenrod': (218, 165, 32), + 'goldenrod1': (255, 193, 37), + 'goldenrod2': (238, 180, 34), + 'goldenrod3': (205, 155, 29), + 'goldenrod4': (139, 105, 20), + 'gray': (190, 190, 190), + 'gray0': (0, 0, 0), + 'gray1': (3, 3, 3), + 'gray10': (26, 26, 26), + 'gray100': (255, 255, 255), + 'gray11': (28, 28, 28), + 'gray12': (31, 31, 31), + 'gray13': (33, 33, 33), + 'gray14': (36, 36, 36), + 'gray15': (38, 38, 38), + 'gray16': (41, 41, 41), + 'gray17': (43, 43, 43), + 'gray18': (46, 46, 46), + 'gray19': (48, 48, 48), + 'gray2': (5, 5, 5), + 'gray20': (51, 51, 51), + 'gray21': (54, 54, 54), + 'gray22': (56, 56, 56), + 'gray23': (59, 59, 59), + 'gray24': (61, 61, 61), + 'gray25': (64, 64, 64), + 'gray26': (66, 66, 66), + 'gray27': (69, 69, 69), + 'gray28': (71, 71, 71), + 'gray29': (74, 74, 74), + 'gray3': (8, 8, 8), + 'gray30': (77, 77, 77), + 'gray31': (79, 79, 79), + 'gray32': (82, 82, 82), + 'gray33': (84, 84, 84), + 'gray34': (87, 87, 87), + 'gray35': (89, 89, 89), + 'gray36': (92, 92, 92), + 'gray37': (94, 94, 94), + 'gray38': (97, 97, 97), + 'gray39': (99, 99, 99), + 'gray4': (10, 10, 10), + 'gray40': (102, 102, 102), + 'gray41': (105, 105, 105), + 'gray42': (107, 107, 107), + 'gray43': (110, 110, 110), + 'gray44': (112, 112, 112), + 'gray45': (115, 115, 115), + 'gray46': (117, 117, 117), + 'gray47': (120, 120, 120), + 'gray48': (122, 122, 122), + 'gray49': (125, 125, 125), + 'gray5': (13, 13, 13), + 'gray50': (127, 127, 127), + 'gray51': (130, 130, 130), + 'gray52': (133, 133, 133), + 'gray53': (135, 135, 135), + 'gray54': (138, 138, 138), + 'gray55': (140, 140, 140), + 'gray56': (143, 143, 143), + 'gray57': (145, 145, 145), + 'gray58': (148, 148, 148), + 'gray59': (150, 150, 150), + 'gray6': (15, 15, 15), + 'gray60': (153, 153, 153), + 'gray61': (156, 156, 156), + 'gray62': (158, 158, 158), + 'gray63': (161, 161, 161), + 'gray64': (163, 163, 163), + 'gray65': (166, 166, 166), + 'gray66': (168, 168, 168), + 'gray67': (171, 171, 171), + 'gray68': (173, 173, 173), + 'gray69': (176, 176, 176), + 'gray7': (18, 18, 18), + 'gray70': (179, 179, 179), + 'gray71': (181, 181, 181), + 'gray72': (184, 184, 184), + 'gray73': (186, 186, 186), + 'gray74': (189, 189, 189), + 'gray75': (191, 191, 191), + 'gray76': (194, 194, 194), + 'gray77': (196, 196, 196), + 'gray78': (199, 199, 199), + 'gray79': (201, 201, 201), + 'gray8': (20, 20, 20), + 'gray80': (204, 204, 204), + 'gray81': (207, 207, 207), + 'gray82': (209, 209, 209), + 'gray83': (212, 212, 212), + 'gray84': (214, 214, 214), + 'gray85': (217, 217, 217), + 'gray86': (219, 219, 219), + 'gray87': (222, 222, 222), + 'gray88': (224, 224, 224), + 'gray89': (227, 227, 227), + 'gray9': (23, 23, 23), + 'gray90': (229, 229, 229), + 'gray91': (232, 232, 232), + 'gray92': (235, 235, 235), + 'gray93': (237, 237, 237), + 'gray94': (240, 240, 240), + 'gray95': (242, 242, 242), + 'gray96': (245, 245, 245), + 'gray97': (247, 247, 247), + 'gray98': (250, 250, 250), + 'gray99': (252, 252, 252), + 'green': (0, 255, 0), + 'green1': (0, 255, 0), + 'green2': (0, 238, 0), + 'green3': (0, 205, 0), + 'green4': (0, 139, 0), + 'greenyellow': (173, 255, 47), + 'grey': (190, 190, 190), + 'grey0': (0, 0, 0), + 'grey1': (3, 3, 3), + 'grey10': (26, 26, 26), + 'grey100': (255, 255, 255), + 'grey11': (28, 28, 28), + 'grey12': (31, 31, 31), + 'grey13': (33, 33, 33), + 'grey14': (36, 36, 36), + 'grey15': (38, 38, 38), + 'grey16': (41, 41, 41), + 'grey17': (43, 43, 43), + 'grey18': (46, 46, 46), + 'grey19': (48, 48, 48), + 'grey2': (5, 5, 5), + 'grey20': (51, 51, 51), + 'grey21': (54, 54, 54), + 'grey22': (56, 56, 56), + 'grey23': (59, 59, 59), + 'grey24': (61, 61, 61), + 'grey25': (64, 64, 64), + 'grey26': (66, 66, 66), + 'grey27': (69, 69, 69), + 'grey28': (71, 71, 71), + 'grey29': (74, 74, 74), + 'grey3': (8, 8, 8), + 'grey30': (77, 77, 77), + 'grey31': (79, 79, 79), + 'grey32': (82, 82, 82), + 'grey33': (84, 84, 84), + 'grey34': (87, 87, 87), + 'grey35': (89, 89, 89), + 'grey36': (92, 92, 92), + 'grey37': (94, 94, 94), + 'grey38': (97, 97, 97), + 'grey39': (99, 99, 99), + 'grey4': (10, 10, 10), + 'grey40': (102, 102, 102), + 'grey41': (105, 105, 105), + 'grey42': (107, 107, 107), + 'grey43': (110, 110, 110), + 'grey44': (112, 112, 112), + 'grey45': (115, 115, 115), + 'grey46': (117, 117, 117), + 'grey47': (120, 120, 120), + 'grey48': (122, 122, 122), + 'grey49': (125, 125, 125), + 'grey5': (13, 13, 13), + 'grey50': (127, 127, 127), + 'grey51': (130, 130, 130), + 'grey52': (133, 133, 133), + 'grey53': (135, 135, 135), + 'grey54': (138, 138, 138), + 'grey55': (140, 140, 140), + 'grey56': (143, 143, 143), + 'grey57': (145, 145, 145), + 'grey58': (148, 148, 148), + 'grey59': (150, 150, 150), + 'grey6': (15, 15, 15), + 'grey60': (153, 153, 153), + 'grey61': (156, 156, 156), + 'grey62': (158, 158, 158), + 'grey63': (161, 161, 161), + 'grey64': (163, 163, 163), + 'grey65': (166, 166, 166), + 'grey66': (168, 168, 168), + 'grey67': (171, 171, 171), + 'grey68': (173, 173, 173), + 'grey69': (176, 176, 176), + 'grey7': (18, 18, 18), + 'grey70': (179, 179, 179), + 'grey71': (181, 181, 181), + 'grey72': (184, 184, 184), + 'grey73': (186, 186, 186), + 'grey74': (189, 189, 189), + 'grey75': (191, 191, 191), + 'grey76': (194, 194, 194), + 'grey77': (196, 196, 196), + 'grey78': (199, 199, 199), + 'grey79': (201, 201, 201), + 'grey8': (20, 20, 20), + 'grey80': (204, 204, 204), + 'grey81': (207, 207, 207), + 'grey82': (209, 209, 209), + 'grey83': (212, 212, 212), + 'grey84': (214, 214, 214), + 'grey85': (217, 217, 217), + 'grey86': (219, 219, 219), + 'grey87': (222, 222, 222), + 'grey88': (224, 224, 224), + 'grey89': (227, 227, 227), + 'grey9': (23, 23, 23), + 'grey90': (229, 229, 229), + 'grey91': (232, 232, 232), + 'grey92': (235, 235, 235), + 'grey93': (237, 237, 237), + 'grey94': (240, 240, 240), + 'grey95': (242, 242, 242), + 'grey96': (245, 245, 245), + 'grey97': (247, 247, 247), + 'grey98': (250, 250, 250), + 'grey99': (252, 252, 252), + 'honeydew': (240, 255, 240), + 'honeydew1': (240, 255, 240), + 'honeydew2': (224, 238, 224), + 'honeydew3': (193, 205, 193), + 'honeydew4': (131, 139, 131), + 'hotpink': (255, 105, 180), + 'hotpink1': (255, 110, 180), + 'hotpink2': (238, 106, 167), + 'hotpink3': (205, 96, 144), + 'hotpink4': (139, 58, 98), + 'indianred': (205, 92, 92), + 'indianred1': (255, 106, 106), + 'indianred2': (238, 99, 99), + 'indianred3': (205, 85, 85), + 'indianred4': (139, 58, 58), + 'indigo': (75, 0, 130), + 'ivory': (255, 255, 240), + 'ivory1': (255, 255, 240), + 'ivory2': (238, 238, 224), + 'ivory3': (205, 205, 193), + 'ivory4': (139, 139, 131), + 'khaki': (240, 230, 140), + 'khaki1': (255, 246, 143), + 'khaki2': (238, 230, 133), + 'khaki3': (205, 198, 115), + 'khaki4': (139, 134, 78), + 'lavender': (230, 230, 250), + 'lavenderblush': (255, 240, 245), + 'lavenderblush1': (255, 240, 245), + 'lavenderblush2': (238, 224, 229), + 'lavenderblush3': (205, 193, 197), + 'lavenderblush4': (139, 131, 134), + 'lawngreen': (124, 252, 0), + 'lemonchiffon': (255, 250, 205), + 'lemonchiffon1': (255, 250, 205), + 'lemonchiffon2': (238, 233, 191), + 'lemonchiffon3': (205, 201, 165), + 'lemonchiffon4': (139, 137, 112), + 'lightblue': (173, 216, 230), + 'lightblue1': (191, 239, 255), + 'lightblue2': (178, 223, 238), + 'lightblue3': (154, 192, 205), + 'lightblue4': (104, 131, 139), + 'lightcoral': (240, 128, 128), + 'lightcyan': (224, 255, 255), + 'lightcyan1': (224, 255, 255), + 'lightcyan2': (209, 238, 238), + 'lightcyan3': (180, 205, 205), + 'lightcyan4': (122, 139, 139), + 'lightgoldenrod': (238, 221, 130), + 'lightgoldenrod1': (255, 236, 139), + 'lightgoldenrod2': (238, 220, 130), + 'lightgoldenrod3': (205, 190, 112), + 'lightgoldenrod4': (139, 129, 76), + 'lightgoldenrodyellow': (250, 250, 210), + 'lightgray': (211, 211, 211), + 'lightgreen': (144, 238, 144), + 'lightgrey': (211, 211, 211), + 'lightpink': (255, 182, 193), + 'lightpink1': (255, 174, 185), + 'lightpink2': (238, 162, 173), + 'lightpink3': (205, 140, 149), + 'lightpink4': (139, 95, 101), + 'lightsalmon': (255, 160, 122), + 'lightsalmon1': (255, 160, 122), + 'lightsalmon2': (238, 149, 114), + 'lightsalmon3': (205, 129, 98), + 'lightsalmon4': (139, 87, 66), + 'lightseagreen': (32, 178, 170), + 'lightskyblue': (135, 206, 250), + 'lightskyblue1': (176, 226, 255), + 'lightskyblue2': (164, 211, 238), + 'lightskyblue3': (141, 182, 205), + 'lightskyblue4': (96, 123, 139), + 'lightslateblue': (132, 112, 255), + 'lightslategray': (119, 136, 153), + 'lightslategrey': (119, 136, 153), + 'lightsteelblue': (176, 196, 222), + 'lightsteelblue1': (202, 225, 255), + 'lightsteelblue2': (188, 210, 238), + 'lightsteelblue3': (162, 181, 205), + 'lightsteelblue4': (110, 123, 139), + 'lightyellow': (255, 255, 224), + 'lightyellow1': (255, 255, 224), + 'lightyellow2': (238, 238, 209), + 'lightyellow3': (205, 205, 180), + 'lightyellow4': (139, 139, 122), + 'lime': (0, 255, 0), + 'limegreen': (50, 205, 50), + 'linen': (250, 240, 230), + 'magenta': (255, 0, 255), + 'magenta1': (255, 0, 255), + 'magenta2': (238, 0, 238), + 'magenta3': (205, 0, 205), + 'magenta4': (139, 0, 139), + 'maroon': (176, 48, 96), + 'maroon1': (255, 52, 179), + 'maroon2': (238, 48, 167), + 'maroon3': (205, 41, 144), + 'maroon4': (139, 28, 98), + 'mediumaquamarine': (102, 205, 170), + 'mediumblue': (0, 0, 205), + 'mediumorchid': (186, 85, 211), + 'mediumorchid1': (224, 102, 255), + 'mediumorchid2': (209, 95, 238), + 'mediumorchid3': (180, 82, 205), + 'mediumorchid4': (122, 55, 139), + 'mediumpurple': (147, 112, 219), + 'mediumpurple1': (171, 130, 255), + 'mediumpurple2': (159, 121, 238), + 'mediumpurple3': (137, 104, 205), + 'mediumpurple4': (93, 71, 139), + 'mediumseagreen': (60, 179, 113), + 'mediumslateblue': (123, 104, 238), + 'mediumspringgreen': (0, 250, 154), + 'mediumturquoise': (72, 209, 204), + 'mediumvioletred': (199, 21, 133), + 'midnightblue': (25, 25, 112), + 'mintcream': (245, 255, 250), + 'mistyrose': (255, 228, 225), + 'mistyrose1': (255, 228, 225), + 'mistyrose2': (238, 213, 210), + 'mistyrose3': (205, 183, 181), + 'mistyrose4': (139, 125, 123), + 'moccasin': (255, 228, 181), + 'navajowhite': (255, 222, 173), + 'navajowhite1': (255, 222, 173), + 'navajowhite2': (238, 207, 161), + 'navajowhite3': (205, 179, 139), + 'navajowhite4': (139, 121, 94), + 'navy': (0, 0, 128), + 'navyblue': (0, 0, 128), + 'oldlace': (253, 245, 230), + 'olive': (128, 128, 0), + 'olivedrab': (107, 142, 35), + 'olivedrab1': (192, 255, 62), + 'olivedrab2': (179, 238, 58), + 'olivedrab3': (154, 205, 50), + 'olivedrab4': (105, 139, 34), + 'orange': (255, 165, 0), + 'orange1': (255, 165, 0), + 'orange2': (238, 154, 0), + 'orange3': (205, 133, 0), + 'orange4': (139, 90, 0), + 'orangered': (255, 69, 0), + 'orangered1': (255, 69, 0), + 'orangered2': (238, 64, 0), + 'orangered3': (205, 55, 0), + 'orangered4': (139, 37, 0), + 'orchid': (218, 112, 214), + 'orchid1': (255, 131, 250), + 'orchid2': (238, 122, 233), + 'orchid3': (205, 105, 201), + 'orchid4': (139, 71, 137), + 'palegoldenrod': (238, 232, 170), + 'palegreen': (152, 251, 152), + 'palegreen1': (154, 255, 154), + 'palegreen2': (144, 238, 144), + 'palegreen3': (124, 205, 124), + 'palegreen4': (84, 139, 84), + 'paleturquoise': (175, 238, 238), + 'paleturquoise1': (187, 255, 255), + 'paleturquoise2': (174, 238, 238), + 'paleturquoise3': (150, 205, 205), + 'paleturquoise4': (102, 139, 139), + 'palevioletred': (219, 112, 147), + 'palevioletred1': (255, 130, 171), + 'palevioletred2': (238, 121, 159), + 'palevioletred3': (205, 104, 137), + 'palevioletred4': (139, 71, 93), + 'papayawhip': (255, 239, 213), + 'peachpuff': (255, 218, 185), + 'peachpuff1': (255, 218, 185), + 'peachpuff2': (238, 203, 173), + 'peachpuff3': (205, 175, 149), + 'peachpuff4': (139, 119, 101), + 'peru': (205, 133, 63), + 'pink': (255, 192, 203), + 'pink1': (255, 181, 197), + 'pink2': (238, 169, 184), + 'pink3': (205, 145, 158), + 'pink4': (139, 99, 108), + 'plum': (221, 160, 221), + 'plum1': (255, 187, 255), + 'plum2': (238, 174, 238), + 'plum3': (205, 150, 205), + 'plum4': (139, 102, 139), + 'powderblue': (176, 224, 230), + 'purple': (160, 32, 240), + 'purple1': (155, 48, 255), + 'purple2': (145, 44, 238), + 'purple3': (125, 38, 205), + 'purple4': (85, 26, 139), + 'rebeccapurple': (102, 51, 153), + 'red': (255, 0, 0), + 'red1': (255, 0, 0), + 'red2': (238, 0, 0), + 'red3': (205, 0, 0), + 'red4': (139, 0, 0), + 'rosybrown': (188, 143, 143), + 'rosybrown1': (255, 193, 193), + 'rosybrown2': (238, 180, 180), + 'rosybrown3': (205, 155, 155), + 'rosybrown4': (139, 105, 105), + 'royalblue': (65, 105, 225), + 'royalblue1': (72, 118, 255), + 'royalblue2': (67, 110, 238), + 'royalblue3': (58, 95, 205), + 'royalblue4': (39, 64, 139), + 'saddlebrown': (139, 69, 19), + 'salmon': (250, 128, 114), + 'salmon1': (255, 140, 105), + 'salmon2': (238, 130, 98), + 'salmon3': (205, 112, 84), + 'salmon4': (139, 76, 57), + 'sandybrown': (244, 164, 96), + 'seagreen': (46, 139, 87), + 'seagreen1': (84, 255, 159), + 'seagreen2': (78, 238, 148), + 'seagreen3': (67, 205, 128), + 'seagreen4': (46, 139, 87), + 'seashell': (255, 245, 238), + 'seashell1': (255, 245, 238), + 'seashell2': (238, 229, 222), + 'seashell3': (205, 197, 191), + 'seashell4': (139, 134, 130), + 'sienna': (160, 82, 45), + 'sienna1': (255, 130, 71), + 'sienna2': (238, 121, 66), + 'sienna3': (205, 104, 57), + 'sienna4': (139, 71, 38), + 'silver': (192, 192, 192), + 'skyblue': (135, 206, 235), + 'skyblue1': (135, 206, 255), + 'skyblue2': (126, 192, 238), + 'skyblue3': (108, 166, 205), + 'skyblue4': (74, 112, 139), + 'slateblue': (106, 90, 205), + 'slateblue1': (131, 111, 255), + 'slateblue2': (122, 103, 238), + 'slateblue3': (105, 89, 205), + 'slateblue4': (71, 60, 139), + 'slategray': (112, 128, 144), + 'slategray1': (198, 226, 255), + 'slategray2': (185, 211, 238), + 'slategray3': (159, 182, 205), + 'slategray4': (108, 123, 139), + 'slategrey': (112, 128, 144), + 'snow': (255, 250, 250), + 'snow1': (255, 250, 250), + 'snow2': (238, 233, 233), + 'snow3': (205, 201, 201), + 'snow4': (139, 137, 137), + 'springgreen': (0, 255, 127), + 'springgreen1': (0, 255, 127), + 'springgreen2': (0, 238, 118), + 'springgreen3': (0, 205, 102), + 'springgreen4': (0, 139, 69), + 'steelblue': (70, 130, 180), + 'steelblue1': (99, 184, 255), + 'steelblue2': (92, 172, 238), + 'steelblue3': (79, 148, 205), + 'steelblue4': (54, 100, 139), + 'tan': (210, 180, 140), + 'tan1': (255, 165, 79), + 'tan2': (238, 154, 73), + 'tan3': (205, 133, 63), + 'tan4': (139, 90, 43), + 'teal': (0, 128, 128), + 'thistle': (216, 191, 216), + 'thistle1': (255, 225, 255), + 'thistle2': (238, 210, 238), + 'thistle3': (205, 181, 205), + 'thistle4': (139, 123, 139), + 'tomato': (255, 99, 71), + 'tomato1': (255, 99, 71), + 'tomato2': (238, 92, 66), + 'tomato3': (205, 79, 57), + 'tomato4': (139, 54, 38), + 'turquoise': (64, 224, 208), + 'turquoise1': (0, 245, 255), + 'turquoise2': (0, 229, 238), + 'turquoise3': (0, 197, 205), + 'turquoise4': (0, 134, 139), + 'violet': (238, 130, 238), + 'violetred': (208, 32, 144), + 'violetred1': (255, 62, 150), + 'violetred2': (238, 58, 140), + 'violetred3': (205, 50, 120), + 'violetred4': (139, 34, 82), + 'webgray': (128, 128, 128), + 'webgreen': (0, 128, 0), + 'webgrey': (128, 128, 128), + 'webmaroon': (128, 0, 0), + 'webpurple': (128, 0, 128), + 'wheat': (245, 222, 179), + 'wheat1': (255, 231, 186), + 'wheat2': (238, 216, 174), + 'wheat3': (205, 186, 150), + 'wheat4': (139, 126, 102), + 'white': (255, 255, 255), + 'whitesmoke': (245, 245, 245), + 'x11gray': (190, 190, 190), + 'x11green': (0, 255, 0), + 'x11grey': (190, 190, 190), + 'x11maroon': (176, 48, 96), + 'x11purple': (160, 32, 240), + 'yellow': (255, 255, 0), + 'yellow1': (255, 255, 0), + 'yellow2': (238, 238, 0), + 'yellow3': (205, 205, 0), + 'yellow4': (139, 139, 0), + 'yellowgreen': (154, 205, 50)} + + +class ColorTranslator(object): + """Translates RGB color to a color in the configured range.""" + + def __init__(self, num_colors=256): + """ + Initialize the translator. + + :arg int num_colors: The number of colors the terminal will suport + """ + if num_colors >= 256: + self.table = RGB_TO_256 + elif num_colors >= 88: + self.table = RGB_TO_88 + elif num_colors >= 16: + self.table = RGB_TO_16 + else: + self.table = RGB_TO_8 + + self.cache = {} + + def translate_color(self, rgb): + """ + Translate an RGB color to a color code in the configured color range. + + :arg tuple rgb: A tuple of 3 integers specifying an RGB color + :rtype: int + + This works by treating RGB colors as coordinates in three dimensional + space and finding the closest point within the configured color range + using the formula: + d^2 = (x2 - x1)^2 + (y2 - y1)^2 + (x2 - x1)^2 + + + """ + rtn = self.cache.get(rgb, None) + if rtn is None: + distance = closest = None + for code, rgb2 in self.table.values(): + new_distance = sqrt(sum(pow(rgb2[idx] - rgb[idx], 2) + for idx in range(3))) + if distance is None or new_distance < distance: + distance = new_distance + closest = code + + rtn = self.cache[rgb] = closest + + return rtn From 8348f1a88645cc15b03d92271148cd5e6d29aec4 Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Mon, 9 Dec 2019 07:18:41 -0500 Subject: [PATCH 402/459] Add truecolor detection --- blessed/terminal.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/blessed/terminal.py b/blessed/terminal.py index 931488f4..be0db988 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -233,6 +233,13 @@ def __init__(self, kind=None, stream=None, force_styling=False): ' returned for the remainder of this process.' % ( self._kind, _CUR_TERM,)) + if self._does_styling: + colorterm = os.environ.get('COLORTERM', None) + self._truecolor = (colorterm in ('truecolor', '24bit') + or platform.system() == 'Windows') + else: + self._truecolor = False + # initialize capabilities and terminal keycodes database self.__init__capabilities() self.__init__keycodes() From fe20a7d0188448377338ebd2491c15acff3070d9 Mon Sep 17 00:00:00 2001 From: Johnny Wezel Date: Mon, 6 Jan 2020 17:37:54 +0100 Subject: [PATCH 403/459] Map key (127) to Backspace --- blessed/keyboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blessed/keyboard.py b/blessed/keyboard.py index 82345d18..c48ee519 100644 --- a/blessed/keyboard.py +++ b/blessed/keyboard.py @@ -372,7 +372,7 @@ def _inject_curses_keynames(): (six.unichr(8), curses.KEY_BACKSPACE), (six.unichr(9), curses.KEY_TAB), (six.unichr(27), curses.KEY_EXIT), - (six.unichr(127), curses.KEY_DC), + (six.unichr(127), curses.KEY_BACKSPACE), (u"\x1b[A", curses.KEY_UP), (u"\x1b[B", curses.KEY_DOWN), From 0783df0ed85c97dac4426a1b5d1324a03ed69a08 Mon Sep 17 00:00:00 2001 From: Johnny Wezel Date: Mon, 6 Jan 2020 19:21:12 +0100 Subject: [PATCH 404/459] Change key name for (127) to BACKSPACE --- blessed/tests/test_keyboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index fbc75fd3..7a9bf1e7 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -880,7 +880,7 @@ def child(kind): assert resolve(unichr(8)).name == "KEY_BACKSPACE" assert resolve(unichr(9)).name == "KEY_TAB" assert resolve(unichr(27)).name == "KEY_ESCAPE" - assert resolve(unichr(127)).name == "KEY_DELETE" + assert resolve(unichr(127)).name == "KEY_BACKSPACE" assert resolve(u"\x1b[A").name == "KEY_UP" assert resolve(u"\x1b[B").name == "KEY_DOWN" assert resolve(u"\x1b[C").name == "KEY_RIGHT" From 4d884b79cac456b9fcea059cf59fe139ec265445 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Thu, 9 Jan 2020 22:03:31 -0800 Subject: [PATCH 405/459] initializing multi-dimensional lists --- blessed/__init__.py | 2 +- blessed/colors.py | 1108 ----------------------------------------- blessed/colorspace.py | 954 +++++++++++++++++++++++++++++++++++ blessed/formatters.py | 135 +++-- blessed/terminal.py | 58 ++- docs/history.rst | 5 + docs/overview.rst | 46 +- docs/pains.rst | 9 + 8 files changed, 1118 insertions(+), 1199 deletions(-) delete mode 100644 blessed/colors.py create mode 100644 blessed/colorspace.py diff --git a/blessed/__init__.py b/blessed/__init__.py index 30fcfe0f..18d53cb7 100644 --- a/blessed/__init__.py +++ b/blessed/__init__.py @@ -19,4 +19,4 @@ 'support due to http://bugs.python.org/issue10570.') __all__ = ('Terminal',) -__version__ = '1.15.0' +__version__ = '1.16.0' diff --git a/blessed/colors.py b/blessed/colors.py deleted file mode 100644 index 5c7bd54a..00000000 --- a/blessed/colors.py +++ /dev/null @@ -1,1108 +0,0 @@ -""" -XTerm color mappings to X11 names. - -The XTerm values come from the following source files: -- https://github.com/ThomasDickey/xterm-snapshots/blob/master/256colres.h -- https://github.com/ThomasDickey/xterm-snapshots/blob/master/88colres.h -- https://github.com/ThomasDickey/xterm-snapshots/blob/master/XTerm-col.ad - - -The X11 color names are found in: -- https://github.com/freedesktop/xorg-rgb/blob/master/rgb.txt - -""" -# pylint: disable=too-many-lines - -from math import sqrt - - -RGB_256 = {0: (0, 0, 0), - 1: (205, 0, 0), - 2: (0, 205, 0), - 3: (205, 205, 0), - 4: (0, 0, 238), - 5: (205, 0, 205), - 6: (0, 205, 205), - 7: (229, 229, 229), - 8: (127, 127, 127), - 9: (255, 0, 0), - 10: (0, 255, 0), - 11: (255, 255, 0), - 12: (92, 92, 255), - 13: (255, 0, 255), - 14: (0, 255, 255), - 15: (255, 255, 255), - 16: (0, 0, 0), - 17: (0, 0, 95), - 18: (0, 0, 135), - 19: (0, 0, 175), - 20: (0, 0, 215), - 21: (0, 0, 255), - 22: (0, 95, 0), - 23: (0, 95, 95), - 24: (0, 95, 135), - 25: (0, 95, 175), - 26: (0, 95, 215), - 27: (0, 95, 255), - 28: (0, 135, 0), - 29: (0, 135, 95), - 30: (0, 135, 135), - 31: (0, 135, 175), - 32: (0, 135, 215), - 33: (0, 135, 255), - 34: (0, 175, 0), - 35: (0, 175, 95), - 36: (0, 175, 135), - 37: (0, 175, 175), - 38: (0, 175, 215), - 39: (0, 175, 255), - 40: (0, 215, 0), - 41: (0, 215, 95), - 42: (0, 215, 135), - 43: (0, 215, 175), - 44: (0, 215, 215), - 45: (0, 215, 255), - 46: (0, 255, 0), - 47: (0, 255, 95), - 48: (0, 255, 135), - 49: (0, 255, 175), - 50: (0, 255, 215), - 51: (0, 255, 255), - 52: (95, 0, 0), - 53: (95, 0, 95), - 54: (95, 0, 135), - 55: (95, 0, 175), - 56: (95, 0, 215), - 57: (95, 0, 255), - 58: (95, 95, 0), - 59: (95, 95, 95), - 60: (95, 95, 135), - 61: (95, 95, 175), - 62: (95, 95, 215), - 63: (95, 95, 255), - 64: (95, 135, 0), - 65: (95, 135, 95), - 66: (95, 135, 135), - 67: (95, 135, 175), - 68: (95, 135, 215), - 69: (95, 135, 255), - 70: (95, 175, 0), - 71: (95, 175, 95), - 72: (95, 175, 135), - 73: (95, 175, 175), - 74: (95, 175, 215), - 75: (95, 175, 255), - 76: (95, 215, 0), - 77: (95, 215, 95), - 78: (95, 215, 135), - 79: (95, 215, 175), - 80: (95, 215, 215), - 81: (95, 215, 255), - 82: (95, 255, 0), - 83: (95, 255, 95), - 84: (95, 255, 135), - 85: (95, 255, 175), - 86: (95, 255, 215), - 87: (95, 255, 255), - 88: (135, 0, 0), - 89: (135, 0, 95), - 90: (135, 0, 135), - 91: (135, 0, 175), - 92: (135, 0, 215), - 93: (135, 0, 255), - 94: (135, 95, 0), - 95: (135, 95, 95), - 96: (135, 95, 135), - 97: (135, 95, 175), - 98: (135, 95, 215), - 99: (135, 95, 255), - 100: (135, 135, 0), - 101: (135, 135, 95), - 102: (135, 135, 135), - 103: (135, 135, 175), - 104: (135, 135, 215), - 105: (135, 135, 255), - 106: (135, 175, 0), - 107: (135, 175, 95), - 108: (135, 175, 135), - 109: (135, 175, 175), - 110: (135, 175, 215), - 111: (135, 175, 255), - 112: (135, 215, 0), - 113: (135, 215, 95), - 114: (135, 215, 135), - 115: (135, 215, 175), - 116: (135, 215, 215), - 117: (135, 215, 255), - 118: (135, 255, 0), - 119: (135, 255, 95), - 120: (135, 255, 135), - 121: (135, 255, 175), - 122: (135, 255, 215), - 123: (135, 255, 255), - 124: (175, 0, 0), - 125: (175, 0, 95), - 126: (175, 0, 135), - 127: (175, 0, 175), - 128: (175, 0, 215), - 129: (175, 0, 255), - 130: (175, 95, 0), - 131: (175, 95, 95), - 132: (175, 95, 135), - 133: (175, 95, 175), - 134: (175, 95, 215), - 135: (175, 95, 255), - 136: (175, 135, 0), - 137: (175, 135, 95), - 138: (175, 135, 135), - 139: (175, 135, 175), - 140: (175, 135, 215), - 141: (175, 135, 255), - 142: (175, 175, 0), - 143: (175, 175, 95), - 144: (175, 175, 135), - 145: (175, 175, 175), - 146: (175, 175, 215), - 147: (175, 175, 255), - 148: (175, 215, 0), - 149: (175, 215, 95), - 150: (175, 215, 135), - 151: (175, 215, 175), - 152: (175, 215, 215), - 153: (175, 215, 255), - 154: (175, 255, 0), - 155: (175, 255, 95), - 156: (175, 255, 135), - 157: (175, 255, 175), - 158: (175, 255, 215), - 159: (175, 255, 255), - 160: (215, 0, 0), - 161: (215, 0, 95), - 162: (215, 0, 135), - 163: (215, 0, 175), - 164: (215, 0, 215), - 165: (215, 0, 255), - 166: (215, 95, 0), - 167: (215, 95, 95), - 168: (215, 95, 135), - 169: (215, 95, 175), - 170: (215, 95, 215), - 171: (215, 95, 255), - 172: (215, 135, 0), - 173: (215, 135, 95), - 174: (215, 135, 135), - 175: (215, 135, 175), - 176: (215, 135, 215), - 177: (215, 135, 255), - 178: (215, 175, 0), - 179: (215, 175, 95), - 180: (215, 175, 135), - 181: (215, 175, 175), - 182: (215, 175, 215), - 183: (215, 175, 255), - 184: (215, 215, 0), - 185: (215, 215, 95), - 186: (215, 215, 135), - 187: (215, 215, 175), - 188: (215, 215, 215), - 189: (215, 215, 255), - 190: (215, 255, 0), - 191: (215, 255, 95), - 192: (215, 255, 135), - 193: (215, 255, 175), - 194: (215, 255, 215), - 195: (215, 255, 255), - 196: (255, 0, 0), - 197: (255, 0, 95), - 198: (255, 0, 135), - 199: (255, 0, 175), - 200: (255, 0, 215), - 201: (255, 0, 255), - 202: (255, 95, 0), - 203: (255, 95, 95), - 204: (255, 95, 135), - 205: (255, 95, 175), - 206: (255, 95, 215), - 207: (255, 95, 255), - 208: (255, 135, 0), - 209: (255, 135, 95), - 210: (255, 135, 135), - 211: (255, 135, 175), - 212: (255, 135, 215), - 213: (255, 135, 255), - 214: (255, 175, 0), - 215: (255, 175, 95), - 216: (255, 175, 135), - 217: (255, 175, 175), - 218: (255, 175, 215), - 219: (255, 175, 255), - 220: (255, 215, 0), - 221: (255, 215, 95), - 222: (255, 215, 135), - 223: (255, 215, 175), - 224: (255, 215, 215), - 225: (255, 215, 255), - 226: (255, 255, 0), - 227: (255, 255, 95), - 228: (255, 255, 135), - 229: (255, 255, 175), - 230: (255, 255, 215), - 231: (255, 255, 255), - 232: (8, 8, 8), - 233: (18, 18, 18), - 234: (28, 28, 28), - 235: (38, 38, 38), - 236: (48, 48, 48), - 237: (58, 58, 58), - 238: (68, 68, 68), - 239: (78, 78, 78), - 240: (88, 88, 88), - 241: (98, 98, 98), - 242: (108, 108, 108), - 243: (118, 118, 118), - 244: (128, 128, 128), - 245: (138, 138, 138), - 246: (148, 148, 148), - 247: (158, 158, 158), - 248: (168, 168, 168), - 249: (178, 178, 178), - 250: (188, 188, 188), - 251: (198, 198, 198), - 252: (208, 208, 208), - 253: (218, 218, 218), - 254: (228, 228, 228), - 255: (238, 238, 238)} - -RGB_16 = dict((index, RGB_256[index]) for index in range(16)) -RGB_8 = dict((index, RGB_256[index]) for index in range(8)) - -RGB_TO_256 = {} -for key, value in sorted(RGB_256.items()): - RGB_TO_256.setdefault(value, key) - -RGB_TO_16 = {} -for key, value in sorted(RGB_16.items()): - RGB_TO_16.setdefault(value, key) - -RGB_TO_8 = {} -for key, value in sorted(RGB_8.items()): - RGB_TO_8.setdefault(value, key) - -RGB_88 = {0: (0, 0, 0), - 1: (205, 0, 0), - 2: (0, 205, 0), - 3: (205, 205, 0), - 4: (0, 0, 238), - 5: (205, 0, 205), - 6: (0, 205, 205), - 7: (229, 229, 229), - 8: (127, 127, 127), - 9: (255, 0, 0), - 10: (0, 255, 0), - 11: (255, 255, 0), - 12: (92, 92, 255), - 13: (255, 0, 255), - 14: (0, 255, 255), - 15: (255, 255, 255), - 16: (0, 0, 0), - 17: (0, 0, 139), - 18: (0, 0, 205), - 19: (0, 0, 255), - 20: (0, 139, 0), - 21: (0, 139, 139), - 22: (0, 139, 205), - 23: (0, 139, 255), - 24: (0, 205, 0), - 25: (0, 205, 139), - 26: (0, 205, 205), - 27: (0, 205, 255), - 28: (0, 255, 0), - 29: (0, 255, 139), - 30: (0, 255, 205), - 31: (0, 255, 255), - 32: (139, 0, 0), - 33: (139, 0, 139), - 34: (139, 0, 205), - 35: (139, 0, 255), - 36: (139, 139, 0), - 37: (139, 139, 139), - 38: (139, 139, 205), - 39: (139, 139, 255), - 40: (139, 205, 0), - 41: (139, 205, 139), - 42: (139, 205, 205), - 43: (139, 205, 255), - 44: (139, 255, 0), - 45: (139, 255, 139), - 46: (139, 255, 205), - 47: (139, 255, 255), - 48: (205, 0, 0), - 49: (205, 0, 139), - 50: (205, 0, 205), - 51: (205, 0, 255), - 52: (205, 139, 0), - 53: (205, 139, 139), - 54: (205, 139, 205), - 55: (205, 139, 255), - 56: (205, 205, 0), - 57: (205, 205, 139), - 58: (205, 205, 205), - 59: (205, 205, 255), - 60: (205, 255, 0), - 61: (205, 255, 139), - 62: (205, 255, 205), - 63: (205, 255, 255), - 64: (255, 0, 0), - 65: (255, 0, 139), - 66: (255, 0, 205), - 67: (255, 0, 255), - 68: (255, 139, 0), - 69: (255, 139, 139), - 70: (255, 139, 205), - 71: (255, 139, 255), - 72: (255, 205, 0), - 73: (255, 205, 139), - 74: (255, 205, 205), - 75: (255, 205, 255), - 76: (255, 255, 0), - 77: (255, 255, 139), - 78: (255, 255, 205), - 79: (255, 255, 255), - 80: (46, 46, 46), - 81: (92, 92, 92), - 82: (115, 115, 115), - 83: (139, 139, 139), - 84: (162, 162, 162), - 85: (185, 185, 185), - 86: (208, 208, 208), - 87: (231, 231, 231)} - -RGB_TO_88 = {} -for key, value in sorted(RGB_88.items()): - RGB_TO_88.setdefault(value, key) - -X11_NAMES = {'aliceblue': (240, 248, 255), - 'antiquewhite': (250, 235, 215), - 'antiquewhite1': (255, 239, 219), - 'antiquewhite2': (238, 223, 204), - 'antiquewhite3': (205, 192, 176), - 'antiquewhite4': (139, 131, 120), - 'aqua': (0, 255, 255), - 'aquamarine': (127, 255, 212), - 'aquamarine1': (127, 255, 212), - 'aquamarine2': (118, 238, 198), - 'aquamarine3': (102, 205, 170), - 'aquamarine4': (69, 139, 116), - 'azure': (240, 255, 255), - 'azure1': (240, 255, 255), - 'azure2': (224, 238, 238), - 'azure3': (193, 205, 205), - 'azure4': (131, 139, 139), - 'beige': (245, 245, 220), - 'bisque': (255, 228, 196), - 'bisque1': (255, 228, 196), - 'bisque2': (238, 213, 183), - 'bisque3': (205, 183, 158), - 'bisque4': (139, 125, 107), - 'black': (0, 0, 0), - 'blanchedalmond': (255, 235, 205), - 'blue': (0, 0, 255), - 'blue1': (0, 0, 255), - 'blue2': (0, 0, 238), - 'blue3': (0, 0, 205), - 'blue4': (0, 0, 139), - 'blueviolet': (138, 43, 226), - 'brown': (165, 42, 42), - 'brown1': (255, 64, 64), - 'brown2': (238, 59, 59), - 'brown3': (205, 51, 51), - 'brown4': (139, 35, 35), - 'burlywood': (222, 184, 135), - 'burlywood1': (255, 211, 155), - 'burlywood2': (238, 197, 145), - 'burlywood3': (205, 170, 125), - 'burlywood4': (139, 115, 85), - 'cadetblue': (95, 158, 160), - 'cadetblue1': (152, 245, 255), - 'cadetblue2': (142, 229, 238), - 'cadetblue3': (122, 197, 205), - 'cadetblue4': (83, 134, 139), - 'chartreuse': (127, 255, 0), - 'chartreuse1': (127, 255, 0), - 'chartreuse2': (118, 238, 0), - 'chartreuse3': (102, 205, 0), - 'chartreuse4': (69, 139, 0), - 'chocolate': (210, 105, 30), - 'chocolate1': (255, 127, 36), - 'chocolate2': (238, 118, 33), - 'chocolate3': (205, 102, 29), - 'chocolate4': (139, 69, 19), - 'coral': (255, 127, 80), - 'coral1': (255, 114, 86), - 'coral2': (238, 106, 80), - 'coral3': (205, 91, 69), - 'coral4': (139, 62, 47), - 'cornflowerblue': (100, 149, 237), - 'cornsilk': (255, 248, 220), - 'cornsilk1': (255, 248, 220), - 'cornsilk2': (238, 232, 205), - 'cornsilk3': (205, 200, 177), - 'cornsilk4': (139, 136, 120), - 'crimson': (220, 20, 60), - 'cyan': (0, 255, 255), - 'cyan1': (0, 255, 255), - 'cyan2': (0, 238, 238), - 'cyan3': (0, 205, 205), - 'cyan4': (0, 139, 139), - 'darkblue': (0, 0, 139), - 'darkcyan': (0, 139, 139), - 'darkgoldenrod': (184, 134, 11), - 'darkgoldenrod1': (255, 185, 15), - 'darkgoldenrod2': (238, 173, 14), - 'darkgoldenrod3': (205, 149, 12), - 'darkgoldenrod4': (139, 101, 8), - 'darkgray': (169, 169, 169), - 'darkgreen': (0, 100, 0), - 'darkgrey': (169, 169, 169), - 'darkkhaki': (189, 183, 107), - 'darkmagenta': (139, 0, 139), - 'darkolivegreen': (85, 107, 47), - 'darkolivegreen1': (202, 255, 112), - 'darkolivegreen2': (188, 238, 104), - 'darkolivegreen3': (162, 205, 90), - 'darkolivegreen4': (110, 139, 61), - 'darkorange': (255, 140, 0), - 'darkorange1': (255, 127, 0), - 'darkorange2': (238, 118, 0), - 'darkorange3': (205, 102, 0), - 'darkorange4': (139, 69, 0), - 'darkorchid': (153, 50, 204), - 'darkorchid1': (191, 62, 255), - 'darkorchid2': (178, 58, 238), - 'darkorchid3': (154, 50, 205), - 'darkorchid4': (104, 34, 139), - 'darkred': (139, 0, 0), - 'darksalmon': (233, 150, 122), - 'darkseagreen': (143, 188, 143), - 'darkseagreen1': (193, 255, 193), - 'darkseagreen2': (180, 238, 180), - 'darkseagreen3': (155, 205, 155), - 'darkseagreen4': (105, 139, 105), - 'darkslateblue': (72, 61, 139), - 'darkslategray': (47, 79, 79), - 'darkslategray1': (151, 255, 255), - 'darkslategray2': (141, 238, 238), - 'darkslategray3': (121, 205, 205), - 'darkslategray4': (82, 139, 139), - 'darkslategrey': (47, 79, 79), - 'darkturquoise': (0, 206, 209), - 'darkviolet': (148, 0, 211), - 'deeppink': (255, 20, 147), - 'deeppink1': (255, 20, 147), - 'deeppink2': (238, 18, 137), - 'deeppink3': (205, 16, 118), - 'deeppink4': (139, 10, 80), - 'deepskyblue': (0, 191, 255), - 'deepskyblue1': (0, 191, 255), - 'deepskyblue2': (0, 178, 238), - 'deepskyblue3': (0, 154, 205), - 'deepskyblue4': (0, 104, 139), - 'dimgray': (105, 105, 105), - 'dimgrey': (105, 105, 105), - 'dodgerblue': (30, 144, 255), - 'dodgerblue1': (30, 144, 255), - 'dodgerblue2': (28, 134, 238), - 'dodgerblue3': (24, 116, 205), - 'dodgerblue4': (16, 78, 139), - 'firebrick': (178, 34, 34), - 'firebrick1': (255, 48, 48), - 'firebrick2': (238, 44, 44), - 'firebrick3': (205, 38, 38), - 'firebrick4': (139, 26, 26), - 'floralwhite': (255, 250, 240), - 'forestgreen': (34, 139, 34), - 'fuchsia': (255, 0, 255), - 'gainsboro': (220, 220, 220), - 'ghostwhite': (248, 248, 255), - 'gold': (255, 215, 0), - 'gold1': (255, 215, 0), - 'gold2': (238, 201, 0), - 'gold3': (205, 173, 0), - 'gold4': (139, 117, 0), - 'goldenrod': (218, 165, 32), - 'goldenrod1': (255, 193, 37), - 'goldenrod2': (238, 180, 34), - 'goldenrod3': (205, 155, 29), - 'goldenrod4': (139, 105, 20), - 'gray': (190, 190, 190), - 'gray0': (0, 0, 0), - 'gray1': (3, 3, 3), - 'gray10': (26, 26, 26), - 'gray100': (255, 255, 255), - 'gray11': (28, 28, 28), - 'gray12': (31, 31, 31), - 'gray13': (33, 33, 33), - 'gray14': (36, 36, 36), - 'gray15': (38, 38, 38), - 'gray16': (41, 41, 41), - 'gray17': (43, 43, 43), - 'gray18': (46, 46, 46), - 'gray19': (48, 48, 48), - 'gray2': (5, 5, 5), - 'gray20': (51, 51, 51), - 'gray21': (54, 54, 54), - 'gray22': (56, 56, 56), - 'gray23': (59, 59, 59), - 'gray24': (61, 61, 61), - 'gray25': (64, 64, 64), - 'gray26': (66, 66, 66), - 'gray27': (69, 69, 69), - 'gray28': (71, 71, 71), - 'gray29': (74, 74, 74), - 'gray3': (8, 8, 8), - 'gray30': (77, 77, 77), - 'gray31': (79, 79, 79), - 'gray32': (82, 82, 82), - 'gray33': (84, 84, 84), - 'gray34': (87, 87, 87), - 'gray35': (89, 89, 89), - 'gray36': (92, 92, 92), - 'gray37': (94, 94, 94), - 'gray38': (97, 97, 97), - 'gray39': (99, 99, 99), - 'gray4': (10, 10, 10), - 'gray40': (102, 102, 102), - 'gray41': (105, 105, 105), - 'gray42': (107, 107, 107), - 'gray43': (110, 110, 110), - 'gray44': (112, 112, 112), - 'gray45': (115, 115, 115), - 'gray46': (117, 117, 117), - 'gray47': (120, 120, 120), - 'gray48': (122, 122, 122), - 'gray49': (125, 125, 125), - 'gray5': (13, 13, 13), - 'gray50': (127, 127, 127), - 'gray51': (130, 130, 130), - 'gray52': (133, 133, 133), - 'gray53': (135, 135, 135), - 'gray54': (138, 138, 138), - 'gray55': (140, 140, 140), - 'gray56': (143, 143, 143), - 'gray57': (145, 145, 145), - 'gray58': (148, 148, 148), - 'gray59': (150, 150, 150), - 'gray6': (15, 15, 15), - 'gray60': (153, 153, 153), - 'gray61': (156, 156, 156), - 'gray62': (158, 158, 158), - 'gray63': (161, 161, 161), - 'gray64': (163, 163, 163), - 'gray65': (166, 166, 166), - 'gray66': (168, 168, 168), - 'gray67': (171, 171, 171), - 'gray68': (173, 173, 173), - 'gray69': (176, 176, 176), - 'gray7': (18, 18, 18), - 'gray70': (179, 179, 179), - 'gray71': (181, 181, 181), - 'gray72': (184, 184, 184), - 'gray73': (186, 186, 186), - 'gray74': (189, 189, 189), - 'gray75': (191, 191, 191), - 'gray76': (194, 194, 194), - 'gray77': (196, 196, 196), - 'gray78': (199, 199, 199), - 'gray79': (201, 201, 201), - 'gray8': (20, 20, 20), - 'gray80': (204, 204, 204), - 'gray81': (207, 207, 207), - 'gray82': (209, 209, 209), - 'gray83': (212, 212, 212), - 'gray84': (214, 214, 214), - 'gray85': (217, 217, 217), - 'gray86': (219, 219, 219), - 'gray87': (222, 222, 222), - 'gray88': (224, 224, 224), - 'gray89': (227, 227, 227), - 'gray9': (23, 23, 23), - 'gray90': (229, 229, 229), - 'gray91': (232, 232, 232), - 'gray92': (235, 235, 235), - 'gray93': (237, 237, 237), - 'gray94': (240, 240, 240), - 'gray95': (242, 242, 242), - 'gray96': (245, 245, 245), - 'gray97': (247, 247, 247), - 'gray98': (250, 250, 250), - 'gray99': (252, 252, 252), - 'green': (0, 255, 0), - 'green1': (0, 255, 0), - 'green2': (0, 238, 0), - 'green3': (0, 205, 0), - 'green4': (0, 139, 0), - 'greenyellow': (173, 255, 47), - 'grey': (190, 190, 190), - 'grey0': (0, 0, 0), - 'grey1': (3, 3, 3), - 'grey10': (26, 26, 26), - 'grey100': (255, 255, 255), - 'grey11': (28, 28, 28), - 'grey12': (31, 31, 31), - 'grey13': (33, 33, 33), - 'grey14': (36, 36, 36), - 'grey15': (38, 38, 38), - 'grey16': (41, 41, 41), - 'grey17': (43, 43, 43), - 'grey18': (46, 46, 46), - 'grey19': (48, 48, 48), - 'grey2': (5, 5, 5), - 'grey20': (51, 51, 51), - 'grey21': (54, 54, 54), - 'grey22': (56, 56, 56), - 'grey23': (59, 59, 59), - 'grey24': (61, 61, 61), - 'grey25': (64, 64, 64), - 'grey26': (66, 66, 66), - 'grey27': (69, 69, 69), - 'grey28': (71, 71, 71), - 'grey29': (74, 74, 74), - 'grey3': (8, 8, 8), - 'grey30': (77, 77, 77), - 'grey31': (79, 79, 79), - 'grey32': (82, 82, 82), - 'grey33': (84, 84, 84), - 'grey34': (87, 87, 87), - 'grey35': (89, 89, 89), - 'grey36': (92, 92, 92), - 'grey37': (94, 94, 94), - 'grey38': (97, 97, 97), - 'grey39': (99, 99, 99), - 'grey4': (10, 10, 10), - 'grey40': (102, 102, 102), - 'grey41': (105, 105, 105), - 'grey42': (107, 107, 107), - 'grey43': (110, 110, 110), - 'grey44': (112, 112, 112), - 'grey45': (115, 115, 115), - 'grey46': (117, 117, 117), - 'grey47': (120, 120, 120), - 'grey48': (122, 122, 122), - 'grey49': (125, 125, 125), - 'grey5': (13, 13, 13), - 'grey50': (127, 127, 127), - 'grey51': (130, 130, 130), - 'grey52': (133, 133, 133), - 'grey53': (135, 135, 135), - 'grey54': (138, 138, 138), - 'grey55': (140, 140, 140), - 'grey56': (143, 143, 143), - 'grey57': (145, 145, 145), - 'grey58': (148, 148, 148), - 'grey59': (150, 150, 150), - 'grey6': (15, 15, 15), - 'grey60': (153, 153, 153), - 'grey61': (156, 156, 156), - 'grey62': (158, 158, 158), - 'grey63': (161, 161, 161), - 'grey64': (163, 163, 163), - 'grey65': (166, 166, 166), - 'grey66': (168, 168, 168), - 'grey67': (171, 171, 171), - 'grey68': (173, 173, 173), - 'grey69': (176, 176, 176), - 'grey7': (18, 18, 18), - 'grey70': (179, 179, 179), - 'grey71': (181, 181, 181), - 'grey72': (184, 184, 184), - 'grey73': (186, 186, 186), - 'grey74': (189, 189, 189), - 'grey75': (191, 191, 191), - 'grey76': (194, 194, 194), - 'grey77': (196, 196, 196), - 'grey78': (199, 199, 199), - 'grey79': (201, 201, 201), - 'grey8': (20, 20, 20), - 'grey80': (204, 204, 204), - 'grey81': (207, 207, 207), - 'grey82': (209, 209, 209), - 'grey83': (212, 212, 212), - 'grey84': (214, 214, 214), - 'grey85': (217, 217, 217), - 'grey86': (219, 219, 219), - 'grey87': (222, 222, 222), - 'grey88': (224, 224, 224), - 'grey89': (227, 227, 227), - 'grey9': (23, 23, 23), - 'grey90': (229, 229, 229), - 'grey91': (232, 232, 232), - 'grey92': (235, 235, 235), - 'grey93': (237, 237, 237), - 'grey94': (240, 240, 240), - 'grey95': (242, 242, 242), - 'grey96': (245, 245, 245), - 'grey97': (247, 247, 247), - 'grey98': (250, 250, 250), - 'grey99': (252, 252, 252), - 'honeydew': (240, 255, 240), - 'honeydew1': (240, 255, 240), - 'honeydew2': (224, 238, 224), - 'honeydew3': (193, 205, 193), - 'honeydew4': (131, 139, 131), - 'hotpink': (255, 105, 180), - 'hotpink1': (255, 110, 180), - 'hotpink2': (238, 106, 167), - 'hotpink3': (205, 96, 144), - 'hotpink4': (139, 58, 98), - 'indianred': (205, 92, 92), - 'indianred1': (255, 106, 106), - 'indianred2': (238, 99, 99), - 'indianred3': (205, 85, 85), - 'indianred4': (139, 58, 58), - 'indigo': (75, 0, 130), - 'ivory': (255, 255, 240), - 'ivory1': (255, 255, 240), - 'ivory2': (238, 238, 224), - 'ivory3': (205, 205, 193), - 'ivory4': (139, 139, 131), - 'khaki': (240, 230, 140), - 'khaki1': (255, 246, 143), - 'khaki2': (238, 230, 133), - 'khaki3': (205, 198, 115), - 'khaki4': (139, 134, 78), - 'lavender': (230, 230, 250), - 'lavenderblush': (255, 240, 245), - 'lavenderblush1': (255, 240, 245), - 'lavenderblush2': (238, 224, 229), - 'lavenderblush3': (205, 193, 197), - 'lavenderblush4': (139, 131, 134), - 'lawngreen': (124, 252, 0), - 'lemonchiffon': (255, 250, 205), - 'lemonchiffon1': (255, 250, 205), - 'lemonchiffon2': (238, 233, 191), - 'lemonchiffon3': (205, 201, 165), - 'lemonchiffon4': (139, 137, 112), - 'lightblue': (173, 216, 230), - 'lightblue1': (191, 239, 255), - 'lightblue2': (178, 223, 238), - 'lightblue3': (154, 192, 205), - 'lightblue4': (104, 131, 139), - 'lightcoral': (240, 128, 128), - 'lightcyan': (224, 255, 255), - 'lightcyan1': (224, 255, 255), - 'lightcyan2': (209, 238, 238), - 'lightcyan3': (180, 205, 205), - 'lightcyan4': (122, 139, 139), - 'lightgoldenrod': (238, 221, 130), - 'lightgoldenrod1': (255, 236, 139), - 'lightgoldenrod2': (238, 220, 130), - 'lightgoldenrod3': (205, 190, 112), - 'lightgoldenrod4': (139, 129, 76), - 'lightgoldenrodyellow': (250, 250, 210), - 'lightgray': (211, 211, 211), - 'lightgreen': (144, 238, 144), - 'lightgrey': (211, 211, 211), - 'lightpink': (255, 182, 193), - 'lightpink1': (255, 174, 185), - 'lightpink2': (238, 162, 173), - 'lightpink3': (205, 140, 149), - 'lightpink4': (139, 95, 101), - 'lightsalmon': (255, 160, 122), - 'lightsalmon1': (255, 160, 122), - 'lightsalmon2': (238, 149, 114), - 'lightsalmon3': (205, 129, 98), - 'lightsalmon4': (139, 87, 66), - 'lightseagreen': (32, 178, 170), - 'lightskyblue': (135, 206, 250), - 'lightskyblue1': (176, 226, 255), - 'lightskyblue2': (164, 211, 238), - 'lightskyblue3': (141, 182, 205), - 'lightskyblue4': (96, 123, 139), - 'lightslateblue': (132, 112, 255), - 'lightslategray': (119, 136, 153), - 'lightslategrey': (119, 136, 153), - 'lightsteelblue': (176, 196, 222), - 'lightsteelblue1': (202, 225, 255), - 'lightsteelblue2': (188, 210, 238), - 'lightsteelblue3': (162, 181, 205), - 'lightsteelblue4': (110, 123, 139), - 'lightyellow': (255, 255, 224), - 'lightyellow1': (255, 255, 224), - 'lightyellow2': (238, 238, 209), - 'lightyellow3': (205, 205, 180), - 'lightyellow4': (139, 139, 122), - 'lime': (0, 255, 0), - 'limegreen': (50, 205, 50), - 'linen': (250, 240, 230), - 'magenta': (255, 0, 255), - 'magenta1': (255, 0, 255), - 'magenta2': (238, 0, 238), - 'magenta3': (205, 0, 205), - 'magenta4': (139, 0, 139), - 'maroon': (176, 48, 96), - 'maroon1': (255, 52, 179), - 'maroon2': (238, 48, 167), - 'maroon3': (205, 41, 144), - 'maroon4': (139, 28, 98), - 'mediumaquamarine': (102, 205, 170), - 'mediumblue': (0, 0, 205), - 'mediumorchid': (186, 85, 211), - 'mediumorchid1': (224, 102, 255), - 'mediumorchid2': (209, 95, 238), - 'mediumorchid3': (180, 82, 205), - 'mediumorchid4': (122, 55, 139), - 'mediumpurple': (147, 112, 219), - 'mediumpurple1': (171, 130, 255), - 'mediumpurple2': (159, 121, 238), - 'mediumpurple3': (137, 104, 205), - 'mediumpurple4': (93, 71, 139), - 'mediumseagreen': (60, 179, 113), - 'mediumslateblue': (123, 104, 238), - 'mediumspringgreen': (0, 250, 154), - 'mediumturquoise': (72, 209, 204), - 'mediumvioletred': (199, 21, 133), - 'midnightblue': (25, 25, 112), - 'mintcream': (245, 255, 250), - 'mistyrose': (255, 228, 225), - 'mistyrose1': (255, 228, 225), - 'mistyrose2': (238, 213, 210), - 'mistyrose3': (205, 183, 181), - 'mistyrose4': (139, 125, 123), - 'moccasin': (255, 228, 181), - 'navajowhite': (255, 222, 173), - 'navajowhite1': (255, 222, 173), - 'navajowhite2': (238, 207, 161), - 'navajowhite3': (205, 179, 139), - 'navajowhite4': (139, 121, 94), - 'navy': (0, 0, 128), - 'navyblue': (0, 0, 128), - 'oldlace': (253, 245, 230), - 'olive': (128, 128, 0), - 'olivedrab': (107, 142, 35), - 'olivedrab1': (192, 255, 62), - 'olivedrab2': (179, 238, 58), - 'olivedrab3': (154, 205, 50), - 'olivedrab4': (105, 139, 34), - 'orange': (255, 165, 0), - 'orange1': (255, 165, 0), - 'orange2': (238, 154, 0), - 'orange3': (205, 133, 0), - 'orange4': (139, 90, 0), - 'orangered': (255, 69, 0), - 'orangered1': (255, 69, 0), - 'orangered2': (238, 64, 0), - 'orangered3': (205, 55, 0), - 'orangered4': (139, 37, 0), - 'orchid': (218, 112, 214), - 'orchid1': (255, 131, 250), - 'orchid2': (238, 122, 233), - 'orchid3': (205, 105, 201), - 'orchid4': (139, 71, 137), - 'palegoldenrod': (238, 232, 170), - 'palegreen': (152, 251, 152), - 'palegreen1': (154, 255, 154), - 'palegreen2': (144, 238, 144), - 'palegreen3': (124, 205, 124), - 'palegreen4': (84, 139, 84), - 'paleturquoise': (175, 238, 238), - 'paleturquoise1': (187, 255, 255), - 'paleturquoise2': (174, 238, 238), - 'paleturquoise3': (150, 205, 205), - 'paleturquoise4': (102, 139, 139), - 'palevioletred': (219, 112, 147), - 'palevioletred1': (255, 130, 171), - 'palevioletred2': (238, 121, 159), - 'palevioletred3': (205, 104, 137), - 'palevioletred4': (139, 71, 93), - 'papayawhip': (255, 239, 213), - 'peachpuff': (255, 218, 185), - 'peachpuff1': (255, 218, 185), - 'peachpuff2': (238, 203, 173), - 'peachpuff3': (205, 175, 149), - 'peachpuff4': (139, 119, 101), - 'peru': (205, 133, 63), - 'pink': (255, 192, 203), - 'pink1': (255, 181, 197), - 'pink2': (238, 169, 184), - 'pink3': (205, 145, 158), - 'pink4': (139, 99, 108), - 'plum': (221, 160, 221), - 'plum1': (255, 187, 255), - 'plum2': (238, 174, 238), - 'plum3': (205, 150, 205), - 'plum4': (139, 102, 139), - 'powderblue': (176, 224, 230), - 'purple': (160, 32, 240), - 'purple1': (155, 48, 255), - 'purple2': (145, 44, 238), - 'purple3': (125, 38, 205), - 'purple4': (85, 26, 139), - 'rebeccapurple': (102, 51, 153), - 'red': (255, 0, 0), - 'red1': (255, 0, 0), - 'red2': (238, 0, 0), - 'red3': (205, 0, 0), - 'red4': (139, 0, 0), - 'rosybrown': (188, 143, 143), - 'rosybrown1': (255, 193, 193), - 'rosybrown2': (238, 180, 180), - 'rosybrown3': (205, 155, 155), - 'rosybrown4': (139, 105, 105), - 'royalblue': (65, 105, 225), - 'royalblue1': (72, 118, 255), - 'royalblue2': (67, 110, 238), - 'royalblue3': (58, 95, 205), - 'royalblue4': (39, 64, 139), - 'saddlebrown': (139, 69, 19), - 'salmon': (250, 128, 114), - 'salmon1': (255, 140, 105), - 'salmon2': (238, 130, 98), - 'salmon3': (205, 112, 84), - 'salmon4': (139, 76, 57), - 'sandybrown': (244, 164, 96), - 'seagreen': (46, 139, 87), - 'seagreen1': (84, 255, 159), - 'seagreen2': (78, 238, 148), - 'seagreen3': (67, 205, 128), - 'seagreen4': (46, 139, 87), - 'seashell': (255, 245, 238), - 'seashell1': (255, 245, 238), - 'seashell2': (238, 229, 222), - 'seashell3': (205, 197, 191), - 'seashell4': (139, 134, 130), - 'sienna': (160, 82, 45), - 'sienna1': (255, 130, 71), - 'sienna2': (238, 121, 66), - 'sienna3': (205, 104, 57), - 'sienna4': (139, 71, 38), - 'silver': (192, 192, 192), - 'skyblue': (135, 206, 235), - 'skyblue1': (135, 206, 255), - 'skyblue2': (126, 192, 238), - 'skyblue3': (108, 166, 205), - 'skyblue4': (74, 112, 139), - 'slateblue': (106, 90, 205), - 'slateblue1': (131, 111, 255), - 'slateblue2': (122, 103, 238), - 'slateblue3': (105, 89, 205), - 'slateblue4': (71, 60, 139), - 'slategray': (112, 128, 144), - 'slategray1': (198, 226, 255), - 'slategray2': (185, 211, 238), - 'slategray3': (159, 182, 205), - 'slategray4': (108, 123, 139), - 'slategrey': (112, 128, 144), - 'snow': (255, 250, 250), - 'snow1': (255, 250, 250), - 'snow2': (238, 233, 233), - 'snow3': (205, 201, 201), - 'snow4': (139, 137, 137), - 'springgreen': (0, 255, 127), - 'springgreen1': (0, 255, 127), - 'springgreen2': (0, 238, 118), - 'springgreen3': (0, 205, 102), - 'springgreen4': (0, 139, 69), - 'steelblue': (70, 130, 180), - 'steelblue1': (99, 184, 255), - 'steelblue2': (92, 172, 238), - 'steelblue3': (79, 148, 205), - 'steelblue4': (54, 100, 139), - 'tan': (210, 180, 140), - 'tan1': (255, 165, 79), - 'tan2': (238, 154, 73), - 'tan3': (205, 133, 63), - 'tan4': (139, 90, 43), - 'teal': (0, 128, 128), - 'thistle': (216, 191, 216), - 'thistle1': (255, 225, 255), - 'thistle2': (238, 210, 238), - 'thistle3': (205, 181, 205), - 'thistle4': (139, 123, 139), - 'tomato': (255, 99, 71), - 'tomato1': (255, 99, 71), - 'tomato2': (238, 92, 66), - 'tomato3': (205, 79, 57), - 'tomato4': (139, 54, 38), - 'turquoise': (64, 224, 208), - 'turquoise1': (0, 245, 255), - 'turquoise2': (0, 229, 238), - 'turquoise3': (0, 197, 205), - 'turquoise4': (0, 134, 139), - 'violet': (238, 130, 238), - 'violetred': (208, 32, 144), - 'violetred1': (255, 62, 150), - 'violetred2': (238, 58, 140), - 'violetred3': (205, 50, 120), - 'violetred4': (139, 34, 82), - 'webgray': (128, 128, 128), - 'webgreen': (0, 128, 0), - 'webgrey': (128, 128, 128), - 'webmaroon': (128, 0, 0), - 'webpurple': (128, 0, 128), - 'wheat': (245, 222, 179), - 'wheat1': (255, 231, 186), - 'wheat2': (238, 216, 174), - 'wheat3': (205, 186, 150), - 'wheat4': (139, 126, 102), - 'white': (255, 255, 255), - 'whitesmoke': (245, 245, 245), - 'x11gray': (190, 190, 190), - 'x11green': (0, 255, 0), - 'x11grey': (190, 190, 190), - 'x11maroon': (176, 48, 96), - 'x11purple': (160, 32, 240), - 'yellow': (255, 255, 0), - 'yellow1': (255, 255, 0), - 'yellow2': (238, 238, 0), - 'yellow3': (205, 205, 0), - 'yellow4': (139, 139, 0), - 'yellowgreen': (154, 205, 50)} - - -class ColorTranslator(object): - """Translates RGB color to a color in the configured range.""" - - def __init__(self, num_colors=256): - """ - Initialize the translator. - - :arg int num_colors: The number of colors the terminal will suport - """ - if num_colors >= 256: - self.table = RGB_TO_256 - elif num_colors >= 88: - self.table = RGB_TO_88 - elif num_colors >= 16: - self.table = RGB_TO_16 - else: - self.table = RGB_TO_8 - - self.cache = {} - - def translate_color(self, rgb): - """ - Translate an RGB color to a color code in the configured color range. - - :arg tuple rgb: A tuple of 3 integers specifying an RGB color - :rtype: int - - This works by treating RGB colors as coordinates in three dimensional - space and finding the closest point within the configured color range - using the formula: - d^2 = (x2 - x1)^2 + (y2 - y1)^2 + (x2 - x1)^2 - - - """ - rtn = self.cache.get(rgb, None) - if rtn is None: - distance = closest = None - for code, rgb2 in self.table.values(): - new_distance = sqrt(sum(pow(rgb2[idx] - rgb[idx], 2) - for idx in range(3))) - if distance is None or new_distance < distance: - distance = new_distance - closest = code - - rtn = self.cache[rgb] = closest - - return rtn diff --git a/blessed/colorspace.py b/blessed/colorspace.py new file mode 100644 index 00000000..5e622c1b --- /dev/null +++ b/blessed/colorspace.py @@ -0,0 +1,954 @@ +""" +References, + +- https://github.com/freedesktop/xorg-rgb/blob/master/rgb.txt +- https://github.com/ThomasDickey/xterm-snapshots/blob/master/256colres.h +- https://github.com/ThomasDickey/xterm-snapshots/blob/master/XTerm-col.ad +- https://en.wikipedia.org/wiki/ANSI_escape_code#Colors +- https://gist.github.com/XVilka/8346728 +- https://devblogs.microsoft.com/commandline/24-bit-color-in-the-windows-console/ +- http://jdebp.eu/Softwares/nosh/guide/TerminalCapabilities.html +""" +import collections + +RGBColor = collections.namedtuple("RGBColor", ["red", "green", "blue"]) + +#: X11 Color names to (XTerm-defined) RGB values from xorg-rgb/rgb.txt +X11_COLORNAMES_TO_RGB = { + 'aliceblue': RGBColor(240, 248, 255), + 'antiquewhite': RGBColor(250, 235, 215), + 'antiquewhite1': RGBColor(255, 239, 219), + 'antiquewhite2': RGBColor(238, 223, 204), + 'antiquewhite3': RGBColor(205, 192, 176), + 'antiquewhite4': RGBColor(139, 131, 120), + 'aqua': RGBColor(0, 255, 255), + 'aquamarine': RGBColor(127, 255, 212), + 'aquamarine1': RGBColor(127, 255, 212), + 'aquamarine2': RGBColor(118, 238, 198), + 'aquamarine3': RGBColor(102, 205, 170), + 'aquamarine4': RGBColor(69, 139, 116), + 'azure': RGBColor(240, 255, 255), + 'azure1': RGBColor(240, 255, 255), + 'azure2': RGBColor(224, 238, 238), + 'azure3': RGBColor(193, 205, 205), + 'azure4': RGBColor(131, 139, 139), + 'beige': RGBColor(245, 245, 220), + 'bisque': RGBColor(255, 228, 196), + 'bisque1': RGBColor(255, 228, 196), + 'bisque2': RGBColor(238, 213, 183), + 'bisque3': RGBColor(205, 183, 158), + 'bisque4': RGBColor(139, 125, 107), + 'black': RGBColor(0, 0, 0), + 'blanchedalmond': RGBColor(255, 235, 205), + 'blue': RGBColor(0, 0, 255), + 'blue1': RGBColor(0, 0, 255), + 'blue2': RGBColor(0, 0, 238), + 'blue3': RGBColor(0, 0, 205), + 'blue4': RGBColor(0, 0, 139), + 'blueviolet': RGBColor(138, 43, 226), + 'brown': RGBColor(165, 42, 42), + 'brown1': RGBColor(255, 64, 64), + 'brown2': RGBColor(238, 59, 59), + 'brown3': RGBColor(205, 51, 51), + 'brown4': RGBColor(139, 35, 35), + 'burlywood': RGBColor(222, 184, 135), + 'burlywood1': RGBColor(255, 211, 155), + 'burlywood2': RGBColor(238, 197, 145), + 'burlywood3': RGBColor(205, 170, 125), + 'burlywood4': RGBColor(139, 115, 85), + 'cadetblue': RGBColor(95, 158, 160), + 'cadetblue1': RGBColor(152, 245, 255), + 'cadetblue2': RGBColor(142, 229, 238), + 'cadetblue3': RGBColor(122, 197, 205), + 'cadetblue4': RGBColor(83, 134, 139), + 'chartreuse': RGBColor(127, 255, 0), + 'chartreuse1': RGBColor(127, 255, 0), + 'chartreuse2': RGBColor(118, 238, 0), + 'chartreuse3': RGBColor(102, 205, 0), + 'chartreuse4': RGBColor(69, 139, 0), + 'chocolate': RGBColor(210, 105, 30), + 'chocolate1': RGBColor(255, 127, 36), + 'chocolate2': RGBColor(238, 118, 33), + 'chocolate3': RGBColor(205, 102, 29), + 'chocolate4': RGBColor(139, 69, 19), + 'coral': RGBColor(255, 127, 80), + 'coral1': RGBColor(255, 114, 86), + 'coral2': RGBColor(238, 106, 80), + 'coral3': RGBColor(205, 91, 69), + 'coral4': RGBColor(139, 62, 47), + 'cornflowerblue': RGBColor(100, 149, 237), + 'cornsilk': RGBColor(255, 248, 220), + 'cornsilk1': RGBColor(255, 248, 220), + 'cornsilk2': RGBColor(238, 232, 205), + 'cornsilk3': RGBColor(205, 200, 177), + 'cornsilk4': RGBColor(139, 136, 120), + 'crimson': RGBColor(220, 20, 60), + 'cyan': RGBColor(0, 255, 255), + 'cyan1': RGBColor(0, 255, 255), + 'cyan2': RGBColor(0, 238, 238), + 'cyan3': RGBColor(0, 205, 205), + 'cyan4': RGBColor(0, 139, 139), + 'darkblue': RGBColor(0, 0, 139), + 'darkcyan': RGBColor(0, 139, 139), + 'darkgoldenrod': RGBColor(184, 134, 11), + 'darkgoldenrod1': RGBColor(255, 185, 15), + 'darkgoldenrod2': RGBColor(238, 173, 14), + 'darkgoldenrod3': RGBColor(205, 149, 12), + 'darkgoldenrod4': RGBColor(139, 101, 8), + 'darkgray': RGBColor(169, 169, 169), + 'darkgreen': RGBColor(0, 100, 0), + 'darkgrey': RGBColor(169, 169, 169), + 'darkkhaki': RGBColor(189, 183, 107), + 'darkmagenta': RGBColor(139, 0, 139), + 'darkolivegreen': RGBColor(85, 107, 47), + 'darkolivegreen1': RGBColor(202, 255, 112), + 'darkolivegreen2': RGBColor(188, 238, 104), + 'darkolivegreen3': RGBColor(162, 205, 90), + 'darkolivegreen4': RGBColor(110, 139, 61), + 'darkorange': RGBColor(255, 140, 0), + 'darkorange1': RGBColor(255, 127, 0), + 'darkorange2': RGBColor(238, 118, 0), + 'darkorange3': RGBColor(205, 102, 0), + 'darkorange4': RGBColor(139, 69, 0), + 'darkorchid': RGBColor(153, 50, 204), + 'darkorchid1': RGBColor(191, 62, 255), + 'darkorchid2': RGBColor(178, 58, 238), + 'darkorchid3': RGBColor(154, 50, 205), + 'darkorchid4': RGBColor(104, 34, 139), + 'darkred': RGBColor(139, 0, 0), + 'darksalmon': RGBColor(233, 150, 122), + 'darkseagreen': RGBColor(143, 188, 143), + 'darkseagreen1': RGBColor(193, 255, 193), + 'darkseagreen2': RGBColor(180, 238, 180), + 'darkseagreen3': RGBColor(155, 205, 155), + 'darkseagreen4': RGBColor(105, 139, 105), + 'darkslateblue': RGBColor(72, 61, 139), + 'darkslategray': RGBColor(47, 79, 79), + 'darkslategray1': RGBColor(151, 255, 255), + 'darkslategray2': RGBColor(141, 238, 238), + 'darkslategray3': RGBColor(121, 205, 205), + 'darkslategray4': RGBColor(82, 139, 139), + 'darkslategrey': RGBColor(47, 79, 79), + 'darkturquoise': RGBColor(0, 206, 209), + 'darkviolet': RGBColor(148, 0, 211), + 'deeppink': RGBColor(255, 20, 147), + 'deeppink1': RGBColor(255, 20, 147), + 'deeppink2': RGBColor(238, 18, 137), + 'deeppink3': RGBColor(205, 16, 118), + 'deeppink4': RGBColor(139, 10, 80), + 'deepskyblue': RGBColor(0, 191, 255), + 'deepskyblue1': RGBColor(0, 191, 255), + 'deepskyblue2': RGBColor(0, 178, 238), + 'deepskyblue3': RGBColor(0, 154, 205), + 'deepskyblue4': RGBColor(0, 104, 139), + 'dimgray': RGBColor(105, 105, 105), + 'dimgrey': RGBColor(105, 105, 105), + 'dodgerblue': RGBColor(30, 144, 255), + 'dodgerblue1': RGBColor(30, 144, 255), + 'dodgerblue2': RGBColor(28, 134, 238), + 'dodgerblue3': RGBColor(24, 116, 205), + 'dodgerblue4': RGBColor(16, 78, 139), + 'firebrick': RGBColor(178, 34, 34), + 'firebrick1': RGBColor(255, 48, 48), + 'firebrick2': RGBColor(238, 44, 44), + 'firebrick3': RGBColor(205, 38, 38), + 'firebrick4': RGBColor(139, 26, 26), + 'floralwhite': RGBColor(255, 250, 240), + 'forestgreen': RGBColor(34, 139, 34), + 'fuchsia': RGBColor(255, 0, 255), + 'gainsboro': RGBColor(220, 220, 220), + 'ghostwhite': RGBColor(248, 248, 255), + 'gold': RGBColor(255, 215, 0), + 'gold1': RGBColor(255, 215, 0), + 'gold2': RGBColor(238, 201, 0), + 'gold3': RGBColor(205, 173, 0), + 'gold4': RGBColor(139, 117, 0), + 'goldenrod': RGBColor(218, 165, 32), + 'goldenrod1': RGBColor(255, 193, 37), + 'goldenrod2': RGBColor(238, 180, 34), + 'goldenrod3': RGBColor(205, 155, 29), + 'goldenrod4': RGBColor(139, 105, 20), + 'gray': RGBColor(190, 190, 190), + 'gray0': RGBColor(0, 0, 0), + 'gray1': RGBColor(3, 3, 3), + 'gray10': RGBColor(26, 26, 26), + 'gray100': RGBColor(255, 255, 255), + 'gray11': RGBColor(28, 28, 28), + 'gray12': RGBColor(31, 31, 31), + 'gray13': RGBColor(33, 33, 33), + 'gray14': RGBColor(36, 36, 36), + 'gray15': RGBColor(38, 38, 38), + 'gray16': RGBColor(41, 41, 41), + 'gray17': RGBColor(43, 43, 43), + 'gray18': RGBColor(46, 46, 46), + 'gray19': RGBColor(48, 48, 48), + 'gray2': RGBColor(5, 5, 5), + 'gray20': RGBColor(51, 51, 51), + 'gray21': RGBColor(54, 54, 54), + 'gray22': RGBColor(56, 56, 56), + 'gray23': RGBColor(59, 59, 59), + 'gray24': RGBColor(61, 61, 61), + 'gray25': RGBColor(64, 64, 64), + 'gray26': RGBColor(66, 66, 66), + 'gray27': RGBColor(69, 69, 69), + 'gray28': RGBColor(71, 71, 71), + 'gray29': RGBColor(74, 74, 74), + 'gray3': RGBColor(8, 8, 8), + 'gray30': RGBColor(77, 77, 77), + 'gray31': RGBColor(79, 79, 79), + 'gray32': RGBColor(82, 82, 82), + 'gray33': RGBColor(84, 84, 84), + 'gray34': RGBColor(87, 87, 87), + 'gray35': RGBColor(89, 89, 89), + 'gray36': RGBColor(92, 92, 92), + 'gray37': RGBColor(94, 94, 94), + 'gray38': RGBColor(97, 97, 97), + 'gray39': RGBColor(99, 99, 99), + 'gray4': RGBColor(10, 10, 10), + 'gray40': RGBColor(102, 102, 102), + 'gray41': RGBColor(105, 105, 105), + 'gray42': RGBColor(107, 107, 107), + 'gray43': RGBColor(110, 110, 110), + 'gray44': RGBColor(112, 112, 112), + 'gray45': RGBColor(115, 115, 115), + 'gray46': RGBColor(117, 117, 117), + 'gray47': RGBColor(120, 120, 120), + 'gray48': RGBColor(122, 122, 122), + 'gray49': RGBColor(125, 125, 125), + 'gray5': RGBColor(13, 13, 13), + 'gray50': RGBColor(127, 127, 127), + 'gray51': RGBColor(130, 130, 130), + 'gray52': RGBColor(133, 133, 133), + 'gray53': RGBColor(135, 135, 135), + 'gray54': RGBColor(138, 138, 138), + 'gray55': RGBColor(140, 140, 140), + 'gray56': RGBColor(143, 143, 143), + 'gray57': RGBColor(145, 145, 145), + 'gray58': RGBColor(148, 148, 148), + 'gray59': RGBColor(150, 150, 150), + 'gray6': RGBColor(15, 15, 15), + 'gray60': RGBColor(153, 153, 153), + 'gray61': RGBColor(156, 156, 156), + 'gray62': RGBColor(158, 158, 158), + 'gray63': RGBColor(161, 161, 161), + 'gray64': RGBColor(163, 163, 163), + 'gray65': RGBColor(166, 166, 166), + 'gray66': RGBColor(168, 168, 168), + 'gray67': RGBColor(171, 171, 171), + 'gray68': RGBColor(173, 173, 173), + 'gray69': RGBColor(176, 176, 176), + 'gray7': RGBColor(18, 18, 18), + 'gray70': RGBColor(179, 179, 179), + 'gray71': RGBColor(181, 181, 181), + 'gray72': RGBColor(184, 184, 184), + 'gray73': RGBColor(186, 186, 186), + 'gray74': RGBColor(189, 189, 189), + 'gray75': RGBColor(191, 191, 191), + 'gray76': RGBColor(194, 194, 194), + 'gray77': RGBColor(196, 196, 196), + 'gray78': RGBColor(199, 199, 199), + 'gray79': RGBColor(201, 201, 201), + 'gray8': RGBColor(20, 20, 20), + 'gray80': RGBColor(204, 204, 204), + 'gray81': RGBColor(207, 207, 207), + 'gray82': RGBColor(209, 209, 209), + 'gray83': RGBColor(212, 212, 212), + 'gray84': RGBColor(214, 214, 214), + 'gray85': RGBColor(217, 217, 217), + 'gray86': RGBColor(219, 219, 219), + 'gray87': RGBColor(222, 222, 222), + 'gray88': RGBColor(224, 224, 224), + 'gray89': RGBColor(227, 227, 227), + 'gray9': RGBColor(23, 23, 23), + 'gray90': RGBColor(229, 229, 229), + 'gray91': RGBColor(232, 232, 232), + 'gray92': RGBColor(235, 235, 235), + 'gray93': RGBColor(237, 237, 237), + 'gray94': RGBColor(240, 240, 240), + 'gray95': RGBColor(242, 242, 242), + 'gray96': RGBColor(245, 245, 245), + 'gray97': RGBColor(247, 247, 247), + 'gray98': RGBColor(250, 250, 250), + 'gray99': RGBColor(252, 252, 252), + 'green': RGBColor(0, 255, 0), + 'green1': RGBColor(0, 255, 0), + 'green2': RGBColor(0, 238, 0), + 'green3': RGBColor(0, 205, 0), + 'green4': RGBColor(0, 139, 0), + 'greenyellow': RGBColor(173, 255, 47), + 'grey': RGBColor(190, 190, 190), + 'grey0': RGBColor(0, 0, 0), + 'grey1': RGBColor(3, 3, 3), + 'grey10': RGBColor(26, 26, 26), + 'grey100': RGBColor(255, 255, 255), + 'grey11': RGBColor(28, 28, 28), + 'grey12': RGBColor(31, 31, 31), + 'grey13': RGBColor(33, 33, 33), + 'grey14': RGBColor(36, 36, 36), + 'grey15': RGBColor(38, 38, 38), + 'grey16': RGBColor(41, 41, 41), + 'grey17': RGBColor(43, 43, 43), + 'grey18': RGBColor(46, 46, 46), + 'grey19': RGBColor(48, 48, 48), + 'grey2': RGBColor(5, 5, 5), + 'grey20': RGBColor(51, 51, 51), + 'grey21': RGBColor(54, 54, 54), + 'grey22': RGBColor(56, 56, 56), + 'grey23': RGBColor(59, 59, 59), + 'grey24': RGBColor(61, 61, 61), + 'grey25': RGBColor(64, 64, 64), + 'grey26': RGBColor(66, 66, 66), + 'grey27': RGBColor(69, 69, 69), + 'grey28': RGBColor(71, 71, 71), + 'grey29': RGBColor(74, 74, 74), + 'grey3': RGBColor(8, 8, 8), + 'grey30': RGBColor(77, 77, 77), + 'grey31': RGBColor(79, 79, 79), + 'grey32': RGBColor(82, 82, 82), + 'grey33': RGBColor(84, 84, 84), + 'grey34': RGBColor(87, 87, 87), + 'grey35': RGBColor(89, 89, 89), + 'grey36': RGBColor(92, 92, 92), + 'grey37': RGBColor(94, 94, 94), + 'grey38': RGBColor(97, 97, 97), + 'grey39': RGBColor(99, 99, 99), + 'grey4': RGBColor(10, 10, 10), + 'grey40': RGBColor(102, 102, 102), + 'grey41': RGBColor(105, 105, 105), + 'grey42': RGBColor(107, 107, 107), + 'grey43': RGBColor(110, 110, 110), + 'grey44': RGBColor(112, 112, 112), + 'grey45': RGBColor(115, 115, 115), + 'grey46': RGBColor(117, 117, 117), + 'grey47': RGBColor(120, 120, 120), + 'grey48': RGBColor(122, 122, 122), + 'grey49': RGBColor(125, 125, 125), + 'grey5': RGBColor(13, 13, 13), + 'grey50': RGBColor(127, 127, 127), + 'grey51': RGBColor(130, 130, 130), + 'grey52': RGBColor(133, 133, 133), + 'grey53': RGBColor(135, 135, 135), + 'grey54': RGBColor(138, 138, 138), + 'grey55': RGBColor(140, 140, 140), + 'grey56': RGBColor(143, 143, 143), + 'grey57': RGBColor(145, 145, 145), + 'grey58': RGBColor(148, 148, 148), + 'grey59': RGBColor(150, 150, 150), + 'grey6': RGBColor(15, 15, 15), + 'grey60': RGBColor(153, 153, 153), + 'grey61': RGBColor(156, 156, 156), + 'grey62': RGBColor(158, 158, 158), + 'grey63': RGBColor(161, 161, 161), + 'grey64': RGBColor(163, 163, 163), + 'grey65': RGBColor(166, 166, 166), + 'grey66': RGBColor(168, 168, 168), + 'grey67': RGBColor(171, 171, 171), + 'grey68': RGBColor(173, 173, 173), + 'grey69': RGBColor(176, 176, 176), + 'grey7': RGBColor(18, 18, 18), + 'grey70': RGBColor(179, 179, 179), + 'grey71': RGBColor(181, 181, 181), + 'grey72': RGBColor(184, 184, 184), + 'grey73': RGBColor(186, 186, 186), + 'grey74': RGBColor(189, 189, 189), + 'grey75': RGBColor(191, 191, 191), + 'grey76': RGBColor(194, 194, 194), + 'grey77': RGBColor(196, 196, 196), + 'grey78': RGBColor(199, 199, 199), + 'grey79': RGBColor(201, 201, 201), + 'grey8': RGBColor(20, 20, 20), + 'grey80': RGBColor(204, 204, 204), + 'grey81': RGBColor(207, 207, 207), + 'grey82': RGBColor(209, 209, 209), + 'grey83': RGBColor(212, 212, 212), + 'grey84': RGBColor(214, 214, 214), + 'grey85': RGBColor(217, 217, 217), + 'grey86': RGBColor(219, 219, 219), + 'grey87': RGBColor(222, 222, 222), + 'grey88': RGBColor(224, 224, 224), + 'grey89': RGBColor(227, 227, 227), + 'grey9': RGBColor(23, 23, 23), + 'grey90': RGBColor(229, 229, 229), + 'grey91': RGBColor(232, 232, 232), + 'grey92': RGBColor(235, 235, 235), + 'grey93': RGBColor(237, 237, 237), + 'grey94': RGBColor(240, 240, 240), + 'grey95': RGBColor(242, 242, 242), + 'grey96': RGBColor(245, 245, 245), + 'grey97': RGBColor(247, 247, 247), + 'grey98': RGBColor(250, 250, 250), + 'grey99': RGBColor(252, 252, 252), + 'honeydew': RGBColor(240, 255, 240), + 'honeydew1': RGBColor(240, 255, 240), + 'honeydew2': RGBColor(224, 238, 224), + 'honeydew3': RGBColor(193, 205, 193), + 'honeydew4': RGBColor(131, 139, 131), + 'hotpink': RGBColor(255, 105, 180), + 'hotpink1': RGBColor(255, 110, 180), + 'hotpink2': RGBColor(238, 106, 167), + 'hotpink3': RGBColor(205, 96, 144), + 'hotpink4': RGBColor(139, 58, 98), + 'indianred': RGBColor(205, 92, 92), + 'indianred1': RGBColor(255, 106, 106), + 'indianred2': RGBColor(238, 99, 99), + 'indianred3': RGBColor(205, 85, 85), + 'indianred4': RGBColor(139, 58, 58), + 'indigo': RGBColor(75, 0, 130), + 'ivory': RGBColor(255, 255, 240), + 'ivory1': RGBColor(255, 255, 240), + 'ivory2': RGBColor(238, 238, 224), + 'ivory3': RGBColor(205, 205, 193), + 'ivory4': RGBColor(139, 139, 131), + 'khaki': RGBColor(240, 230, 140), + 'khaki1': RGBColor(255, 246, 143), + 'khaki2': RGBColor(238, 230, 133), + 'khaki3': RGBColor(205, 198, 115), + 'khaki4': RGBColor(139, 134, 78), + 'lavender': RGBColor(230, 230, 250), + 'lavenderblush': RGBColor(255, 240, 245), + 'lavenderblush1': RGBColor(255, 240, 245), + 'lavenderblush2': RGBColor(238, 224, 229), + 'lavenderblush3': RGBColor(205, 193, 197), + 'lavenderblush4': RGBColor(139, 131, 134), + 'lawngreen': RGBColor(124, 252, 0), + 'lemonchiffon': RGBColor(255, 250, 205), + 'lemonchiffon1': RGBColor(255, 250, 205), + 'lemonchiffon2': RGBColor(238, 233, 191), + 'lemonchiffon3': RGBColor(205, 201, 165), + 'lemonchiffon4': RGBColor(139, 137, 112), + 'lightblue': RGBColor(173, 216, 230), + 'lightblue1': RGBColor(191, 239, 255), + 'lightblue2': RGBColor(178, 223, 238), + 'lightblue3': RGBColor(154, 192, 205), + 'lightblue4': RGBColor(104, 131, 139), + 'lightcoral': RGBColor(240, 128, 128), + 'lightcyan': RGBColor(224, 255, 255), + 'lightcyan1': RGBColor(224, 255, 255), + 'lightcyan2': RGBColor(209, 238, 238), + 'lightcyan3': RGBColor(180, 205, 205), + 'lightcyan4': RGBColor(122, 139, 139), + 'lightgoldenrod': RGBColor(238, 221, 130), + 'lightgoldenrod1': RGBColor(255, 236, 139), + 'lightgoldenrod2': RGBColor(238, 220, 130), + 'lightgoldenrod3': RGBColor(205, 190, 112), + 'lightgoldenrod4': RGBColor(139, 129, 76), + 'lightgoldenrodyellow': RGBColor(250, 250, 210), + 'lightgray': RGBColor(211, 211, 211), + 'lightgreen': RGBColor(144, 238, 144), + 'lightgrey': RGBColor(211, 211, 211), + 'lightpink': RGBColor(255, 182, 193), + 'lightpink1': RGBColor(255, 174, 185), + 'lightpink2': RGBColor(238, 162, 173), + 'lightpink3': RGBColor(205, 140, 149), + 'lightpink4': RGBColor(139, 95, 101), + 'lightsalmon': RGBColor(255, 160, 122), + 'lightsalmon1': RGBColor(255, 160, 122), + 'lightsalmon2': RGBColor(238, 149, 114), + 'lightsalmon3': RGBColor(205, 129, 98), + 'lightsalmon4': RGBColor(139, 87, 66), + 'lightseagreen': RGBColor(32, 178, 170), + 'lightskyblue': RGBColor(135, 206, 250), + 'lightskyblue1': RGBColor(176, 226, 255), + 'lightskyblue2': RGBColor(164, 211, 238), + 'lightskyblue3': RGBColor(141, 182, 205), + 'lightskyblue4': RGBColor(96, 123, 139), + 'lightslateblue': RGBColor(132, 112, 255), + 'lightslategray': RGBColor(119, 136, 153), + 'lightslategrey': RGBColor(119, 136, 153), + 'lightsteelblue': RGBColor(176, 196, 222), + 'lightsteelblue1': RGBColor(202, 225, 255), + 'lightsteelblue2': RGBColor(188, 210, 238), + 'lightsteelblue3': RGBColor(162, 181, 205), + 'lightsteelblue4': RGBColor(110, 123, 139), + 'lightyellow': RGBColor(255, 255, 224), + 'lightyellow1': RGBColor(255, 255, 224), + 'lightyellow2': RGBColor(238, 238, 209), + 'lightyellow3': RGBColor(205, 205, 180), + 'lightyellow4': RGBColor(139, 139, 122), + 'lime': RGBColor(0, 255, 0), + 'limegreen': RGBColor(50, 205, 50), + 'linen': RGBColor(250, 240, 230), + 'magenta': RGBColor(255, 0, 255), + 'magenta1': RGBColor(255, 0, 255), + 'magenta2': RGBColor(238, 0, 238), + 'magenta3': RGBColor(205, 0, 205), + 'magenta4': RGBColor(139, 0, 139), + 'maroon': RGBColor(176, 48, 96), + 'maroon1': RGBColor(255, 52, 179), + 'maroon2': RGBColor(238, 48, 167), + 'maroon3': RGBColor(205, 41, 144), + 'maroon4': RGBColor(139, 28, 98), + 'mediumaquamarine': RGBColor(102, 205, 170), + 'mediumblue': RGBColor(0, 0, 205), + 'mediumorchid': RGBColor(186, 85, 211), + 'mediumorchid1': RGBColor(224, 102, 255), + 'mediumorchid2': RGBColor(209, 95, 238), + 'mediumorchid3': RGBColor(180, 82, 205), + 'mediumorchid4': RGBColor(122, 55, 139), + 'mediumpurple': RGBColor(147, 112, 219), + 'mediumpurple1': RGBColor(171, 130, 255), + 'mediumpurple2': RGBColor(159, 121, 238), + 'mediumpurple3': RGBColor(137, 104, 205), + 'mediumpurple4': RGBColor(93, 71, 139), + 'mediumseagreen': RGBColor(60, 179, 113), + 'mediumslateblue': RGBColor(123, 104, 238), + 'mediumspringgreen': RGBColor(0, 250, 154), + 'mediumturquoise': RGBColor(72, 209, 204), + 'mediumvioletred': RGBColor(199, 21, 133), + 'midnightblue': RGBColor(25, 25, 112), + 'mintcream': RGBColor(245, 255, 250), + 'mistyrose': RGBColor(255, 228, 225), + 'mistyrose1': RGBColor(255, 228, 225), + 'mistyrose2': RGBColor(238, 213, 210), + 'mistyrose3': RGBColor(205, 183, 181), + 'mistyrose4': RGBColor(139, 125, 123), + 'moccasin': RGBColor(255, 228, 181), + 'navajowhite': RGBColor(255, 222, 173), + 'navajowhite1': RGBColor(255, 222, 173), + 'navajowhite2': RGBColor(238, 207, 161), + 'navajowhite3': RGBColor(205, 179, 139), + 'navajowhite4': RGBColor(139, 121, 94), + 'navy': RGBColor(0, 0, 128), + 'navyblue': RGBColor(0, 0, 128), + 'oldlace': RGBColor(253, 245, 230), + 'olive': RGBColor(128, 128, 0), + 'olivedrab': RGBColor(107, 142, 35), + 'olivedrab1': RGBColor(192, 255, 62), + 'olivedrab2': RGBColor(179, 238, 58), + 'olivedrab3': RGBColor(154, 205, 50), + 'olivedrab4': RGBColor(105, 139, 34), + 'orange': RGBColor(255, 165, 0), + 'orange1': RGBColor(255, 165, 0), + 'orange2': RGBColor(238, 154, 0), + 'orange3': RGBColor(205, 133, 0), + 'orange4': RGBColor(139, 90, 0), + 'orangered': RGBColor(255, 69, 0), + 'orangered1': RGBColor(255, 69, 0), + 'orangered2': RGBColor(238, 64, 0), + 'orangered3': RGBColor(205, 55, 0), + 'orangered4': RGBColor(139, 37, 0), + 'orchid': RGBColor(218, 112, 214), + 'orchid1': RGBColor(255, 131, 250), + 'orchid2': RGBColor(238, 122, 233), + 'orchid3': RGBColor(205, 105, 201), + 'orchid4': RGBColor(139, 71, 137), + 'palegoldenrod': RGBColor(238, 232, 170), + 'palegreen': RGBColor(152, 251, 152), + 'palegreen1': RGBColor(154, 255, 154), + 'palegreen2': RGBColor(144, 238, 144), + 'palegreen3': RGBColor(124, 205, 124), + 'palegreen4': RGBColor(84, 139, 84), + 'paleturquoise': RGBColor(175, 238, 238), + 'paleturquoise1': RGBColor(187, 255, 255), + 'paleturquoise2': RGBColor(174, 238, 238), + 'paleturquoise3': RGBColor(150, 205, 205), + 'paleturquoise4': RGBColor(102, 139, 139), + 'palevioletred': RGBColor(219, 112, 147), + 'palevioletred1': RGBColor(255, 130, 171), + 'palevioletred2': RGBColor(238, 121, 159), + 'palevioletred3': RGBColor(205, 104, 137), + 'palevioletred4': RGBColor(139, 71, 93), + 'papayawhip': RGBColor(255, 239, 213), + 'peachpuff': RGBColor(255, 218, 185), + 'peachpuff1': RGBColor(255, 218, 185), + 'peachpuff2': RGBColor(238, 203, 173), + 'peachpuff3': RGBColor(205, 175, 149), + 'peachpuff4': RGBColor(139, 119, 101), + 'peru': RGBColor(205, 133, 63), + 'pink': RGBColor(255, 192, 203), + 'pink1': RGBColor(255, 181, 197), + 'pink2': RGBColor(238, 169, 184), + 'pink3': RGBColor(205, 145, 158), + 'pink4': RGBColor(139, 99, 108), + 'plum': RGBColor(221, 160, 221), + 'plum1': RGBColor(255, 187, 255), + 'plum2': RGBColor(238, 174, 238), + 'plum3': RGBColor(205, 150, 205), + 'plum4': RGBColor(139, 102, 139), + 'powderblue': RGBColor(176, 224, 230), + 'purple': RGBColor(160, 32, 240), + 'purple1': RGBColor(155, 48, 255), + 'purple2': RGBColor(145, 44, 238), + 'purple3': RGBColor(125, 38, 205), + 'purple4': RGBColor(85, 26, 139), + 'rebeccapurple': RGBColor(102, 51, 153), + 'red': RGBColor(255, 0, 0), + 'red1': RGBColor(255, 0, 0), + 'red2': RGBColor(238, 0, 0), + 'red3': RGBColor(205, 0, 0), + 'red4': RGBColor(139, 0, 0), + 'rosybrown': RGBColor(188, 143, 143), + 'rosybrown1': RGBColor(255, 193, 193), + 'rosybrown2': RGBColor(238, 180, 180), + 'rosybrown3': RGBColor(205, 155, 155), + 'rosybrown4': RGBColor(139, 105, 105), + 'royalblue': RGBColor(65, 105, 225), + 'royalblue1': RGBColor(72, 118, 255), + 'royalblue2': RGBColor(67, 110, 238), + 'royalblue3': RGBColor(58, 95, 205), + 'royalblue4': RGBColor(39, 64, 139), + 'saddlebrown': RGBColor(139, 69, 19), + 'salmon': RGBColor(250, 128, 114), + 'salmon1': RGBColor(255, 140, 105), + 'salmon2': RGBColor(238, 130, 98), + 'salmon3': RGBColor(205, 112, 84), + 'salmon4': RGBColor(139, 76, 57), + 'sandybrown': RGBColor(244, 164, 96), + 'seagreen': RGBColor(46, 139, 87), + 'seagreen1': RGBColor(84, 255, 159), + 'seagreen2': RGBColor(78, 238, 148), + 'seagreen3': RGBColor(67, 205, 128), + 'seagreen4': RGBColor(46, 139, 87), + 'seashell': RGBColor(255, 245, 238), + 'seashell1': RGBColor(255, 245, 238), + 'seashell2': RGBColor(238, 229, 222), + 'seashell3': RGBColor(205, 197, 191), + 'seashell4': RGBColor(139, 134, 130), + 'sienna': RGBColor(160, 82, 45), + 'sienna1': RGBColor(255, 130, 71), + 'sienna2': RGBColor(238, 121, 66), + 'sienna3': RGBColor(205, 104, 57), + 'sienna4': RGBColor(139, 71, 38), + 'silver': RGBColor(192, 192, 192), + 'skyblue': RGBColor(135, 206, 235), + 'skyblue1': RGBColor(135, 206, 255), + 'skyblue2': RGBColor(126, 192, 238), + 'skyblue3': RGBColor(108, 166, 205), + 'skyblue4': RGBColor(74, 112, 139), + 'slateblue': RGBColor(106, 90, 205), + 'slateblue1': RGBColor(131, 111, 255), + 'slateblue2': RGBColor(122, 103, 238), + 'slateblue3': RGBColor(105, 89, 205), + 'slateblue4': RGBColor(71, 60, 139), + 'slategray': RGBColor(112, 128, 144), + 'slategray1': RGBColor(198, 226, 255), + 'slategray2': RGBColor(185, 211, 238), + 'slategray3': RGBColor(159, 182, 205), + 'slategray4': RGBColor(108, 123, 139), + 'slategrey': RGBColor(112, 128, 144), + 'snow': RGBColor(255, 250, 250), + 'snow1': RGBColor(255, 250, 250), + 'snow2': RGBColor(238, 233, 233), + 'snow3': RGBColor(205, 201, 201), + 'snow4': RGBColor(139, 137, 137), + 'springgreen': RGBColor(0, 255, 127), + 'springgreen1': RGBColor(0, 255, 127), + 'springgreen2': RGBColor(0, 238, 118), + 'springgreen3': RGBColor(0, 205, 102), + 'springgreen4': RGBColor(0, 139, 69), + 'steelblue': RGBColor(70, 130, 180), + 'steelblue1': RGBColor(99, 184, 255), + 'steelblue2': RGBColor(92, 172, 238), + 'steelblue3': RGBColor(79, 148, 205), + 'steelblue4': RGBColor(54, 100, 139), + 'tan': RGBColor(210, 180, 140), + 'tan1': RGBColor(255, 165, 79), + 'tan2': RGBColor(238, 154, 73), + 'tan3': RGBColor(205, 133, 63), + 'tan4': RGBColor(139, 90, 43), + 'teal': RGBColor(0, 128, 128), + 'thistle': RGBColor(216, 191, 216), + 'thistle1': RGBColor(255, 225, 255), + 'thistle2': RGBColor(238, 210, 238), + 'thistle3': RGBColor(205, 181, 205), + 'thistle4': RGBColor(139, 123, 139), + 'tomato': RGBColor(255, 99, 71), + 'tomato1': RGBColor(255, 99, 71), + 'tomato2': RGBColor(238, 92, 66), + 'tomato3': RGBColor(205, 79, 57), + 'tomato4': RGBColor(139, 54, 38), + 'turquoise': RGBColor(64, 224, 208), + 'turquoise1': RGBColor(0, 245, 255), + 'turquoise2': RGBColor(0, 229, 238), + 'turquoise3': RGBColor(0, 197, 205), + 'turquoise4': RGBColor(0, 134, 139), + 'violet': RGBColor(238, 130, 238), + 'violetred': RGBColor(208, 32, 144), + 'violetred1': RGBColor(255, 62, 150), + 'violetred2': RGBColor(238, 58, 140), + 'violetred3': RGBColor(205, 50, 120), + 'violetred4': RGBColor(139, 34, 82), + 'webgray': RGBColor(128, 128, 128), + 'webgreen': RGBColor(0, 128, 0), + 'webgrey': RGBColor(128, 128, 128), + 'webmaroon': RGBColor(128, 0, 0), + 'webpurple': RGBColor(128, 0, 128), + 'wheat': RGBColor(245, 222, 179), + 'wheat1': RGBColor(255, 231, 186), + 'wheat2': RGBColor(238, 216, 174), + 'wheat3': RGBColor(205, 186, 150), + 'wheat4': RGBColor(139, 126, 102), + 'white': RGBColor(255, 255, 255), + 'whitesmoke': RGBColor(245, 245, 245), + 'x11gray': RGBColor(190, 190, 190), + 'x11green': RGBColor(0, 255, 0), + 'x11grey': RGBColor(190, 190, 190), + 'x11maroon': RGBColor(176, 48, 96), + 'x11purple': RGBColor(160, 32, 240), + 'yellow': RGBColor(255, 255, 0), + 'yellow1': RGBColor(255, 255, 0), + 'yellow2': RGBColor(238, 238, 0), + 'yellow3': RGBColor(205, 205, 0), + 'yellow4': RGBColor(139, 139, 0), + 'yellowgreen': RGBColor(154, 205, 50) +} + +#: Curses color indices of 8, 16, and 256-color terminals +RGB_256TABLE = ( + RGBColor(0, 0, 0), + RGBColor(205, 0, 0), + RGBColor(0, 205, 0), + RGBColor(205, 205, 0), + RGBColor(0, 0, 238), + RGBColor(205, 0, 205), + RGBColor(0, 205, 205), + RGBColor(229, 229, 229), + RGBColor(127, 127, 127), + RGBColor(255, 0, 0), + RGBColor(0, 255, 0), + RGBColor(255, 255, 0), + RGBColor(92, 92, 255), + RGBColor(255, 0, 255), + RGBColor(0, 255, 255), + RGBColor(255, 255, 255), + RGBColor(0, 0, 0), + RGBColor(0, 0, 95), + RGBColor(0, 0, 135), + RGBColor(0, 0, 175), + RGBColor(0, 0, 215), + RGBColor(0, 0, 255), + RGBColor(0, 95, 0), + RGBColor(0, 95, 95), + RGBColor(0, 95, 135), + RGBColor(0, 95, 175), + RGBColor(0, 95, 215), + RGBColor(0, 95, 255), + RGBColor(0, 135, 0), + RGBColor(0, 135, 95), + RGBColor(0, 135, 135), + RGBColor(0, 135, 175), + RGBColor(0, 135, 215), + RGBColor(0, 135, 255), + RGBColor(0, 175, 0), + RGBColor(0, 175, 95), + RGBColor(0, 175, 135), + RGBColor(0, 175, 175), + RGBColor(0, 175, 215), + RGBColor(0, 175, 255), + RGBColor(0, 215, 0), + RGBColor(0, 215, 95), + RGBColor(0, 215, 135), + RGBColor(0, 215, 175), + RGBColor(0, 215, 215), + RGBColor(0, 215, 255), + RGBColor(0, 255, 0), + RGBColor(0, 255, 95), + RGBColor(0, 255, 135), + RGBColor(0, 255, 175), + RGBColor(0, 255, 215), + RGBColor(0, 255, 255), + RGBColor(95, 0, 0), + RGBColor(95, 0, 95), + RGBColor(95, 0, 135), + RGBColor(95, 0, 175), + RGBColor(95, 0, 215), + RGBColor(95, 0, 255), + RGBColor(95, 95, 0), + RGBColor(95, 95, 95), + RGBColor(95, 95, 135), + RGBColor(95, 95, 175), + RGBColor(95, 95, 215), + RGBColor(95, 95, 255), + RGBColor(95, 135, 0), + RGBColor(95, 135, 95), + RGBColor(95, 135, 135), + RGBColor(95, 135, 175), + RGBColor(95, 135, 215), + RGBColor(95, 135, 255), + RGBColor(95, 175, 0), + RGBColor(95, 175, 95), + RGBColor(95, 175, 135), + RGBColor(95, 175, 175), + RGBColor(95, 175, 215), + RGBColor(95, 175, 255), + RGBColor(95, 215, 0), + RGBColor(95, 215, 95), + RGBColor(95, 215, 135), + RGBColor(95, 215, 175), + RGBColor(95, 215, 215), + RGBColor(95, 215, 255), + RGBColor(95, 255, 0), + RGBColor(95, 255, 95), + RGBColor(95, 255, 135), + RGBColor(95, 255, 175), + RGBColor(95, 255, 215), + RGBColor(95, 255, 255), + RGBColor(135, 0, 0), + RGBColor(135, 0, 95), + RGBColor(135, 0, 135), + RGBColor(135, 0, 175), + RGBColor(135, 0, 215), + RGBColor(135, 0, 255), + RGBColor(135, 95, 0), + RGBColor(135, 95, 95), + RGBColor(135, 95, 135), + RGBColor(135, 95, 175), + RGBColor(135, 95, 215), + RGBColor(135, 95, 255), + RGBColor(135, 135, 0), + RGBColor(135, 135, 95), + RGBColor(135, 135, 135), + RGBColor(135, 135, 175), + RGBColor(135, 135, 215), + RGBColor(135, 135, 255), + RGBColor(135, 175, 0), + RGBColor(135, 175, 95), + RGBColor(135, 175, 135), + RGBColor(135, 175, 175), + RGBColor(135, 175, 215), + RGBColor(135, 175, 255), + RGBColor(135, 215, 0), + RGBColor(135, 215, 95), + RGBColor(135, 215, 135), + RGBColor(135, 215, 175), + RGBColor(135, 215, 215), + RGBColor(135, 215, 255), + RGBColor(135, 255, 0), + RGBColor(135, 255, 95), + RGBColor(135, 255, 135), + RGBColor(135, 255, 175), + RGBColor(135, 255, 215), + RGBColor(135, 255, 255), + RGBColor(175, 0, 0), + RGBColor(175, 0, 95), + RGBColor(175, 0, 135), + RGBColor(175, 0, 175), + RGBColor(175, 0, 215), + RGBColor(175, 0, 255), + RGBColor(175, 95, 0), + RGBColor(175, 95, 95), + RGBColor(175, 95, 135), + RGBColor(175, 95, 175), + RGBColor(175, 95, 215), + RGBColor(175, 95, 255), + RGBColor(175, 135, 0), + RGBColor(175, 135, 95), + RGBColor(175, 135, 135), + RGBColor(175, 135, 175), + RGBColor(175, 135, 215), + RGBColor(175, 135, 255), + RGBColor(175, 175, 0), + RGBColor(175, 175, 95), + RGBColor(175, 175, 135), + RGBColor(175, 175, 175), + RGBColor(175, 175, 215), + RGBColor(175, 175, 255), + RGBColor(175, 215, 0), + RGBColor(175, 215, 95), + RGBColor(175, 215, 135), + RGBColor(175, 215, 175), + RGBColor(175, 215, 215), + RGBColor(175, 215, 255), + RGBColor(175, 255, 0), + RGBColor(175, 255, 95), + RGBColor(175, 255, 135), + RGBColor(175, 255, 175), + RGBColor(175, 255, 215), + RGBColor(175, 255, 255), + RGBColor(215, 0, 0), + RGBColor(215, 0, 95), + RGBColor(215, 0, 135), + RGBColor(215, 0, 175), + RGBColor(215, 0, 215), + RGBColor(215, 0, 255), + RGBColor(215, 95, 0), + RGBColor(215, 95, 95), + RGBColor(215, 95, 135), + RGBColor(215, 95, 175), + RGBColor(215, 95, 215), + RGBColor(215, 95, 255), + RGBColor(215, 135, 0), + RGBColor(215, 135, 95), + RGBColor(215, 135, 135), + RGBColor(215, 135, 175), + RGBColor(215, 135, 215), + RGBColor(215, 135, 255), + RGBColor(215, 175, 0), + RGBColor(215, 175, 95), + RGBColor(215, 175, 135), + RGBColor(215, 175, 175), + RGBColor(215, 175, 215), + RGBColor(215, 175, 255), + RGBColor(215, 215, 0), + RGBColor(215, 215, 95), + RGBColor(215, 215, 135), + RGBColor(215, 215, 175), + RGBColor(215, 215, 215), + RGBColor(215, 215, 255), + RGBColor(215, 255, 0), + RGBColor(215, 255, 95), + RGBColor(215, 255, 135), + RGBColor(215, 255, 175), + RGBColor(215, 255, 215), + RGBColor(215, 255, 255), + RGBColor(255, 0, 0), + RGBColor(255, 0, 135), + RGBColor(255, 0, 95), + RGBColor(255, 0, 175), + RGBColor(255, 0, 215), + RGBColor(255, 0, 255), + RGBColor(255, 95, 0), + RGBColor(255, 95, 95), + RGBColor(255, 95, 135), + RGBColor(255, 95, 175), + RGBColor(255, 95, 215), + RGBColor(255, 95, 255), + RGBColor(255, 135, 0), + RGBColor(255, 135, 95), + RGBColor(255, 135, 135), + RGBColor(255, 135, 175), + RGBColor(255, 135, 215), + RGBColor(255, 135, 255), + RGBColor(255, 175, 0), + RGBColor(255, 175, 95), + RGBColor(255, 175, 135), + RGBColor(255, 175, 175), + RGBColor(255, 175, 215), + RGBColor(255, 175, 255), + RGBColor(255, 215, 0), + RGBColor(255, 215, 95), + RGBColor(255, 215, 135), + RGBColor(255, 215, 175), + RGBColor(255, 215, 215), + RGBColor(255, 215, 255), + RGBColor(255, 255, 0), + RGBColor(255, 255, 95), + RGBColor(255, 255, 135), + RGBColor(255, 255, 175), + RGBColor(255, 255, 215), + RGBColor(255, 255, 255), + RGBColor(8, 8, 8), + RGBColor(18, 18, 18), + RGBColor(28, 28, 28), + RGBColor(38, 38, 38), + RGBColor(48, 48, 48), + RGBColor(58, 58, 58), + RGBColor(68, 68, 68), + RGBColor(78, 78, 78), + RGBColor(88, 88, 88), + RGBColor(98, 98, 98), + RGBColor(108, 108, 108), + RGBColor(118, 118, 118), + RGBColor(128, 128, 128), + RGBColor(138, 138, 138), + RGBColor(148, 148, 148), + RGBColor(158, 158, 158), + RGBColor(168, 168, 168), + RGBColor(178, 178, 178), + RGBColor(188, 188, 188), + RGBColor(198, 198, 198), + RGBColor(208, 208, 208), + RGBColor(218, 218, 218), + RGBColor(228, 228, 228), + RGBColor(238, 238, 238), +) diff --git a/blessed/formatters.py b/blessed/formatters.py index 998edefd..d857505b 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -1,6 +1,10 @@ """Sub-module providing sequence-formatting functions.""" # standard imports import platform +import math + +# local +from blessed.colorspace import X11_COLORNAMES_TO_RGB, RGB_256TABLE # 3rd-party import six @@ -11,6 +15,7 @@ else: import curses +CGA_COLORS = set(('black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white')) def _make_colors(): """ @@ -18,12 +23,18 @@ def _make_colors(): :rtype: set """ - derivatives = ('on', 'bright', 'on_bright',) - colors = set('black red green yellow blue magenta cyan white'.split()) - return set('_'.join((_deravitive, _color)) - for _deravitive in derivatives - for _color in colors) | colors - + colors = set() + def make_cga_compounds(cga_color): + return set( + ('on_' + cga_color, + 'bright_' + cga_color, + 'on_bright_' + cga_color)) + for color in CGA_COLORS: + colors.update(make_cga_compounds(color)) + colors.update(X11_COLORNAMES_TO_RGB) + for vga_color in X11_COLORNAMES_TO_RGB: + colors.add('on_' + vga_color) + return colors def _make_compoundables(colors): """ @@ -32,18 +43,16 @@ def _make_compoundables(colors): :arg set colors: set of color names as string. :rtype: set """ - _compoundables = set('bold underline reverse blink dim italic shadow ' - 'standout subscript superscript'.split()) + _compoundables = set('bold underline reverse blink italic standout'.split()) return colors | _compoundables - -#: Valid colors and their background (on), bright, -#: and bright-background derivatives. +#: Valid colors and their background (on), bright, and bright-background +#: derivatives. COLORS = _make_colors() -#: Attributes and colors which may be compounded by underscore. -COMPOUNDABLES = _make_compoundables(COLORS) - +#: Attributes that may be compounded with colors, by underscore, such as +#: 'reverse_indigo'. +COLORS_WITH_COMPOUNDABLES = _make_compoundables(COLORS) class ParameterizingString(six.text_type): r""" @@ -360,22 +369,42 @@ def resolve_color(term, color): if term.number_of_colors == 0: return NullCallableString() - # NOTE(erikrose): Does curses automatically exchange red and blue and cyan - # and yellow when a terminal supports setf/setb rather than setaf/setab? - # I'll be blasted if I can find any documentation. The following - # assumes it does: to terminfo(5) describes color(1) as COLOR_RED when - # using setaf, but COLOR_BLUE when using setf. - color_cap = (term._background_color if 'on_' in color else - term._foreground_color) - - # curses constants go up to only 7, so add an offset to get at the - # bright colors at 8-15: - offset = 8 if 'bright_' in color else 0 - base_color = color.rsplit('_', 1)[-1] + # fg/bg capabilities terminals that support 0-256+ colors. + vga_color_cap = (term._background_color if 'on_' in color else + term._foreground_color) - attr = 'COLOR_%s' % (base_color.upper(),) - fmt_attr = color_cap(getattr(curses, attr) + offset) - return FormattingString(fmt_attr, term.normal) + base_color = color.rsplit('_', 1)[-1] + if base_color in CGA_COLORS: + # curses constants go up to only 7, so add an offset to get at the + # bright colors at 8-15: + offset = 8 if 'bright_' in color else 0 + base_color = color.rsplit('_', 1)[-1] + attr = 'COLOR_%s' % (base_color.upper(),) + fmt_attr = vga_color_cap(getattr(curses, attr) + offset) + return FormattingString(fmt_attr, term.normal) + + assert base_color in X11_COLORNAMES_TO_RGB, ( + 'color not known', base_color) + rgb = X11_COLORNAMES_TO_RGB[base_color] + + # downconvert X11 colors to CGA, EGA, or VGA color spaces + if term.number_of_colors <= 256: + depth = term.number_of_colors + if depth == 88: + depth = 16 + assert depth in (0, 8, 16, 256), ( + 'Unexpected number_of_colors', term.number_of_colors) + fmt_attr = vga_color_cap(rgb_downconvert(*rgb, depth)) + return FormattingString(fmt_attr, term.normal) + + # Modern 24-bit color terminals are written pretty basically. The + # foreground and background sequences are: + # - ^[38;2;;;m + # - ^[48;2;;;m + fgbg_seq = ('48' if 'on_' in color else '38') + assert term.number_of_colors == 1 << 24 + fmt_attr = u'\x1b[' + fgbg_seq + ';2;{0};{1};{2}m' + return FormattingString(fmt_attr.format(*rgb), term.normal) def resolve_attribute(term, attr): @@ -406,7 +435,7 @@ def resolve_attribute(term, attr): # call for each compounding section, joined and returned as # a completed completed FormattingString. formatters = split_compound(attr) - if all(fmt in COMPOUNDABLES for fmt in formatters): + if all(fmt in COLORS_WITH_COMPOUNDABLES for fmt in formatters): resolution = (resolve_attribute(term, fmt) for fmt in formatters) return FormattingString(u''.join(resolution), term.normal) @@ -424,3 +453,49 @@ def resolve_attribute(term, attr): return proxy return ParameterizingString(tparm_capseq, term.normal, attr) + + +def rgb_downconvert(red, green, blue, depth): + """ + Translate an RGB color to a color code in the configured color depth. + + :arg red: RGB value of Red. + :arg green: RGB value of Green. + :arg blue: RGB value of Blue. + :rtype: int + + This works by treating RGB colors as coordinates in three dimensional + space and finding the closest point within the configured color range + using the formula:: + + d^2 = (r2 - r1)^2 + (g2 - g1)^2 + (b2 - b1)^2 + + For mapping of two sets of {r,g,b} color spaces. + """ + # NOTE(jquast): Color distance is a complex problem, but for our + # perspective the HSV colorspace is the most meaningfully matching, + # especially for "far" distances, this RGB may not make good sense. + # + # We would prioritize Hue (color) above Saturation (tones) or Value + # (lightness), or maybe HVS, but because our output is RGB, so is our + # internal values, and thus our calculation for now. + # + # I hope to make a kind of demo application that might suggest the + # difference, if any, to help ascertain the trade-off. + assert depth in (0, 8, 16, 256) + + # white(7) returns for depth 0. + color_idx = 7 + shortest_distance = None + for cmp_depth, cmp_rgb in enumerate(RGB_256TABLE): + # XXX TODO: Should this have 'abs'? pow(abs(cmp_rgb.red - red), 2) ? + cmp_distance = math.sqrt(sum(( + pow(cmp_rgb.red - red, 2), + pow(cmp_rgb.green - green, 2), + pow(cmp_rgb.blue - blue, 2)))) + if shortest_distance is None or cmp_distance < shortest_distance: + shortest_distance = cmp_distance + color_idx = cmp_depth + if cmp_depth == depth: + break + return color_idx diff --git a/blessed/terminal.py b/blessed/terminal.py index be0db988..13b6d7df 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -38,6 +38,7 @@ NullCallableString, resolve_capability, resolve_attribute, + COLORS, ) from ._capabilities import ( @@ -100,14 +101,13 @@ class Terminal(object): _sugar = dict( save='sc', restore='rc', - # 'clear' clears the whole screen. clear_eol='el', clear_bol='el1', clear_eos='ed', - position='cup', # deprecated enter_fullscreen='smcup', exit_fullscreen='rmcup', move='cup', + position='cup', move_x='hpa', move_y='vpa', move_left='cub1', @@ -226,6 +226,11 @@ def __init__(self, kind=None, stream=None, force_styling=False): if _CUR_TERM is None or self._kind == _CUR_TERM: _CUR_TERM = self._kind else: + # termcap 'kind' is immutable in a python process! Once + # initialized by setupterm, it is unsupported by the + # 'curses' module to change the terminal type again. If you + # are a downstream developer and you need this + # functionality, consider sub-processing, instead. warnings.warn( 'A terminal of kind "%s" has been requested; due to an' ' internal python curses bug, terminal capabilities' @@ -233,17 +238,20 @@ def __init__(self, kind=None, stream=None, force_styling=False): ' returned for the remainder of this process.' % ( self._kind, _CUR_TERM,)) - if self._does_styling: - colorterm = os.environ.get('COLORTERM', None) - self._truecolor = (colorterm in ('truecolor', '24bit') - or platform.system() == 'Windows') - else: - self._truecolor = False - - # initialize capabilities and terminal keycodes database + self.__init__color_capabilities() self.__init__capabilities() self.__init__keycodes() + def __init__color_capabilities(self): + if not self.does_styling: + self.number_of_colors = 0 + elif platform.system() == 'Windows' or ( + os.environ.get('COLORTERM') in ('truecolor', '24bit') + ): + self.number_of_colors = 1 << 24 + else: + self.number_of_colors = max(0, curses.tigetnum('colors') or -1) + def __init__capabilities(self): # important that we lay these in their ordered direction, so that our # preferred, 'color' over 'set_a_attributes1', for example. @@ -347,7 +355,11 @@ def __getattr__(self, attr): val = resolve_attribute(self, attr) # Cache capability resolution: note this will prevent this # __getattr__ method for being called again. That's the idea! - setattr(self, attr, val) + if attr not in COLORS: + # TODO XXX temporarily(?), we don't cache these for colors, so that + # we can see the dang results when we dynamically change + # 'number_of_colors' at runtime. + setattr(self, attr, val) return val @property @@ -692,25 +704,19 @@ def stream(self): @property def number_of_colors(self): """ - Read-only property: number of colors supported by terminal. - - Common values are 0, 8, 16, 88, and 256. + Number of colors supported by terminal. - Most commonly, this may be used to test whether the terminal supports - colors. Though the underlying capability returns -1 when there is no - color support, we return 0. This lets you test more Pythonically:: + Common return values are 0, 8, 16, 256, or 1 << 24. - if term.number_of_colors: - ... + This may be used to test whether the terminal supports colors, + and at what depth, if that's a concern. """ - # This is actually the only remotely useful numeric capability. We - # don't name it after the underlying capability, because we deviate - # slightly from its behavior, and we might someday wish to give direct - # access to it. + return self._number_of_colors - # trim value to 0, as tigetnum('colors') returns -1 if no support, - # and -2 if no such capability. - return max(0, self.does_styling and curses.tigetnum('colors') or -1) + @number_of_colors.setter + def number_of_colors(self, value): + assert value in (0, 8, 16, 256, 1 << 24) + self._number_of_colors = value @property def _foreground_color(self): diff --git a/docs/history.rst b/docs/history.rst index 8f1a813d..bf1238f4 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -1,5 +1,10 @@ Version History =============== +1.17 + * deprecated: "compoundable" with ``superscript``, ``subscript``, or + ``shadow``, or ``dim``, such as in phrase ``Terminal.blue_subscript('a')``. + Use Unicode text or 256 or 24-bit color codes instead. + 1.16 * Windows support?! :ghissue:`110` by :ghuser:`avylove`. diff --git a/docs/overview.rst b/docs/overview.rst index 4a87660e..d4224b95 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -59,7 +59,7 @@ for more complex strings:: Capabilities ~~~~~~~~~~~~ -The basic capabilities supported by most terminals are: +Capabilities supported by most terminals are: ``bold`` Turn on 'extra bright' mode. @@ -69,37 +69,8 @@ The basic capabilities supported by most terminals are: Turn on blinking. ``normal`` Reset attributes to default. - -The less commonly supported capabilities: - -``dim`` - Enable half-bright mode. ``underline`` Enable underline mode. -``no_underline`` - Exit underline mode. -``italic`` - Enable italicized text. -``no_italic`` - Exit italics. -``shadow`` - Enable shadow text mode (rare). -``no_shadow`` - Exit shadow text mode. -``standout`` - Enable standout mode (often, an alias for ``reverse``). -``no_standout`` - Exit standout mode. -``subscript`` - Enable subscript mode. -``no_subscript`` - Exit subscript mode. -``superscript`` - Enable superscript mode. -``no_superscript`` - Exit superscript mode. -``flash`` - Visual bell, flashes the screen. Note that, while the inverse of *underline* is *no_underline*, the only way to turn off *bold* or *reverse* is *normal*, which also cancels any custom @@ -108,13 +79,20 @@ colors. Many of these are aliases, their true capability names (such as 'smul' for 'begin underline mode') may still be used. Any capability in the `terminfo(5)`_ manual, under column **Cap-name**, may be used as an attribute of a -:class:`~.Terminal` instance. If it is not a supported capability, or a non-tty -is used as an output stream, an empty string is returned. - +:class:`~.Terminal` instance. If it is not a supported capability for the +current terminal, or a non-tty is used as output stream, an empty string is +always returned. Colors ~~~~~~ +XXX TODO XXX + +all X11 colors are available, +rgb(int, int, int), +truecolor is automatically detected, +or, downsampled to 256 or 16, 8, etc colorspace. + Color terminals are capable of at least 8 basic colors. * ``black`` @@ -181,7 +159,7 @@ on amber or green* on monochrome terminals. Whereas the more declarative formatter *black_on_green* would remain colorless. .. note:: On most color terminals, *bright_black* is not invisible -- it is - actually a very dark shade of gray! + actually a very dark shade of grey! Compound Formatting ~~~~~~~~~~~~~~~~~~~ diff --git a/docs/pains.rst b/docs/pains.rst index 629a67ed..ac7454d6 100644 --- a/docs/pains.rst +++ b/docs/pains.rst @@ -9,6 +9,15 @@ you from several considerations shared here. 8 and 16 colors --------------- +XXX + +https://en.wikipedia.org/wiki/ANSI_escape_code#3/4_bit + +As a curses library, I think we should safely assume to map our colorspace +to rgb values that match xterm. + +XXX + Where 8 and 16 colors are used, they should be assumed to be the `CGA Color Palette`_. Though there is no terminal standard that proclaims that the CGA colors are used, their values are the best approximations From 83fbabf77550b041bc45fd4dc36d2af39da1e993 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Thu, 9 Jan 2020 22:04:54 -0800 Subject: [PATCH 406/459] allocating hash heaps --- blessed/formatters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blessed/formatters.py b/blessed/formatters.py index d857505b..cb055e84 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -427,7 +427,7 @@ def resolve_attribute(term, attr): return resolve_color(term, attr) # A direct compoundable, such as `bold' or `on_red'. - if attr in COMPOUNDABLES: + if attr in COLORS_WITH_COMPOUNDABLES: sequence = resolve_capability(term, attr) return FormattingString(sequence, term.normal) From 449f7d636059c8cb44df1091fe35d792928f65bc Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 10 Jan 2020 17:54:36 -0800 Subject: [PATCH 407/459] add color_rgb and on_color_rgb(), and bin/plasma.py demo --- blessed/formatters.py | 36 +++++++++++++---------------------- blessed/terminal.py | 44 +++++++++++++++++++++++++++++++++---------- 2 files changed, 47 insertions(+), 33 deletions(-) diff --git a/blessed/formatters.py b/blessed/formatters.py index cb055e84..eb5e95ad 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -24,35 +24,27 @@ def _make_colors(): :rtype: set """ colors = set() - def make_cga_compounds(cga_color): - return set( - ('on_' + cga_color, - 'bright_' + cga_color, - 'on_bright_' + cga_color)) - for color in CGA_COLORS: - colors.update(make_cga_compounds(color)) - colors.update(X11_COLORNAMES_TO_RGB) + # basic CGA foreground color, background, high intensity, and bold + # background ('iCE colors' in my day). + for cga_color in CGA_COLORS: + colors.add(cga_color) + colors.add('on_' + cga_color) + colors.add('bright_' + cga_color) + colors.add('on_bright_' + cga_color) + + # foreground and background VGA color for vga_color in X11_COLORNAMES_TO_RGB: + colors.add(vga_color) colors.add('on_' + vga_color) return colors -def _make_compoundables(colors): - """ - Return given set ``colors`` along with all "compoundable" attributes. - - :arg set colors: set of color names as string. - :rtype: set - """ - _compoundables = set('bold underline reverse blink italic standout'.split()) - return colors | _compoundables - #: Valid colors and their background (on), bright, and bright-background #: derivatives. COLORS = _make_colors() #: Attributes that may be compounded with colors, by underscore, such as #: 'reverse_indigo'. -COLORS_WITH_COMPOUNDABLES = _make_compoundables(COLORS) +COMPOUNDABLES = set('bold underline reverse blink italic standout'.split()) class ParameterizingString(six.text_type): r""" @@ -427,7 +419,7 @@ def resolve_attribute(term, attr): return resolve_color(term, attr) # A direct compoundable, such as `bold' or `on_red'. - if attr in COLORS_WITH_COMPOUNDABLES: + if attr in COMPOUNDABLES: sequence = resolve_capability(term, attr) return FormattingString(sequence, term.normal) @@ -435,7 +427,7 @@ def resolve_attribute(term, attr): # call for each compounding section, joined and returned as # a completed completed FormattingString. formatters = split_compound(attr) - if all(fmt in COLORS_WITH_COMPOUNDABLES for fmt in formatters): + if all(fmt in (COLORS | COMPOUNDABLES) for fmt in formatters): resolution = (resolve_attribute(term, fmt) for fmt in formatters) return FormattingString(u''.join(resolution), term.normal) @@ -482,8 +474,6 @@ def rgb_downconvert(red, green, blue, depth): # # I hope to make a kind of demo application that might suggest the # difference, if any, to help ascertain the trade-off. - assert depth in (0, 8, 16, 256) - # white(7) returns for depth 0. color_idx = 7 shortest_distance = None diff --git a/blessed/terminal.py b/blessed/terminal.py index 13b6d7df..729efc11 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -38,7 +38,9 @@ NullCallableString, resolve_capability, resolve_attribute, - COLORS, + FormattingString, + rgb_downconvert, + COLORS, ) from ._capabilities import ( @@ -252,6 +254,10 @@ def __init__color_capabilities(self): else: self.number_of_colors = max(0, curses.tigetnum('colors') or -1) + def __clear_color_capabilities(self): + for cached_color_cap in set(dir(self)) & COLORS: + delattr(self, cached_color_cap) + def __init__capabilities(self): # important that we lay these in their ordered direction, so that our # preferred, 'color' over 'set_a_attributes1', for example. @@ -341,7 +347,7 @@ def __getattr__(self, attr): >>> term.bold_blink_red_on_green("merry x-mas!") - For a parametrized capability such as ``move`` (or ``cup``), pass the + For a parameterized capability such as ``move`` (or ``cup``), pass the parameters as positional arguments:: >>> term.move(line, column) @@ -352,14 +358,13 @@ def __getattr__(self, attr): """ if not self.does_styling: return NullCallableString() + # Fetch the missing 'attribute' into some kind of curses-resolved + # capability, and cache by attaching to this Terminal class instance. + # + # Note that this will prevent future calls to __getattr__(), but + # that's precisely the idea of the cache! val = resolve_attribute(self, attr) - # Cache capability resolution: note this will prevent this - # __getattr__ method for being called again. That's the idea! - if attr not in COLORS: - # TODO XXX temporarily(?), we don't cache these for colors, so that - # we can see the dang results when we dynamically change - # 'number_of_colors' at runtime. - setattr(self, attr, val) + setattr(self, attr, val) return val @property @@ -661,6 +666,16 @@ def color(self): return ParameterizingString(self._foreground_color, self.normal, 'color') + def color_rgb(self, red, green, blue): + if self.number_of_colors == 1 << 24: + # "truecolor" 24-bit + fmt_attr = u'\x1b[38;2;{0};{1};{2}m'.format(red, green, blue) + return FormattingString(fmt_attr, self.normal) + + # color by approximation to 256 or 16-color terminals + color_idx = rgb_downconvert(red, green, blue, depth=self.number_of_colors) + return FormattingString(self._foreground_color(color_idx), self.normal) + @property def on_color(self): """ @@ -674,6 +689,14 @@ def on_color(self): return ParameterizingString(self._background_color, self.normal, 'on_color') + def on_color_rgb(self, red, green, blue): + if self.number_of_colors == 1 << 24: + fmt_attr = u'\x1b[48;2;{0};{1};{2}m'.format(red, green, blue) + return FormattingString(fmt_attr, self.normal) + + color_idx = rgb_downconvert(red, green, blue, depth=self.number_of_colors) + return FormattingString(self._background_color(color_idx), self.normal) + @property def normal(self): """ @@ -715,8 +738,9 @@ def number_of_colors(self): @number_of_colors.setter def number_of_colors(self, value): - assert value in (0, 8, 16, 256, 1 << 24) + assert value in (0, 4, 8, 16, 256, 1 << 24) self._number_of_colors = value + self.__clear_color_capabilities() @property def _foreground_color(self): From 7478243f3af1effd1a3850f214fa4a02267f8053 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 10 Jan 2020 18:01:16 -0800 Subject: [PATCH 408/459] add bin/plasma.py --- bin/plasma.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100755 bin/plasma.py diff --git a/bin/plasma.py b/bin/plasma.py new file mode 100755 index 00000000..8c6386d0 --- /dev/null +++ b/bin/plasma.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +import math +import colorsys +import time +import sys +import blessed +# todo: if we use unicode shaded blocks, we can do monotone/single color + +def plasma (term): + result = '' + for y in range(term.height): + for x in range (term.width): + hue = (4.0 + + math.sin((x + (time.time() * 5)) / 19.0) + + math.sin((y + (time.time() * 5)) / 9.0) + + math.sin((x + y) / 25.0) + + math.sin(math.sqrt(x**2.0 + y**2.0) / 8.0)) + rgb = colorsys.hsv_to_rgb(hue / 8.0, 1, 1) + xyz = int(round(rgb[0]*255)), int(round(rgb[1]*255)), int(round(rgb[2]*255)) + result += term.on_color_rgb(*xyz) + ' ' + return result + +if __name__=="__main__": + term = blessed.Terminal() + with term.cbreak(), term.hidden_cursor(): + while True: + print(term.home + plasma(term), end='') + sys.stdout.flush() + inp = term.inkey(timeout=0.2) + if inp == '\x09': + term.number_of_colors = { + 4: 8, + 8: 16, + 16: 256, + 256: 1 << 24, + 1 << 24: 4, + }[term.number_of_colors] From 11bc75340e21d0ae279c43e9f92e8d75d6e277b0 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 10 Jan 2020 19:30:44 -0800 Subject: [PATCH 409/459] doc about backspace <- delete --- docs/history.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/history.rst b/docs/history.rst index bf1238f4..d5c29dd6 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -1,6 +1,8 @@ Version History =============== 1.17 + * bugfix: ``chr(127)``, ``\x7f`` has changed from keycode ``term.DELETE`` + to ``term.BACKSPACE``, :ghissue:115` by :ghuser:`jwezel`. * deprecated: "compoundable" with ``superscript``, ``subscript``, or ``shadow``, or ``dim``, such as in phrase ``Terminal.blue_subscript('a')``. Use Unicode text or 256 or 24-bit color codes instead. From 430b9f5e1f62a431b0e4bcd823ed4c6629f87484 Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Fri, 10 Jan 2020 23:37:04 -0500 Subject: [PATCH 410/459] Additional color algorithms --- blessed/color.py | 186 ++++++++++++++++++++++++++++++++++++++++++ blessed/formatters.py | 18 ++-- 2 files changed, 191 insertions(+), 13 deletions(-) create mode 100644 blessed/color.py diff --git a/blessed/color.py b/blessed/color.py new file mode 100644 index 00000000..1b31743e --- /dev/null +++ b/blessed/color.py @@ -0,0 +1,186 @@ +""" +Sub-module providing color functions. + +References, +- https://en.wikipedia.org/wiki/Color_difference +- http://www.easyrgb.com/en/math.php +- Measuring Colour by R.W.G. Hunt and M.R. Pointer + +""" + +from math import sqrt + + +def rgb_to_xyz(red, green, blue): + """ + Convert standard RGB color to XYZ color. + + :arg red: RGB value of Red. + :arg green: RGB value of Green. + :arg blue: RGB value of Blue. + :returns: Tuple (X, Y, Z) representing XYZ color + :rtype: tuple + + D65/2° standard illuminant + """ + rgb = [] + for val in red, green, blue: + val /= 255 + if val > 0.04045: + val = pow((val + 0.055) / 1.055, 2.4) + else: + val /= 12.92 + val *= 100 + rgb.append(val) + + red, green, blue = rgb # pylint: disable=unbalanced-tuple-unpacking + x_val = red * 0.4124 + green * 0.3576 + blue * 0.1805 + y_val = red * 0.2126 + green * 0.7152 + blue * 0.0722 + z_val = red * 0.0193 + green * 0.1192 + blue * 0.9505 + + return x_val, y_val, z_val + + +def xyz_to_lab(x_val, y_val, z_val): + """ + Convert XYZ color to CIE-Lab color. + + :arg x_val: XYZ value of X. + :arg y_val: XYZ value of Y. + :arg z_val: XYZ value of Z. + :returns: Tuple (L, a, b) representing CIE-Lab color + :rtype: tuple + + D65/2° standard illuminant + """ + xyz = [] + for val, ref in (x_val, 95.047), (y_val, 100.0), (z_val, 108.883): + val /= ref + if val > 0.008856: + val = pow(val, 1 / 3) + else: + val = 7.787 * val + 16 / 116 + xyz.append(val) + + x_val, y_val, z_val = xyz # pylint: disable=unbalanced-tuple-unpacking + cie_l = 116 * y_val - 16 + cie_a = 500 * (x_val - y_val) + cie_b = 200 * (y_val - z_val) + + return cie_l, cie_a, cie_b + + +def rgb_to_lab(red, green, blue): + """ + Convert RGB color to CIE-Lab color. + + :arg red: RGB value of Red. + :arg green: RGB value of Green. + :arg blue: RGB value of Blue. + :returns: Tuple (L, a, b) representing CIE-Lab color + :rtype: tuple + + D65/2° standard illuminant + """ + return xyz_to_lab(*rgb_to_xyz(red, green, blue)) + + +def dist_rgb(rgb1, rgb2): + """ + Determine distance between two rgb colors. + + :arg tuple rgb1: RGB color definition + :arg tuple rgb2: RGB color definition + :returns: Square of the distance between provided colors + :rtype: float + + This works by treating RGB colors as coordinates in three dimensional + space and finding the closest point within the configured color range + using the formula:: + + d^2 = (r2 - r1)^2 + (g2 - g1)^2 + (b2 - b1)^2 + + For efficiency, the square of the distance is returned + which is sufficient for comparisons + """ + return sum(pow(rgb1[idx] - rgb2[idx], 2) for idx in (0, 1, 2)) + + +def dist_rgb_weighted(rgb1, rgb2): + """ + Determine the weighted distance between two rgb colors. + + :arg tuple rgb1: RGB color definition + :arg tuple rgb2: RGB color definition + :returns: Square of the distance between provided colors + :rtype: float + + Similar to a standard distance formula, the values are weighted + to approximate human perception of color differences + + For efficiency, the square of the distance is returned + which is sufficient for comparisons + """ + red_mean = (rgb1[0] + rgb2[0]) / 2 + + return ((2 + red_mean / 256) * pow(rgb1[0] - rgb2[0], 2) + + 4 * pow(rgb1[1] - rgb2[1], 2) + + (2 + (255 - red_mean) / 256) * pow(rgb1[2] - rgb2[2], 2)) + + +def dist_cie76(rgb1, rgb2): + """ + Determine distance between two rgb colors using the CIE94 algorithm. + + :arg tuple rgb1: RGB color definition + :arg tuple rgb2: RGB color definition + :returns: Square of the distance between provided colors + :rtype: float + + For efficiency, the square of the distance is returned + which is sufficient for comparisons + """ + l_1, a_1, b_1 = rgb_to_lab(*rgb1) + l_2, a_2, b_2 = rgb_to_lab(*rgb2) + return pow(l_1 - l_2, 2) + pow(a_1 - a_2, 2) + pow(b_1 - b_2, 2) + + +def dist_cie94(rgb1, rgb2): + # pylint: disable=too-many-locals + """ + Determine distance between two rgb colors using the CIE94 algorithm. + + :arg tuple rgb1: RGB color definition + :arg tuple rgb2: RGB color definition + :returns: Square of the distance between provided colors + :rtype: float + + For efficiency, the square of the distance is returned + which is sufficient for comparisons + """ + l_1, a_1, b_1 = rgb_to_lab(*rgb1) + l_2, a_2, b_2 = rgb_to_lab(*rgb2) + + s_l = k_l = k_c = k_h = 1 + k_1 = 0.045 + k_2 = 0.015 + + delta_l = l_1 - l_2 + delta_a = a_1 - a_2 + delta_b = b_1 - b_2 + c_1 = sqrt(a_1 ** 2 + b_1 ** 2) + c_2 = sqrt(a_2 ** 2 + b_2 ** 2) + delta_c = c_1 - c_2 + delta_h = sqrt(delta_a ** 2 + delta_b ** 2 + delta_c ** 2) + s_c = 1 + k_1 * c_1 + s_h = 1 + k_2 * c_1 + + return ((delta_l / (k_l * s_l)) ** 2 + + (delta_c / (k_c * s_c)) ** 2 + + (delta_h / (k_h * s_h)) ** 2) + + +COLOR_ALGORITHMS = {'rgb': dist_rgb, + 'rgb-weighted': dist_rgb_weighted, + 'cie76': dist_cie76, + 'cie94': dist_cie94} diff --git a/blessed/formatters.py b/blessed/formatters.py index eb5e95ad..14e48db6 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -1,9 +1,9 @@ """Sub-module providing sequence-formatting functions.""" # standard imports import platform -import math # local +from blessed.color import COLOR_ALGORITHMS from blessed.colorspace import X11_COLORNAMES_TO_RGB, RGB_256TABLE # 3rd-party @@ -447,7 +447,7 @@ def resolve_attribute(term, attr): return ParameterizingString(tparm_capseq, term.normal, attr) -def rgb_downconvert(red, green, blue, depth): +def rgb_downconvert(red, green, blue, depth, algorithm='cie94'): """ Translate an RGB color to a color code in the configured color depth. @@ -456,12 +456,6 @@ def rgb_downconvert(red, green, blue, depth): :arg blue: RGB value of Blue. :rtype: int - This works by treating RGB colors as coordinates in three dimensional - space and finding the closest point within the configured color range - using the formula:: - - d^2 = (r2 - r1)^2 + (g2 - g1)^2 + (b2 - b1)^2 - For mapping of two sets of {r,g,b} color spaces. """ # NOTE(jquast): Color distance is a complex problem, but for our @@ -475,14 +469,12 @@ def rgb_downconvert(red, green, blue, depth): # I hope to make a kind of demo application that might suggest the # difference, if any, to help ascertain the trade-off. # white(7) returns for depth 0. + + get_distance = COLOR_ALGORITHMS[algorithm] color_idx = 7 shortest_distance = None for cmp_depth, cmp_rgb in enumerate(RGB_256TABLE): - # XXX TODO: Should this have 'abs'? pow(abs(cmp_rgb.red - red), 2) ? - cmp_distance = math.sqrt(sum(( - pow(cmp_rgb.red - red, 2), - pow(cmp_rgb.green - green, 2), - pow(cmp_rgb.blue - blue, 2)))) + cmp_distance = get_distance(cmp_rgb, (red, green, blue)) if shortest_distance is None or cmp_distance < shortest_distance: shortest_distance = cmp_distance color_idx = cmp_depth From 80ca7c33f220f24dfe78cd493500b8decfeddb6e Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Fri, 10 Jan 2020 23:37:24 -0500 Subject: [PATCH 411/459] Add colorchart.py --- bin/colorchart.py | 90 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 bin/colorchart.py diff --git a/bin/colorchart.py b/bin/colorchart.py new file mode 100644 index 00000000..f83eb71f --- /dev/null +++ b/bin/colorchart.py @@ -0,0 +1,90 @@ +import re + +import blessed +from blessed.colorspace import X11_COLORNAMES_TO_RGB + + +RE_NATURAL = re.compile(r'(dark|light|)(.+?)(\d*)$') + + +def naturalize(string): + + intensity, word, num = RE_NATURAL.match(string).groups() + + if intensity == 'light': + intensity = -1 + elif intensity == 'medium': + intensity = 1 + elif intensity == 'dark': + intensity = 2 + else: + intensity = 0 + + return word, intensity, int(num) if num else 0 + + +def color_table(term): + + output = {} + for color, code in X11_COLORNAMES_TO_RGB.items(): + + if code in output: + output[code] = '%s %s' % (output[code], color) + continue + + chart = '' + for noc in (1 << 24, 256, 16, 8): + term.number_of_colors = noc + chart += getattr(term, color)(u'█') + + output[code] = '%s %s' % (chart, color) + + for color in sorted(X11_COLORNAMES_TO_RGB, key=naturalize): + code = X11_COLORNAMES_TO_RGB[color] + if code in output: + print(output.pop(code)) + + +def color_chart(term): + + output = {} + for color, code in X11_COLORNAMES_TO_RGB.items(): + + if code in output: + continue + + chart = '' + for noc in (1 << 24, 256, 16, 8): + term.number_of_colors = noc + chart += getattr(term, color)(u'█') + + output[code] = chart + + width = term.width + + line = '' + line_len = 0 + for color in sorted(X11_COLORNAMES_TO_RGB, key=naturalize): + code = X11_COLORNAMES_TO_RGB[color] + if code in output: + chart = output.pop(code) + if line_len + 5 > width: + print(line) + line = '' + line_len = 0 + + line += ' %s' % chart + line_len += 5 + + print(line) + + for color in sorted(X11_COLORNAMES_TO_RGB, key=naturalize): + code = X11_COLORNAMES_TO_RGB[color] + if code in output: + print(output.pop(code)) + + +if __name__ == '__main__': + + # color_table(blessed.Terminal()) + color_chart(blessed.Terminal()) From 14ba720d76829514449d2cc1de590d48e21346ad Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 11 Jan 2020 13:57:42 -0800 Subject: [PATCH 412/459] move rgb_downcovert to Terminal method and let the algorithms be exposed as an attribute, plasma.py can cycle algorithms. --- bin/plasma.py | 126 +++++++++++++++++++++++++++++++++--------- blessed/color.py | 8 +-- blessed/colorspace.py | 10 ++++ blessed/formatters.py | 48 +--------------- blessed/terminal.py | 39 ++++++++++++- 5 files changed, 153 insertions(+), 78 deletions(-) diff --git a/bin/plasma.py b/bin/plasma.py index 8c6386d0..7fb93eb7 100755 --- a/bin/plasma.py +++ b/bin/plasma.py @@ -1,37 +1,113 @@ #!/usr/bin/env python import math import colorsys +import collections +import contextlib +import timeit import time import sys import blessed -# todo: if we use unicode shaded blocks, we can do monotone/single color - -def plasma (term): +scale_255 = lambda val: int(round(val * 255)) + +def rgb_at_xy(term, x, y, t): + h, w = term.height, term.width + hue = 4.0 + ( + math.sin(x / 16.0) + + math.sin(y / 32.0) + + math.sin(math.sqrt( + ((x - w / 2.0) * (x - w / 2.0) + + (y - h / 2.0) * (y - h / 2.0)) + ) / 8.0 + t*3) + ) + math.sin(math.sqrt((x * x + y * y)) / 8.0) + saturation = 1-y / h + lightness = 1-x / w + return tuple(map(scale_255, colorsys.hsv_to_rgb(hue / 8.0, saturation, lightness))) + +def screen_plasma(term, plasma_fn, t): result = '' - for y in range(term.height): + for y in range(term.height - 1): for x in range (term.width): - hue = (4.0 - + math.sin((x + (time.time() * 5)) / 19.0) - + math.sin((y + (time.time() * 5)) / 9.0) - + math.sin((x + y) / 25.0) - + math.sin(math.sqrt(x**2.0 + y**2.0) / 8.0)) - rgb = colorsys.hsv_to_rgb(hue / 8.0, 1, 1) - xyz = int(round(rgb[0]*255)), int(round(rgb[1]*255)), int(round(rgb[2]*255)) - result += term.on_color_rgb(*xyz) + ' ' + result += term.on_color_rgb(*plasma_fn(term, x, y, t)) + ' ' return result + +@contextlib.contextmanager +def elapsed_timer(): + """Timer pattern, from https://stackoverflow.com/a/30024601.""" + start = timeit.default_timer() + + def elapser(): + return timeit.default_timer() - start + + # pylint: disable=unnecessary-lambda + yield lambda: elapser() -if __name__=="__main__": - term = blessed.Terminal() +def please_wait(term): + txt_wait = 'please wait ...' + outp = term.move(term.height-1, 0) + term.clear_eol + term.center(txt_wait) + print(outp, end='') + sys.stdout.flush() + +def paused(term): + txt_paused = 'paused' + outp = term.move(term.height-1, int(term.width/2 - len(txt_paused)/2)) + outp += txt_paused + print(outp, end='') + sys.stdout.flush() + +def next_algo(algo, forward): + from blessed.color import COLOR_DISTANCE_ALGORITHMS + algos = tuple(sorted(COLOR_DISTANCE_ALGORITHMS)) + next_index = algos.index(algo) + (1 if forward else -1) + if next_index == len(algos): + next_index = 0 + return algos[next_index] + +def next_color(color, forward): + colorspaces = (4, 8, 16, 256, 1 << 24) + next_index = colorspaces.index(color) + (1 if forward else -1) + if next_index == len(colorspaces): + next_index = 0 + return colorspaces[next_index] + +def status(term, elapsed): + left_txt = (f'{term.number_of_colors} colors - ' + f'{term.color_distance_algorithm} - ?: help ') + right_txt = f'fps: {1 / elapsed:2.2f}' + return ('\n' + term.normal + + term.white_on_blue + left_txt + + term.rjust(right_txt, term.width-len(left_txt))) + +def main(term): with term.cbreak(), term.hidden_cursor(): + pause = False + t = time.time() while True: - print(term.home + plasma(term), end='') - sys.stdout.flush() - inp = term.inkey(timeout=0.2) - if inp == '\x09': - term.number_of_colors = { - 4: 8, - 8: 16, - 16: 256, - 256: 1 << 24, - 1 << 24: 4, - }[term.number_of_colors] + if not pause or dirty: + if not pause: + t = time.time() + with elapsed_timer() as elapsed: + outp = term.home + screen_plasma(term, rgb_at_xy, t) + outp += status(term, elapsed()) + print(outp, end='') + sys.stdout.flush() + dirty = False + if paused: + paused(term) + + inp = term.inkey(timeout=0.01 if not pause else None) + if inp == '?': assert False, "don't panic" + if inp == '\x0c': dirty = True + if inp in ('[', ']'): + term.color_distance_algorithm = next_algo( + term.color_distance_algorithm, inp == '[') + please_wait(term) + dirty = True + if inp == ' ': pause = not pause + if inp.code in (term.KEY_TAB, term.KEY_BTAB): + term.number_of_colors = next_color( + term.number_of_colors, inp.code==term.KEY_TAB) + please_wait(term) + dirty = True + +if __name__ == "__main__": + exit(main(blessed.Terminal())) diff --git a/blessed/color.py b/blessed/color.py index 1b31743e..59384569 100644 --- a/blessed/color.py +++ b/blessed/color.py @@ -180,7 +180,7 @@ def dist_cie94(rgb1, rgb2): (delta_h / (k_h * s_h)) ** 2) -COLOR_ALGORITHMS = {'rgb': dist_rgb, - 'rgb-weighted': dist_rgb_weighted, - 'cie76': dist_cie76, - 'cie94': dist_cie94} +COLOR_DISTANCE_ALGORITHMS = {'rgb': dist_rgb, + 'rgb-weighted': dist_rgb_weighted, + 'cie76': dist_cie76, + 'cie94': dist_cie94} diff --git a/blessed/colorspace.py b/blessed/colorspace.py index 5e622c1b..9b7c493f 100644 --- a/blessed/colorspace.py +++ b/blessed/colorspace.py @@ -11,6 +11,16 @@ """ import collections +__all__ = ( + 'CGA_COLORS', + 'RGBColor', + 'RGB_256TABLE', + 'X11_COLORNAMES_TO_RGB', +) + +CGA_COLORS = set( + ('black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white')) + RGBColor = collections.namedtuple("RGBColor", ["red", "green", "blue"]) #: X11 Color names to (XTerm-defined) RGB values from xorg-rgb/rgb.txt diff --git a/blessed/formatters.py b/blessed/formatters.py index 14e48db6..629f37db 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -3,8 +3,7 @@ import platform # local -from blessed.color import COLOR_ALGORITHMS -from blessed.colorspace import X11_COLORNAMES_TO_RGB, RGB_256TABLE +from blessed.colorspace import X11_COLORNAMES_TO_RGB, CGA_COLORS # 3rd-party import six @@ -15,8 +14,6 @@ else: import curses -CGA_COLORS = set(('black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white')) - def _make_colors(): """ Return set of valid colors and their derivatives. @@ -381,12 +378,7 @@ def resolve_color(term, color): # downconvert X11 colors to CGA, EGA, or VGA color spaces if term.number_of_colors <= 256: - depth = term.number_of_colors - if depth == 88: - depth = 16 - assert depth in (0, 8, 16, 256), ( - 'Unexpected number_of_colors', term.number_of_colors) - fmt_attr = vga_color_cap(rgb_downconvert(*rgb, depth)) + fmt_attr = vga_color_cap(term.rgb_downconvert(*rgb)) return FormattingString(fmt_attr, term.normal) # Modern 24-bit color terminals are written pretty basically. The @@ -445,39 +437,3 @@ def resolve_attribute(term, attr): return proxy return ParameterizingString(tparm_capseq, term.normal, attr) - - -def rgb_downconvert(red, green, blue, depth, algorithm='cie94'): - """ - Translate an RGB color to a color code in the configured color depth. - - :arg red: RGB value of Red. - :arg green: RGB value of Green. - :arg blue: RGB value of Blue. - :rtype: int - - For mapping of two sets of {r,g,b} color spaces. - """ - # NOTE(jquast): Color distance is a complex problem, but for our - # perspective the HSV colorspace is the most meaningfully matching, - # especially for "far" distances, this RGB may not make good sense. - # - # We would prioritize Hue (color) above Saturation (tones) or Value - # (lightness), or maybe HVS, but because our output is RGB, so is our - # internal values, and thus our calculation for now. - # - # I hope to make a kind of demo application that might suggest the - # difference, if any, to help ascertain the trade-off. - # white(7) returns for depth 0. - - get_distance = COLOR_ALGORITHMS[algorithm] - color_idx = 7 - shortest_distance = None - for cmp_depth, cmp_rgb in enumerate(RGB_256TABLE): - cmp_distance = get_distance(cmp_rgb, (red, green, blue)) - if shortest_distance is None or cmp_distance < shortest_distance: - shortest_distance = cmp_distance - color_idx = cmp_depth - if cmp_depth == depth: - break - return color_idx diff --git a/blessed/terminal.py b/blessed/terminal.py index 729efc11..556dd1b0 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -39,7 +39,6 @@ resolve_capability, resolve_attribute, FormattingString, - rgb_downconvert, COLORS, ) @@ -62,6 +61,9 @@ _time_left, ) +from .color import COLOR_DISTANCE_ALGORITHMS +from .colorspace import RGB_256TABLE + if platform.system() == 'Windows': import jinxed as curses # pylint: disable=import-error HAS_TTY = True @@ -139,6 +141,11 @@ class Terminal(object): terminal_enquire='u9', ) + #: Color distance algorithm used by :meth:`rgb_downconvert`. + #: The slowest, but most accurate, 'cie94', is default. Other + #: available options are 'rgb', 'rgb-weighted', and 'cie76'. + color_distance_algorithm = 'cie94' + def __init__(self, kind=None, stream=None, force_styling=False): """ Initialize the terminal. @@ -673,7 +680,7 @@ def color_rgb(self, red, green, blue): return FormattingString(fmt_attr, self.normal) # color by approximation to 256 or 16-color terminals - color_idx = rgb_downconvert(red, green, blue, depth=self.number_of_colors) + color_idx = self.rgb_downconvert(red, green, blue) return FormattingString(self._foreground_color(color_idx), self.normal) @property @@ -694,9 +701,35 @@ def on_color_rgb(self, red, green, blue): fmt_attr = u'\x1b[48;2;{0};{1};{2}m'.format(red, green, blue) return FormattingString(fmt_attr, self.normal) - color_idx = rgb_downconvert(red, green, blue, depth=self.number_of_colors) + color_idx = self.rgb_downconvert(red, green, blue) return FormattingString(self._background_color(color_idx), self.normal) + def rgb_downconvert(self, red, green, blue): + """ + Translate an RGB color to a color code of the terminal's color depth. + + :arg int red: RGB value of Red (0-255). + :arg int green: RGB value of Green (0-255). + :arg int blue: RGB value of Blue (0-255). + :rtype: int """ + # Though pre-computing all 1 << 24 options is memory-intensive, a pre-computed + # "k-d tree" of 256 (x,y,z) vectors of a colorspace in 3 dimensions, such as a + # cone of HSV, or simply 255x255x255 RGB square, any given rgb value is just a + # nearest-neighbor search of 256 points, which k-d should be much faster by + # sub-dividing / culling search points, rather than our "search all 256 points + # always" approach. + fn_distance = COLOR_DISTANCE_ALGORITHMS[self.color_distance_algorithm] + color_idx = 7 + shortest_distance = None + for cmp_depth, cmp_rgb in enumerate(RGB_256TABLE): + cmp_distance = fn_distance(cmp_rgb, (red, green, blue)) + if shortest_distance is None or cmp_distance < shortest_distance: + shortest_distance = cmp_distance + color_idx = cmp_depth + if cmp_depth >= self.number_of_colors: + break + return color_idx + @property def normal(self): """ From 772b5d9098cdd8a157b9601cdab406ba02e3b64e Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 11 Jan 2020 16:56:50 -0800 Subject: [PATCH 413/459] make color_distance_algorithm a setter make a setter, because we need to flush our colors from cache again, like the others. anyway, made an x11 colorpicker. --- bin/plasma.py | 7 ++-- bin/x11_colorpicker.py | 79 ++++++++++++++++++++++++++++++++++++++++++ blessed/colorspace.py | 4 ++- blessed/terminal.py | 32 +++++++++++++---- docs/history.rst | 3 ++ 5 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 bin/x11_colorpicker.py diff --git a/bin/plasma.py b/bin/plasma.py index 7fb93eb7..a66e231a 100755 --- a/bin/plasma.py +++ b/bin/plasma.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# std imports import math import colorsys import collections @@ -6,7 +7,10 @@ import timeit import time import sys + +# local import blessed + scale_255 = lambda val: int(round(val * 255)) def rgb_at_xy(term, x, y, t): @@ -55,8 +59,7 @@ def paused(term): sys.stdout.flush() def next_algo(algo, forward): - from blessed.color import COLOR_DISTANCE_ALGORITHMS - algos = tuple(sorted(COLOR_DISTANCE_ALGORITHMS)) + algos = tuple(sorted(blessed.color.COLOR_DISTANCE_ALGORITHMS)) next_index = algos.index(algo) + (1 if forward else -1) if next_index == len(algos): next_index = 0 diff --git a/bin/x11_colorpicker.py b/bin/x11_colorpicker.py new file mode 100644 index 00000000..473bcf42 --- /dev/null +++ b/bin/x11_colorpicker.py @@ -0,0 +1,79 @@ +import colorsys +import blessed + +hsv_sorted_colors = sorted( + blessed.colorspace.X11_COLORNAMES_TO_RGB.items(), + key=lambda rgb: colorsys.rgb_to_hsv(*rgb[1]), + reverse=True) + +def render(term, idx): + color_name, rgb_color = hsv_sorted_colors[idx] + result = term.home + term.normal + ''.join( + getattr(term, hsv_sorted_colors[i][0]) + '◼' + for i in range(len(hsv_sorted_colors)) + ) + result += term.clear_eos+ '\n' + result += getattr(term, 'on_' + color_name) + term.clear_eos + '\n' + result += term.normal + term.center(f'{color_name}: {rgb_color}') + '\n' + result += term.normal + term.center( + f'{term.number_of_colors} colors - ' + f'{term.color_distance_algorithm}') + + result += term.move(idx // term.width, idx % term.width) + result += term.on_color_rgb(*rgb_color)(' \b') + return result + +def next_algo(algo, forward): + algos = tuple(sorted(blessed.color.COLOR_DISTANCE_ALGORITHMS)) + next_index = algos.index(algo) + (1 if forward else -1) + if next_index == len(algos): + next_index = 0 + return algos[next_index] + + +def next_color(color, forward): + colorspaces = (4, 8, 16, 256, 1 << 24) + next_index = colorspaces.index(color) + (1 if forward else -1) + if next_index == len(colorspaces): + next_index = 0 + return colorspaces[next_index] + + +def main(): + term = blessed.Terminal() + with term.cbreak(), term.hidden_cursor(): + idx = len(hsv_sorted_colors) // 2 + dirty = True + while True: + if dirty: + outp = render(term, idx) + with term.hidden_cursor(): + print(outp, end='', flush=True) + inp = term.inkey() + dirty = True + if inp.code == term.KEY_LEFT or inp == 'h': + idx -= 1 + elif inp.code == term.KEY_DOWN or inp == 'j': + idx += term.width + elif inp.code == term.KEY_UP or inp == 'k': + idx -= term.width + elif inp.code == term.KEY_RIGHT or inp == 'l': + idx += 1 + elif inp.code in (term.KEY_TAB, term.KEY_BTAB): + term.number_of_colors = next_color( + term.number_of_colors, inp.code==term.KEY_TAB) + elif inp in ('[', ']'): + term.color_distance_algorithm = next_algo( + term.color_distance_algorithm, inp == '[') + elif inp == '\x0c': + pass + else: + dirty = False + + while idx < 0: + idx += len(hsv_sorted_colors) + while idx >= len(hsv_sorted_colors): + idx -= len(hsv_sorted_colors) + +if __name__ == '__main__': + main() diff --git a/blessed/colorspace.py b/blessed/colorspace.py index 9b7c493f..c40a41f7 100644 --- a/blessed/colorspace.py +++ b/blessed/colorspace.py @@ -21,7 +21,9 @@ CGA_COLORS = set( ('black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white')) -RGBColor = collections.namedtuple("RGBColor", ["red", "green", "blue"]) +class RGBColor(collections.namedtuple("RGBColor", ["red", "green", "blue"])): + def __str__(self): + return '#{0:02x}{1:02x}{2:02x}'.format(*self) #: X11 Color names to (XTerm-defined) RGB values from xorg-rgb/rgb.txt X11_COLORNAMES_TO_RGB = { diff --git a/blessed/terminal.py b/blessed/terminal.py index 556dd1b0..4046db6b 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -39,7 +39,7 @@ resolve_capability, resolve_attribute, FormattingString, - COLORS, + COLORS, ) from ._capabilities import ( @@ -141,11 +141,6 @@ class Terminal(object): terminal_enquire='u9', ) - #: Color distance algorithm used by :meth:`rgb_downconvert`. - #: The slowest, but most accurate, 'cie94', is default. Other - #: available options are 'rgb', 'rgb-weighted', and 'cie76'. - color_distance_algorithm = 'cie94' - def __init__(self, kind=None, stream=None, force_styling=False): """ Initialize the terminal. @@ -252,6 +247,7 @@ def __init__(self, kind=None, stream=None, force_styling=False): self.__init__keycodes() def __init__color_capabilities(self): + self._color_distance_algorithm = 'cie94' if not self.does_styling: self.number_of_colors = 0 elif platform.system() == 'Windows' or ( @@ -632,10 +628,12 @@ def fullscreen(self): entered at a time. """ self.stream.write(self.enter_fullscreen) + self.stream.flush() try: yield finally: self.stream.write(self.exit_fullscreen) + self.stream.flush() @contextlib.contextmanager def hidden_cursor(self): @@ -649,10 +647,12 @@ def hidden_cursor(self): should be entered at a time. """ self.stream.write(self.hide_cursor) + self.stream.flush() try: yield finally: self.stream.write(self.normal_cursor) + self.stream.flush() @property def color(self): @@ -775,6 +775,24 @@ def number_of_colors(self, value): self._number_of_colors = value self.__clear_color_capabilities() + + @property + def color_distance_algorithm(self): + """ + Color distance algorithm used by :meth:`rgb_downconvert`. + + The slowest, but most accurate, 'cie94', is default. Other + available options are 'rgb', 'rgb-weighted', and 'cie76'. + """ + return self._color_distance_algorithm + + @color_distance_algorithm.setter + def color_distance_algorithm(self, value): + assert value in COLOR_DISTANCE_ALGORITHMS + self._color_distance_algorithm = value + self.__clear_color_capabilities() + + @property def _foreground_color(self): """ @@ -1145,9 +1163,11 @@ def keypad(self): """ try: self.stream.write(self.smkx) + self.stream.flush() yield finally: self.stream.write(self.rmkx) + self.stream.flush() def inkey(self, timeout=None, esc_delay=0.35, **_kwargs): """ diff --git a/docs/history.rst b/docs/history.rst index d5c29dd6..eb3f4451 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -1,6 +1,9 @@ Version History =============== 1.17 + * bugfix: Context Managers, :meth:`~.Terminal.fullscreen`, + :meth:`~.Terminal.hidden_cursor`, and :meth:`~Terminal.keypad` + now flush the stream after writing their sequences. * bugfix: ``chr(127)``, ``\x7f`` has changed from keycode ``term.DELETE`` to ``term.BACKSPACE``, :ghissue:115` by :ghuser:`jwezel`. * deprecated: "compoundable" with ``superscript``, ``subscript``, or From b73652131a22dd35068f7311d271820b1ec9ff3b Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 11 Jan 2020 17:20:27 -0800 Subject: [PATCH 414/459] small bugfix in example --- bin/progress_bar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/progress_bar.py b/bin/progress_bar.py index 16522c58..0bbca0fe 100755 --- a/bin/progress_bar.py +++ b/bin/progress_bar.py @@ -25,7 +25,7 @@ def main(): inp = None print("press 'X' to stop.") sys.stderr.write(term.move(term.height, 0) + u'[') - sys.stderr.write(term.move_x(term.width) + u']' + term.move_x(1)) + sys.stderr.write(term.move_x(term.width - 1) + u']' + term.move_x(1)) while inp != 'X': if col >= (term.width - 2): offset = -1 From 614a9620af56eac3eefc497bf4a1649bccb1b8dd Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 11 Jan 2020 17:51:29 -0800 Subject: [PATCH 415/459] remove _inject_curses_keynames() --- blessed/keyboard.py | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/blessed/keyboard.py b/blessed/keyboard.py index c48ee519..3cec3104 100644 --- a/blessed/keyboard.py +++ b/blessed/keyboard.py @@ -315,40 +315,6 @@ def _read_until(term, pattern, timeout): return match, buf -def _inject_curses_keynames(): - r""" - Inject KEY_NAMES that we think would be useful into the curses module. - - This function compliments the global constant - :obj:`DEFAULT_SEQUENCE_MIXIN`. It is important to note that this - function has the side-effect of **injecting** new attributes to the - curses module, and is called from the global namespace at time of - import. - - Though we may determine *keynames* and codes for keyboard input that - generate multibyte sequences, it is also especially useful to aliases - a few basic ASCII characters such as ``KEY_TAB`` instead of ``u'\t'`` for - uniformity. - - Furthermore, many key-names for application keys enabled only by context - manager :meth:`~.Terminal.keypad` are surprisingly absent. We inject them - here directly into the curses module. - - It is not necessary to directly "monkeypatch" the curses module to - contain these constants, as they will also be accessible as attributes - of the Terminal class instance, they are provided only for convenience - when mixed in with other curses code. - """ - _lastval = max(get_curses_keycodes().values()) - for key in ('TAB', 'KP_MULTIPLY', 'KP_ADD', 'KP_SEPARATOR', 'KP_SUBTRACT', - 'KP_DECIMAL', 'KP_DIVIDE', 'KP_EQUAL', 'KP_0', 'KP_1', 'KP_2', - 'KP_3', 'KP_4', 'KP_5', 'KP_6', 'KP_7', 'KP_8', 'KP_9'): - _lastval += 1 - setattr(curses, 'KEY_{0}'.format(key), _lastval) - - -_inject_curses_keynames() - #: In a perfect world, terminal emulators would always send exactly what #: the terminfo(5) capability database plans for them, accordingly by the #: value of the ``TERM`` name they declare. From 595c88334dca04c6759dc2c0ac079c14fabe017b Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 11 Jan 2020 17:53:01 -0800 Subject: [PATCH 416/459] 2.0 version --- blessed/__init__.py | 2 +- version.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/blessed/__init__.py b/blessed/__init__.py index 18d53cb7..4b158603 100644 --- a/blessed/__init__.py +++ b/blessed/__init__.py @@ -19,4 +19,4 @@ 'support due to http://bugs.python.org/issue10570.') __all__ = ('Terminal',) -__version__ = '1.16.0' +__version__ = '2.0.0' diff --git a/version.json b/version.json index 4d76db6e..f723c478 100644 --- a/version.json +++ b/version.json @@ -1 +1 @@ -{"version": "1.16.1"} +{"version": "2.0.0"} From 367e25c07ab98213b8b5371f8e5e1d831749e625 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 12 Jan 2020 03:49:44 -0800 Subject: [PATCH 417/459] yar --- bin/plasma.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/bin/plasma.py b/bin/plasma.py index a66e231a..0d88d605 100755 --- a/bin/plasma.py +++ b/bin/plasma.py @@ -23,8 +23,8 @@ def rgb_at_xy(term, x, y, t): (y - h / 2.0) * (y - h / 2.0)) ) / 8.0 + t*3) ) + math.sin(math.sqrt((x * x + y * y)) / 8.0) - saturation = 1-y / h - lightness = 1-x / w + saturation = y / h + lightness = x / w return tuple(map(scale_255, colorsys.hsv_to_rgb(hue / 8.0, saturation, lightness))) def screen_plasma(term, plasma_fn, t): @@ -45,13 +45,13 @@ def elapser(): # pylint: disable=unnecessary-lambda yield lambda: elapser() -def please_wait(term): +def show_please_wait(term): txt_wait = 'please wait ...' outp = term.move(term.height-1, 0) + term.clear_eol + term.center(txt_wait) print(outp, end='') sys.stdout.flush() -def paused(term): +def show_paused(term): txt_paused = 'paused' outp = term.move(term.height-1, int(term.width/2 - len(txt_paused)/2)) outp += txt_paused @@ -77,15 +77,15 @@ def status(term, elapsed): f'{term.color_distance_algorithm} - ?: help ') right_txt = f'fps: {1 / elapsed:2.2f}' return ('\n' + term.normal + - term.white_on_blue + left_txt + + term.white_on_blue + term.clear_eol + left_txt + term.rjust(right_txt, term.width-len(left_txt))) def main(term): - with term.cbreak(), term.hidden_cursor(): - pause = False + with term.cbreak(), term.hidden_cursor(), term.fullscreen(): + pause, dirty = False, True t = time.time() while True: - if not pause or dirty: + if dirty or not pause: if not pause: t = time.time() with elapsed_timer() as elapsed: @@ -94,8 +94,8 @@ def main(term): print(outp, end='') sys.stdout.flush() dirty = False - if paused: - paused(term) + if pause: + show_paused(term) inp = term.inkey(timeout=0.01 if not pause else None) if inp == '?': assert False, "don't panic" @@ -103,13 +103,13 @@ def main(term): if inp in ('[', ']'): term.color_distance_algorithm = next_algo( term.color_distance_algorithm, inp == '[') - please_wait(term) + show_please_wait(term) dirty = True if inp == ' ': pause = not pause if inp.code in (term.KEY_TAB, term.KEY_BTAB): term.number_of_colors = next_color( term.number_of_colors, inp.code==term.KEY_TAB) - please_wait(term) + show_please_wait(term) dirty = True if __name__ == "__main__": From 115d46ad8412c8b406d13384e755d09247af9a71 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 12 Jan 2020 03:50:00 -0800 Subject: [PATCH 418/459] hide real cursor in picker, use fullscreen --- bin/x11_colorpicker.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bin/x11_colorpicker.py b/bin/x11_colorpicker.py index 473bcf42..80bbed0e 100644 --- a/bin/x11_colorpicker.py +++ b/bin/x11_colorpicker.py @@ -41,15 +41,15 @@ def next_color(color, forward): def main(): term = blessed.Terminal() - with term.cbreak(), term.hidden_cursor(): + with term.cbreak(), term.hidden_cursor(), term.fullscreen(): idx = len(hsv_sorted_colors) // 2 dirty = True while True: if dirty: outp = render(term, idx) - with term.hidden_cursor(): - print(outp, end='', flush=True) - inp = term.inkey() + print(outp, end='', flush=True) + with term.hidden_cursor(): + inp = term.inkey() dirty = True if inp.code == term.KEY_LEFT or inp == 'h': idx -= 1 From 3c443a2a7bbc8383f4bd8f6c3128f5f7c5ef6f91 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 12 Jan 2020 03:50:15 -0800 Subject: [PATCH 419/459] remove that awful curses injection, polish changes --- blessed/keyboard.py | 61 +++++++++++++++++++++++++++++++-------------- blessed/terminal.py | 4 +-- docs/history.rst | 29 +++++++++++++-------- 3 files changed, 62 insertions(+), 32 deletions(-) diff --git a/blessed/keyboard.py b/blessed/keyboard.py index 3cec3104..d96e6d17 100644 --- a/blessed/keyboard.py +++ b/blessed/keyboard.py @@ -118,6 +118,12 @@ def get_keyboard_codes(): """ keycodes = OrderedDict(get_curses_keycodes()) keycodes.update(CURSES_KEYCODE_OVERRIDE_MIXIN) + # merge _CURSES_KEYCODE_ADDINS added to our module space + keycodes.update({ + name: value + for name, value in globals().items() + if name.startswith('KEY_') + }) # invert dictionary (key, values) => (values, key), preferring the # last-most inserted value ('KEY_DELETE' over 'KEY_DC'). @@ -315,6 +321,23 @@ def _read_until(term, pattern, timeout): return match, buf +#: Though we may determine *keynames* and codes for keyboard input that +#: generate multibyte sequences, it is also especially useful to aliases +#: a few basic ASCII characters such as ``KEY_TAB`` instead of ``u'\t'`` for +#: uniformity. +#: +#: Furthermore, many key-names for application keys enabled only by context +#: manager :meth:`~.Terminal.keypad` are surprisingly absent. We inject them +#: here directly into the curses module. +_CURSES_KEYCODE_ADDINS = ('TAB', 'KP_MULTIPLY', 'KP_ADD', 'KP_SEPARATOR', 'KP_SUBTRACT', 'KP_DECIMAL', + 'KP_DIVIDE', 'KP_EQUAL', 'KP_0', 'KP_1', 'KP_2', 'KP_3', 'KP_4', 'KP_5', 'KP_6', + 'KP_7', 'KP_8', 'KP_9') + +_lastval = max(get_curses_keycodes().values()) +for keycode_name in _CURSES_KEYCODE_ADDINS: + _lastval += 1 + globals()['KEY_' + keycode_name] = _lastval + #: In a perfect world, terminal emulators would always send exactly what #: the terminfo(5) capability database plans for them, accordingly by the #: value of the ``TERM`` name they declare. @@ -336,7 +359,7 @@ def _read_until(term, pattern, timeout): (six.unichr(10), curses.KEY_ENTER), (six.unichr(13), curses.KEY_ENTER), (six.unichr(8), curses.KEY_BACKSPACE), - (six.unichr(9), curses.KEY_TAB), + (six.unichr(9), KEY_TAB), (six.unichr(27), curses.KEY_EXIT), (six.unichr(127), curses.KEY_BACKSPACE), @@ -356,24 +379,24 @@ def _read_until(term, pattern, timeout): # http://fossies.org/linux/rxvt/doc/rxvtRef.html#KeyCodes # # keypad, numlock on - (u"\x1bOM", curses.KEY_ENTER), # return - (u"\x1bOj", curses.KEY_KP_MULTIPLY), # * - (u"\x1bOk", curses.KEY_KP_ADD), # + - (u"\x1bOl", curses.KEY_KP_SEPARATOR), # , - (u"\x1bOm", curses.KEY_KP_SUBTRACT), # - - (u"\x1bOn", curses.KEY_KP_DECIMAL), # . - (u"\x1bOo", curses.KEY_KP_DIVIDE), # / - (u"\x1bOX", curses.KEY_KP_EQUAL), # = - (u"\x1bOp", curses.KEY_KP_0), # 0 - (u"\x1bOq", curses.KEY_KP_1), # 1 - (u"\x1bOr", curses.KEY_KP_2), # 2 - (u"\x1bOs", curses.KEY_KP_3), # 3 - (u"\x1bOt", curses.KEY_KP_4), # 4 - (u"\x1bOu", curses.KEY_KP_5), # 5 - (u"\x1bOv", curses.KEY_KP_6), # 6 - (u"\x1bOw", curses.KEY_KP_7), # 7 - (u"\x1bOx", curses.KEY_KP_8), # 8 - (u"\x1bOy", curses.KEY_KP_9), # 9 + (u"\x1bOM", curses.KEY_ENTER), # return + (u"\x1bOj", KEY_KP_MULTIPLY), # * + (u"\x1bOk", KEY_KP_ADD), # + + (u"\x1bOl", KEY_KP_SEPARATOR), # , + (u"\x1bOm", KEY_KP_SUBTRACT), # - + (u"\x1bOn", KEY_KP_DECIMAL), # . + (u"\x1bOo", KEY_KP_DIVIDE), # / + (u"\x1bOX", KEY_KP_EQUAL), # = + (u"\x1bOp", KEY_KP_0), # 0 + (u"\x1bOq", KEY_KP_1), # 1 + (u"\x1bOr", KEY_KP_2), # 2 + (u"\x1bOs", KEY_KP_3), # 3 + (u"\x1bOt", KEY_KP_4), # 4 + (u"\x1bOu", KEY_KP_5), # 5 + (u"\x1bOv", KEY_KP_6), # 6 + (u"\x1bOw", KEY_KP_7), # 7 + (u"\x1bOx", KEY_KP_8), # 8 + (u"\x1bOy", KEY_KP_9), # 9 # keypad, numlock off (u"\x1b[1~", curses.KEY_FIND), # find diff --git a/blessed/terminal.py b/blessed/terminal.py index 4046db6b..e066770e 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -111,7 +111,7 @@ class Terminal(object): enter_fullscreen='smcup', exit_fullscreen='rmcup', move='cup', - position='cup', + move_yx='cup', move_x='hpa', move_y='vpa', move_left='cub1', @@ -120,7 +120,7 @@ class Terminal(object): move_down='cud1', hide_cursor='civis', normal_cursor='cnorm', - reset_colors='op', # oc doesn't work on my OS X terminal. + reset_colors='op', normal='sgr0', reverse='rev', italic='sitm', diff --git a/docs/history.rst b/docs/history.rst index eb3f4451..6200768d 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -1,28 +1,35 @@ Version History =============== 1.17 + * introduced: 24-bit color support, detected by ``term.number_of_colors == 1 << 24``, + and 24-bit color foreground method :meth:`~Terminal.color_rgb` and background method + :meth:`~Terminal.on_color_rgb`. * bugfix: Context Managers, :meth:`~.Terminal.fullscreen`, :meth:`~.Terminal.hidden_cursor`, and :meth:`~Terminal.keypad` now flush the stream after writing their sequences. - * bugfix: ``chr(127)``, ``\x7f`` has changed from keycode ``term.DELETE`` - to ``term.BACKSPACE``, :ghissue:115` by :ghuser:`jwezel`. - * deprecated: "compoundable" with ``superscript``, ``subscript``, or - ``shadow``, or ``dim``, such as in phrase ``Terminal.blue_subscript('a')``. - Use Unicode text or 256 or 24-bit color codes instead. + * bugfix: ``chr(127)``, ``\x7f`` has changed from keycode ``term.DELETE`` to the more + common match, ``term.BACKSPACE``, :ghissue:115` by :ghuser:`jwezel`. + * deprecated: the direct curses ``move()`` capability is no longer recommended, + suggest to use :meth:`~.Terminal.move_xy()`, which matches the return value of + :meth:`~.Terminal.get_location`. + * deprecated: ``superscript``, ``subscript``, ``shadow``, and ``dim`` are no + longer "compoundable" with colors, such as in phrase ``Terminal.blue_subscript('a')``. + These attributes are not typically supported, anyway. Use Unicode text or 256 or + 24-bit color codes instead. + * deprecated: additional key names, such as ``KEY_TAB``, are no longer "injected" into + the curses module namespace. 1.16 - * Windows support?! :ghissue:`110` by :ghuser:`avylove`. + * introduced: Windows support?! :ghissue:`110` by :ghuser:`avylove`. 1.15 - * disable timing integration tests for keyboard routines. + * enhancement: disable timing integration tests for keyboard routines. They work perfectly fine for regression testing for contributing developers, but people run our tests on build farms and open issues when they fail. So we comment out these useful tests. :ghissue:`100`. - - * Support python 3.7. :ghissue:`102`. - - * Various fixes to test automation :ghissue:`108` + * enhancement: Support python 3.7. :ghissue:`102`. + * enhancement: Various fixes to test automation :ghissue:`108` 1.14 * bugfix: :meth:`~.Terminal.wrap` misbehaved for text containing newlines, From 8c2b393c6848436b8cbc554147fabc447f4ce552 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 12 Jan 2020 03:56:01 -0800 Subject: [PATCH 420/459] closes #59 use devnull for open descriptor --- blessed/terminal.py | 10 ++-------- docs/history.rst | 2 ++ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/blessed/terminal.py b/blessed/terminal.py index e066770e..f6b19514 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -212,15 +212,9 @@ def __init__(self, kind=None, stream=None, force_styling=False): self._kind = kind or os.environ.get('TERM', 'unknown') if self.does_styling: - # Initialize curses (call setupterm). - # - # Make things like tigetstr() work. Explicit args make setupterm() - # work even when -s is passed to nosetests. Lean toward sending - # init sequences to the stream if it has a file descriptor, and - # send them to stdout as a fallback, since they have to go - # somewhere. + # Initialize curses (call setupterm), so things like tigetstr() work. try: - curses.setupterm(self._kind, self._init_descriptor) + curses.setupterm(kind, open(os.devnull).fileno()) except curses.error as err: warnings.warn('Failed to setupterm(kind={0!r}): {1}' .format(self._kind, err)) diff --git a/docs/history.rst b/docs/history.rst index 6200768d..cc7898f9 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -18,6 +18,8 @@ Version History 24-bit color codes instead. * deprecated: additional key names, such as ``KEY_TAB``, are no longer "injected" into the curses module namespace. + * deprecated: :func:`curses.setupterm` is now called with :attr:`os.devnull` + as the file descriptor, let us know if this causes any issues. :ghissue:`59`. 1.16 * introduced: Windows support?! :ghissue:`110` by :ghuser:`avylove`. From 2165c1ef0b9ae077cc75243de61204ed078083a5 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 12 Jan 2020 04:02:27 -0800 Subject: [PATCH 421/459] Remove this legacy exception Closes #69 This was just made to try to appease upstream 'blessings' to accept keyboard support, which they did not. --- blessed/terminal.py | 7 ------- docs/history.rst | 4 ++++ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/blessed/terminal.py b/blessed/terminal.py index f6b19514..e0cbe47c 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -1181,9 +1181,6 @@ def inkey(self, timeout=None, esc_delay=0.35, **_kwargs): :rtype: :class:`~.Keystroke`. :returns: :class:`~.Keystroke`, which may be empty (``u''``) if ``timeout`` is specified and keystroke is not received. - :raises RuntimeError: When :attr:`stream` is not a terminal, having - no keyboard attached, a ``timeout`` value of ``None`` would block - indefinitely, prevented by by raising an exception. .. note:: When used without the context manager :meth:`cbreak`, or :meth:`raw`, :obj:`sys.__stdin__` remains line-buffered, and this @@ -1196,10 +1193,6 @@ def inkey(self, timeout=None, esc_delay=0.35, **_kwargs): if _kwargs: raise TypeError('inkey() got unexpected keyword arguments {!r}' .format(_kwargs)) - if timeout is None and self._keyboard_fd is None: - raise RuntimeError( - 'Terminal.inkey() called, but no terminal with keyboard ' - 'attached to process. This call would hang forever.') resolve = functools.partial(resolve_sequence, mapper=self._keymap, diff --git a/docs/history.rst b/docs/history.rst index cc7898f9..b40ce686 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -20,6 +20,10 @@ Version History the curses module namespace. * deprecated: :func:`curses.setupterm` is now called with :attr:`os.devnull` as the file descriptor, let us know if this causes any issues. :ghissue:`59`. + * deprecated: :meth:`~Terminal.inkey` no longer raises RuntimeError when + :attr:`~Terminal.stream` is not a terminal, programs using + :meth:`~Terminal.inkey` to block indefinitely if a keyboard is not + attached. :ghissue:`69`. 1.16 * introduced: Windows support?! :ghissue:`110` by :ghuser:`avylove`. From d7c3d52f8804d92cfb35a8286cd88fd46e633fc9 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 12 Jan 2020 05:21:21 -0800 Subject: [PATCH 422/459] legacy fix utf8 in here --- blessed/color.py | 1 + 1 file changed, 1 insertion(+) diff --git a/blessed/color.py b/blessed/color.py index 59384569..f2855748 100644 --- a/blessed/color.py +++ b/blessed/color.py @@ -1,3 +1,4 @@ +# encoding: utf-8 """ Sub-module providing color functions. From dda4d1d8fa7c86e3a62404e8ec2cd982bc214178 Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Sun, 12 Jan 2020 10:24:11 -0500 Subject: [PATCH 423/459] Account for duplicates in x11_colorpicker --- bin/x11_colorpicker.py | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/bin/x11_colorpicker.py b/bin/x11_colorpicker.py index 80bbed0e..37562d62 100644 --- a/bin/x11_colorpicker.py +++ b/bin/x11_colorpicker.py @@ -1,20 +1,32 @@ import colorsys import blessed -hsv_sorted_colors = sorted( - blessed.colorspace.X11_COLORNAMES_TO_RGB.items(), - key=lambda rgb: colorsys.rgb_to_hsv(*rgb[1]), - reverse=True) +def sort_colors(): + colors = {} + for color_name, rgb_color in blessed.colorspace.X11_COLORNAMES_TO_RGB.items(): + if rgb_color in colors: + colors[rgb_color].append(color_name) + else: + colors[rgb_color] = [color_name] + + return sorted(colors.items(), + key=lambda rgb: colorsys.rgb_to_hsv(*rgb[0]), + reverse=True) + + +HSV_SORTED_COLORS = sort_colors() + def render(term, idx): - color_name, rgb_color = hsv_sorted_colors[idx] + rgb_color, color_names = HSV_SORTED_COLORS[idx] result = term.home + term.normal + ''.join( - getattr(term, hsv_sorted_colors[i][0]) + '◼' - for i in range(len(hsv_sorted_colors)) + getattr(term, HSV_SORTED_COLORS[i][1][0]) + '◼' + for i in range(len(HSV_SORTED_COLORS)) ) - result += term.clear_eos+ '\n' - result += getattr(term, 'on_' + color_name) + term.clear_eos + '\n' - result += term.normal + term.center(f'{color_name}: {rgb_color}') + '\n' + result += term.clear_eos + '\n' + result += getattr(term, 'on_' + color_names[0]) + term.clear_eos + '\n' + result += term.normal + \ + term.center(f'{" | ".join(color_names)}: {rgb_color}') + '\n' result += term.normal + term.center( f'{term.number_of_colors} colors - ' f'{term.color_distance_algorithm}') @@ -42,7 +54,7 @@ def next_color(color, forward): def main(): term = blessed.Terminal() with term.cbreak(), term.hidden_cursor(), term.fullscreen(): - idx = len(hsv_sorted_colors) // 2 + idx = len(HSV_SORTED_COLORS) // 2 dirty = True while True: if dirty: @@ -71,9 +83,9 @@ def main(): dirty = False while idx < 0: - idx += len(hsv_sorted_colors) - while idx >= len(hsv_sorted_colors): - idx -= len(hsv_sorted_colors) + idx += len(HSV_SORTED_COLORS) + while idx >= len(HSV_SORTED_COLORS): + idx -= len(HSV_SORTED_COLORS) if __name__ == '__main__': main() From 9bc416f4831f2daa10306375c0963f8dfd8abc19 Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Sun, 12 Jan 2020 10:25:13 -0500 Subject: [PATCH 424/459] Add CIE2000 color distance algorithm --- blessed/color.py | 72 +++++++++++++++++++++++++++++++++++++++++++-- blessed/terminal.py | 6 ++-- 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/blessed/color.py b/blessed/color.py index f2855748..4af08ef2 100644 --- a/blessed/color.py +++ b/blessed/color.py @@ -9,7 +9,7 @@ """ -from math import sqrt +from math import atan2, cos, exp, sin, sqrt def rgb_to_xyz(red, green, blue): @@ -181,7 +181,75 @@ def dist_cie94(rgb1, rgb2): (delta_h / (k_h * s_h)) ** 2) +def dist_cie2000(rgb1, rgb2): + # pylint: disable=too-many-locals + """ + Determine distance between two rgb colors using the CIE2000 algorithm. + + :arg tuple rgb1: RGB color definition + :arg tuple rgb2: RGB color definition + :returns: Square of the distance between provided colors + :rtype: float + + For efficiency, the square of the distance is returned + which is sufficient for comparisons + """ + s_l = k_l = k_c = k_h = 1 + + l_1, a_1, b_1 = rgb_to_lab(*rgb1) + l_2, a_2, b_2 = rgb_to_lab(*rgb2) + + delta_l = l_2 - l_1 + l_mean = (l_1 + l_2) / 2 + + c_1 = sqrt(a_1 ** 2 + b_1 ** 2) + c_2 = sqrt(a_2 ** 2 + b_2 ** 2) + c_mean = (c_1 + c_2) / 2 + delta_c = c_1 - c_2 + + g_x = sqrt(c_mean ** 7 / (c_mean ** 7 + 25 ** 7)) + h_1 = atan2(b_1, a_1 + (a_1 / 2) * (1 - g_x)) % 360 + h_2 = atan2(b_2, a_2 + (a_2 / 2) * (1 - g_x)) % 360 + + if 0 in (c_1, c_2): + delta_h_prime = 0 + h_mean = h_1 + h_2 + else: + delta_h_prime = h_2 - h_1 + if abs(delta_h_prime) <= 180: + h_mean = (h_1 + h_2) / 2 + else: + if h_2 <= h_1: + delta_h_prime += 360 + else: + delta_h_prime -= 360 + if h_1 + h_2 < 360: + h_mean = (h_1 + h_2 + 360) / 2 + else: + h_mean = (h_1 + h_2 - 360) / 2 + + delta_h = 2 * sqrt(c_1 * c_2) * sin(delta_h_prime / 2) + + t_x = (1 - + 0.17 * cos(h_mean - 30) + + 0.24 * cos(2 * h_mean) + + 0.32 * cos(3 * h_mean + 6) - + 0.20 * cos(4 * h_mean - 63)) + + s_l = 1 + (0.015 * (l_mean - 50) ** 2) / sqrt(20 + (l_mean - 50) ** 2) + s_c = 1 + 0.045 * c_mean + s_h = 1 + 0.015 * c_mean * t_x + r_t = -2 * g_x * sin(abs(60 * exp(-1 * abs((delta_h - 275) / 25) ** 2))) + + delta_l = delta_l / (k_l * s_l) + delta_c = delta_c / (k_c * s_c) + delta_h = delta_h / (k_h * s_h) + + return delta_l ** 2 + delta_c ** 2 + delta_h ** 2 + r_t * delta_c * delta_h + + COLOR_DISTANCE_ALGORITHMS = {'rgb': dist_rgb, 'rgb-weighted': dist_rgb_weighted, 'cie76': dist_cie76, - 'cie94': dist_cie94} + 'cie94': dist_cie94, + 'cie2000': dist_cie2000} diff --git a/blessed/terminal.py b/blessed/terminal.py index e0cbe47c..1fe78fb9 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -241,7 +241,7 @@ def __init__(self, kind=None, stream=None, force_styling=False): self.__init__keycodes() def __init__color_capabilities(self): - self._color_distance_algorithm = 'cie94' + self._color_distance_algorithm = 'cie2000' if not self.does_styling: self.number_of_colors = 0 elif platform.system() == 'Windows' or ( @@ -775,8 +775,8 @@ def color_distance_algorithm(self): """ Color distance algorithm used by :meth:`rgb_downconvert`. - The slowest, but most accurate, 'cie94', is default. Other - available options are 'rgb', 'rgb-weighted', and 'cie76'. + The slowest, but most accurate, 'cie2000', is default. Other + available options are 'rgb', 'rgb-weighted', 'cie76', and 'cie94'. """ return self._color_distance_algorithm From fed09ec14a84f73d78f6bbc090ac7dd966b3e52f Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Sun, 12 Jan 2020 10:26:03 -0500 Subject: [PATCH 425/459] Enhance badges --- docs/intro.rst | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/docs/intro.rst b/docs/intro.rst index e4186069..d06fdd00 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -1,23 +1,47 @@ -.. image:: https://img.shields.io/travis/jquast/blessed/master.svg +| |docs| |travis| |coveralls| +| |pypi| |downloads| |gitter| +| |linux| |windows| |mac| |bsd| + +.. |docs| image:: https://img.shields.io/readthedocs/blessed.svg?logo=read-the-docs + :target: https://blessed.readthedocs.org + :alt: Documentation Status + +.. |travis| image:: https://img.shields.io/travis/jquast/blessed/master.svg?logo=travis :alt: Travis Continuous Integration :target: https://travis-ci.org/jquast/blessed/ -.. image:: https://coveralls.io/repos/jquast/blessed/badge.svg?branch=master&service=github +.. |coveralls| image:: https://img.shields.io/coveralls/github/jquast/blessed/master?logo=coveralls :alt: Coveralls Code Coverage :target: https://coveralls.io/github/jquast/blessed?branch=master -.. image:: https://img.shields.io/pypi/v/blessed.svg +.. |pypi| image:: https://img.shields.io/pypi/v/blessed.svg?logo=pypi :alt: Latest Version :target: https://pypi.python.org/pypi/blessed -.. image:: https://img.shields.io/pypi/dm/blessed.svg +.. |downloads| image:: https://img.shields.io/pypi/dm/blessed.svg?logo=pypi :alt: Downloads :target: https://pypi.python.org/pypi/blessed -.. image:: https://badges.gitter.im/Join%20Chat.svg +.. |gitter| image:: https://img.shields.io/badge/gitter-Join%20Chat-mediumaquamarine?logo=gitter :alt: Join Chat :target: https://gitter.im/jquast/blessed +.. |linux| image:: https://img.shields.io/badge/Linux-yes-success?logo=linux + :alt: Linux supported + :target: https://pypi.python.org/pypi/enlighten + +.. |windows| image:: https://img.shields.io/badge/Windows-NEW-success?logo=windows + :alt: Windows supported + :target: https://pypi.python.org/pypi/enlighten + +.. |mac| image:: https://img.shields.io/badge/MacOS-yes-success?logo=apple + :alt: MacOS supported + :target: https://pypi.python.org/pypi/enlighten + +.. |bsd| image:: https://img.shields.io/badge/BSD-yes-success?logo=freebsd + :alt: BSD supported + :target: https://pypi.python.org/pypi/enlighten + Introduction ============ From d0810eb0ff510da27b28ec8ccf8bfeb6bef90406 Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Sun, 12 Jan 2020 16:56:21 -0500 Subject: [PATCH 426/459] Account for %i in cursor_report (#94) --- blessed/terminal.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/blessed/terminal.py b/blessed/terminal.py index 1fe78fb9..389faab1 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -592,8 +592,19 @@ def get_location(self, timeout=None): if match: # return matching sequence response, the cursor location. - row, col = match.groups() - return int(row), int(col) + row, col = (int(val) for val in match.groups()) + + # Per https://invisible-island.net/ncurses/terminfo.src.html + # The cursor position report () string must contain two + # scanf(3)-style %d format elements. The first of these must + # correspond to the Y coordinate and the second to the %d. + # If the string contains the sequence %i, it is taken as an + # instruction to decrement each value after reading it (this is + # the inverse sense from the cup string). + if '%i' in self.cursor_report: + row -= 1 + col -= 1 + return row, col finally: if ctx is not None: From 031e70707afeab55f1bbe0d86808f5f753297fb2 Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Sun, 12 Jan 2020 16:57:33 -0500 Subject: [PATCH 427/459] Bugfix split_seqs() for color256 (#101) --- blessed/_capabilities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blessed/_capabilities.py b/blessed/_capabilities.py index c6f96c9f..13ce9a63 100644 --- a/blessed/_capabilities.py +++ b/blessed/_capabilities.py @@ -125,7 +125,7 @@ CAPABILITIES_ADDITIVES = { - 'color256': ('color', re.escape('\x1b') + r'\[38;5;(\d+)m'), + 'color256': ('color', re.escape('\x1b') + r'\[38;5;\d+m'), 'shift_in': ('', re.escape('\x0f')), 'shift_out': ('', re.escape('\x0e')), # sgr(...) outputs strangely, use the basic ANSI/EMCA-48 codes here. From 64a83e7054f2615420276f0dad865177b1ebbc52 Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Sun, 12 Jan 2020 18:19:25 -0500 Subject: [PATCH 428/459] Add limited caching for color distance --- blessed/color.py | 6 ++++++ requirements.txt | 2 ++ 2 files changed, 8 insertions(+) diff --git a/blessed/color.py b/blessed/color.py index 4af08ef2..3c429ee9 100644 --- a/blessed/color.py +++ b/blessed/color.py @@ -11,6 +11,11 @@ from math import atan2, cos, exp, sin, sqrt +try: + from functools import lru_cache +except ImportError: + from backports.functools_lru_cache import lru_cache + def rgb_to_xyz(red, green, blue): """ @@ -71,6 +76,7 @@ def xyz_to_lab(x_val, y_val, z_val): return cie_l, cie_a, cie_b +@lru_cache(maxsize=256) def rgb_to_lab(red, green, blue): """ Convert RGB color to CIE-Lab color. diff --git a/requirements.txt b/requirements.txt index c1fa4fc4..917d2a45 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,5 +2,7 @@ wcwidth>=0.1.4 six>=1.9.0 # support python2.6 by using backport of 'orderedict' ordereddict==1.1; python_version < "2.7" +# support python2.7 by using backport of 'functools.lru_cache' +backports.functools-lru-cache>=1.2.1; python_version < "3.2" # Windows requires jinxed jinxed>=0.5.4; platform_system == "Windows" \ No newline at end of file From 9133e78a78a0b2cbf197a8b4313445f0f6d635c2 Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Sun, 12 Jan 2020 18:50:21 -0500 Subject: [PATCH 429/459] Update travis to test on 3.8 and 3.9-dev --- .travis.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 4206429a..5cc47137 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,8 +15,14 @@ matrix: env: TOXENV=py36 COVERAGE_ID=travis-ci - python: 3.7 env: TOXENV=py37 COVERAGE_ID=travis-ci - - python: 3.8-dev + - python: 3.8 env: TOXENV=py38 COVERAGE_ID=travis-ci + - python: 3.9-dev + env: TOXENV=py39 COVERAGE_ID=travis-ci + +jobs: + allow_failures: + - python: 3.9-dev install: - pip install tox From 59d20299f0eceba510f8724c0c6d5ab338777b88 Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Sun, 12 Jan 2020 18:59:18 -0500 Subject: [PATCH 430/459] Fix Travis SA and Sphinx interpretor --- .travis.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5cc47137..2a579d36 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,9 +2,12 @@ language: python matrix: fast_finish: true include: - - env: TOXENV=about - - env: TOXENV=sa - - env: TOXENV=sphinx + - python: 3.7 + env: TOXENV=about + - python: 3.7 + env: TOXENV=sa + - python: 3.7 + env: TOXENV=sphinx - python: 2.7 env: TOXENV=py27 TEST_QUICK=1 COVERAGE_ID=travis-ci - python: 3.4 From a9fe25696c5f22f3660398426ee1cbf687384126 Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Sun, 12 Jan 2020 20:34:15 -0500 Subject: [PATCH 431/459] Blessed-specific enhancements #117 --- docs/intro.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/intro.rst b/docs/intro.rst index d06fdd00..ae72b3ec 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -182,6 +182,19 @@ Forked *Blessed* is a fork of `blessings `_. Changes since 1.7 have all been proposed but unaccepted upstream. +Enhancements only in *Blessed*: + * 24-bit color support with :meth:`~Terminal.color_rgb` and :meth:`~Terminal.on_color_rgb` methods + * X11 color name attributes + * Windows support + * :meth:`~.Terminal.length` to determine printable length of text containing sequences + * :meth:`~.Terminal.strip`, :meth:`~.Terminal.rstrip`, :meth:`~.Terminal.rstrip`, + and :meth:`~.Terminal.strip_seqs` for removing sequences from text + * :meth:`Terminal.wrap` for wrapping text containing sequences at a specified width + * :meth:`~.Terminal.center`, :meth:`~.Terminal.rjust`, and :meth:`~.Terminal.ljust` + for alignment of text containing sequences + * :meth:`~.cbreak` and :meth:`~.raw` context managers for keyboard input + * :meth:`~.inkey` for keyboard event detection + Furthermore, a project in the node.js language of the `same name `_ is **not** related, or a fork of each other in any way. From 5f6d3cf932f388ecf51dcad46c59f2e1dc033149 Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Tue, 14 Jan 2020 14:20:59 -0500 Subject: [PATCH 432/459] Remove _inject_curses_keynames from docs --- docs/api.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index 3a276bdf..f6e39ab8 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -30,7 +30,6 @@ keyboard.py :private-members: :special-members: __new__ .. autofunction:: _alternative_left_right -.. autofunction:: _inject_curses_keynames .. autodata:: DEFAULT_SEQUENCE_MIXIN .. autodata:: CURSES_KEYCODE_OVERRIDE_MIXIN From 72e648a1c9869d37bbc765c2c48192d5051b44dd Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Tue, 14 Jan 2020 14:32:44 -0500 Subject: [PATCH 433/459] Remove subscript and superscript from tests --- blessed/tests/test_sequences.py | 7 ------- blessed/tests/test_wrap.py | 5 +---- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index 9a1d66aa..f1e75280 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -345,13 +345,6 @@ def child(kind): expected_output = u'boö' assert (t.underline(u'boö') == expected_output) - if t.subscript: - expected_output = u''.join((t.subscript, u'[1]', t.normal)) - else: - expected_output = u'[1]' - - assert (t.subscript(u'[1]') == expected_output) - child(all_terms) diff --git a/blessed/tests/test_wrap.py b/blessed/tests/test_wrap.py index e0458bd0..f23559c7 100644 --- a/blessed/tests/test_wrap.py +++ b/blessed/tests/test_wrap.py @@ -75,16 +75,13 @@ def child(width, pgraph, kwargs): # build a test paragraph, along with a very colorful version term = TestTerminal() attributes = ('bright_red', 'on_bright_blue', 'underline', 'reverse', - 'red_reverse', 'red_on_white', 'superscript', - 'subscript', 'on_bright_white') + 'red_reverse', 'red_on_white', 'on_bright_white') term.bright_red('x') term.on_bright_blue('x') term.underline('x') term.reverse('x') term.red_reverse('x') term.red_on_white('x') - term.superscript('x') - term.subscript('x') term.on_bright_white('x') pgraph_colored = u''.join([ From 95ed5ad9887bb43c24a6ffadf2c9f4d2b98561dd Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Tue, 14 Jan 2020 14:43:45 -0500 Subject: [PATCH 434/459] Explicity set junit_family for pytest --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 89dccabd..a574d25b 100644 --- a/tox.ini +++ b/tox.ini @@ -78,6 +78,7 @@ setenv = TEST_QUICK=1 [pytest] looponfailroots = blessed norecursedirs = .git .tox build +junit_family = xunit1 [coverage] rcfile = {toxinidir}/.coveragerc From 57eddf701a5b4d8ba73ba6f132b5b245c9a716a3 Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Tue, 14 Jan 2020 15:09:21 -0500 Subject: [PATCH 435/459] Update keycode test --- blessed/tests/test_keyboard.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index 7a9bf1e7..c61d6393 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -681,17 +681,19 @@ def test_a_keystroke(): def test_get_keyboard_codes(): "Test all values returned by get_keyboard_codes are from curses." - from blessed.keyboard import ( - get_keyboard_codes, - CURSES_KEYCODE_OVERRIDE_MIXIN, - ) - exemptions = dict(CURSES_KEYCODE_OVERRIDE_MIXIN) - for value, keycode in get_keyboard_codes().items(): + import blessed.keyboard + exemptions = dict(blessed.keyboard.CURSES_KEYCODE_OVERRIDE_MIXIN) + for value, keycode in blessed.keyboard.get_keyboard_codes().items(): if keycode in exemptions: assert value == exemptions[keycode] continue - assert hasattr(curses, keycode) - assert getattr(curses, keycode) == value + if keycode[4:] in blessed.keyboard._CURSES_KEYCODE_ADDINS: + assert not hasattr(curses, keycode) + assert hasattr(blessed.keyboard, keycode) + assert getattr(blessed.keyboard, keycode) == value + else: + assert hasattr(curses, keycode) + assert getattr(curses, keycode) == value def test_alternative_left_right(): From 0e9f056b3acc4862fadff5bb417150a8a38593ab Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Wed, 15 Jan 2020 16:05:17 -0800 Subject: [PATCH 436/459] FOSS gardening by tox.ini updates (#119) - update all test and static analysis libraries - set back program version.json for 1.17.0 release - tox.ini overhaul - add .editorconfig, .pylintrc - delete legacy files (tools/) - add color/colorspace to api documentation --- .coveragerc | 14 --- .editorconfig | 31 ++++++ .gitignore | 1 + .pylintrc | 48 ++++++++ .travis.yml | 33 ++---- CONTRIBUTING.rst | 22 +--- MANIFEST.in | 1 - blessed/__init__.py | 2 +- blessed/tests/accessories.py | 77 +++++++------ docs/api.rst | 27 ++++- requirements-about.txt | 1 - requirements-analysis.txt | 4 - requirements-docs.txt | 3 - requirements-tests.txt | 4 - tools/bump-version.py | 41 ------- tools/custom-combine.py | 62 ----------- tox.ini | 209 +++++++++++++++++++++++------------ version.json | 2 +- 18 files changed, 300 insertions(+), 282 deletions(-) delete mode 100644 .coveragerc create mode 100644 .editorconfig create mode 100644 .pylintrc delete mode 100644 requirements-about.txt delete mode 100644 requirements-analysis.txt delete mode 100644 requirements-docs.txt delete mode 100644 requirements-tests.txt delete mode 100755 tools/bump-version.py delete mode 100755 tools/custom-combine.py diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index de699ea7..00000000 --- a/.coveragerc +++ /dev/null @@ -1,14 +0,0 @@ -[run] -branch = True -source = blessed -parallel = True - -[report] -omit = blessed/tests/* -exclude_lines = pragma: no cover -precision = 1 - -[paths] -source = - blessed/ - /opt/TeamCity/*/blessed/*.py diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..616d4b9a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,31 @@ +root = true + +[*] +charset = utf-8 + +[*.sh,*.json,*.ini] +tab_width = 4 +indent_size = tab +indent_space = space +trim_trailing_whitespace = true +insert_final_newline = true + +[*.py] +tab_width = 4 +indent_size = tab +indent_space = space +trim_trailing_whitespace = true +insert_final_newline = true + +# isort +# https://github.com/timothycrosley/isort/wiki/isort-Settings +line_length = 100 +indent = ' ' +multi_line_output = 1 +length_sort = 1 +sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER +import_heading_stdlib = std imports +import_heading_thirdparty = 3rd party +import_heading_firstparty = local +atomic = true +known_third_party=rapidjson diff --git a/.gitignore b/.gitignore index d1dc4d34..66b09dc4 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ htmlcov .DS_Store .*.sw? .vscode +.python-version diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 00000000..52f1f1ba --- /dev/null +++ b/.pylintrc @@ -0,0 +1,48 @@ +[MASTER] +load-plugins= + pylint.extensions.mccabe, + pylint.extensions.check_elif, + pylint.extensions.docparams, + pylint.extensions.overlapping_exceptions, + pylint.extensions.redefined_variable_type + +persistent = no +jobs = 0 +unsafe-load-any-extension = yes + +[MESSAGES CONTROL] +disable= + I, + fixme, + c-extension-no-member, + ungrouped-imports + +[FORMAT] +max-line-length: 100 +good-names=ks,fd,_ + +[PARAMETER_DOCUMENTATION] +default-docstring-type=sphinx +accept-no-raise-doc=no +accept-no-param-doc=yes +accept-no-return-doc=yes + +[DESIGN] +max-args=10 +max-attributes=7 +max-branches=12 +max-complexity=11 +max-locals=15 +max-module-lines=1000 +max-parents=7 +max-public-methods=20 +max-returns=6 +max-statements=50 + +[SIMILARITIES] +ignore-imports=yes +min-similarity-lines=8 + +[REPORTS] +reports=no +msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg} diff --git a/.travis.yml b/.travis.yml index 2a579d36..3fe7e4ba 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,30 +2,27 @@ language: python matrix: fast_finish: true include: - - python: 3.7 - env: TOXENV=about - - python: 3.7 - env: TOXENV=sa - - python: 3.7 - env: TOXENV=sphinx - python: 2.7 - env: TOXENV=py27 TEST_QUICK=1 COVERAGE_ID=travis-ci + env: TOXENV=py27,coveralls TEST_QUICK=1 COVERAGE_ID=travis-ci - python: 3.4 - env: TOXENV=py34 TEST_QUICK=1 COVERAGE_ID=travis-ci + env: TOXENV=py34,coveralls TEST_QUICK=1 COVERAGE_ID=travis-ci - python: 3.5 - env: TOXENV=py35 TEST_QUICK=1 COVERAGE_ID=travis-ci + env: TOXENV=py35,coveralls TEST_QUICK=1 COVERAGE_ID=travis-ci - python: 3.6 - env: TOXENV=py36 COVERAGE_ID=travis-ci + env: TOXENV=py36,coveralls TEST_QUICK=1 COVERAGE_ID=travis-ci - python: 3.7 - env: TOXENV=py37 COVERAGE_ID=travis-ci + env: TOXENV=py37,coveralls TEST_QUICK=1 COVERAGE_ID=travis-ci - python: 3.8 - env: TOXENV=py38 COVERAGE_ID=travis-ci + env: TOXENV=py38,coveralls COVERAGE_ID=travis-ci - python: 3.9-dev - env: TOXENV=py39 COVERAGE_ID=travis-ci + env: TOXENV=py39,coveralls TEST_QUICK=1 COVERAGE_ID=travis-ci + - python: 3.8 + env: TOXENV=about,pylint,flake8,sphinx COVERAGE_ID=travis-ci jobs: allow_failures: - - python: 3.9-dev + - env: TOXENV=py39,coveralls TEST_QUICK=1 COVERAGE_ID=travis-ci + - env: TOXENV=about,pylint,flake8,sphinx COVERAGE_ID=travis-ci install: - pip install tox @@ -39,11 +36,3 @@ notifications: - contact@jeffquast.com on_success: change on_failure: change -# irc: -# channels: -# - "irc.servercentral.net#1984" -# template: -# - "%{repository}(%{branch}): %{message} (%{duration}) %{build_url}" -# skip_join: true -# on_success: change -# on_failure: change diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 5aa648c2..af76aaf3 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -35,27 +35,13 @@ with python 3.5, stopping at the first failing test case, and looping tox -epy35 -- -fx +The test runner (``tox``) ensures all code and documentation complies with +standard python style guides, pep8 and pep257, as well as various static +analysis tools. Test Coverage ~~~~~~~~~~~~~ When you contribute a new feature, make sure it is covered by tests. -Likewise, a bug fix should include a test demonstrating the bug. Blessed has -nearly 100% line coverage, with roughly 1/2 of the codebase in the form of -tests, which are further combined by a matrix of varying ``TERM`` types, -providing plenty of existing test cases to augment or duplicate in your -favor. -Style and Static Analysis -~~~~~~~~~~~~~~~~~~~~~~~~~ - -The test runner (``tox``) ensures all code and documentation complies -with standard python style guides, pep8 and pep257, as well as various -static analysis tools through the **sa** target, invoked using:: - - tox -esa - -All standards enforced by the underlying style checker tools are adhered to, -with the declarative exception of those found in `landscape.yml -`_, or inline -using ``pylint: disable=`` directives. +Likewise, a bug fix should include a test demonstrating the bug. diff --git a/MANIFEST.in b/MANIFEST.in index f6fd7f2a..5a2ca9ef 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,6 +2,5 @@ include docs/*.rst include LICENSE include version.json include *.txt -include .coveragerc include tox.ini include blessed/tests/wall.ans diff --git a/blessed/__init__.py b/blessed/__init__.py index 4b158603..e4fec9fc 100644 --- a/blessed/__init__.py +++ b/blessed/__init__.py @@ -19,4 +19,4 @@ 'support due to http://bugs.python.org/issue10570.') __all__ = ('Terminal',) -__version__ = '2.0.0' +__version__ = '1.17.0' diff --git a/blessed/tests/accessories.py b/blessed/tests/accessories.py index d2bb09bb..ed22f254 100644 --- a/blessed/tests/accessories.py +++ b/blessed/tests/accessories.py @@ -1,24 +1,27 @@ # -*- coding: utf-8 -*- """Accessories for automated py.test runner.""" # standard imports -from __future__ import with_statement, print_function -import contextlib -import subprocess -import functools -import traceback -import termios +from __future__ import print_function, with_statement + +# std imports +import os +import pty +import sys import codecs import curses -import sys -import pty -import os +import termios +import functools +import traceback +import contextlib +import subprocess -# local -from blessed import Terminal +# 3rd party +import six +# local # 3rd-party import pytest -import six +from blessed import Terminal TestTerminal = functools.partial(Terminal, kind='xterm-256color') SEND_SEMAPHORE = SEMAPHORE = b'SEMAPHORE\n' @@ -28,8 +31,8 @@ many_columns_params = [1, 10] if os.environ.get('TEST_QUICK'): - many_lines_params = [80,] - many_columns_params = [25,] + many_lines_params = [80, ] + many_columns_params = [25, ] all_terms_params = 'xterm screen ansi vt220 rxvt cons25 linux'.split() @@ -49,25 +52,23 @@ def init_subproc_coverage(run_note): - try: - import coverage - except ImportError: - return None - _coveragerc = os.path.join( - os.path.dirname(__file__), - os.pardir, os.pardir, - '.coveragerc') - cov = coverage.Coverage(config_file=_coveragerc) - cov.set_option("run:note", run_note) - cov.start() - return cov + try: + import coverage + except ImportError: + return None + _coveragerc = os.path.join( + os.path.dirname(__file__), + os.pardir, os.pardir, + 'tox.ini') + cov = coverage.Coverage(config_file=_coveragerc) + cov.set_option("run:note", run_note) + cov.start() + return cov class as_subprocess(object): - """This helper executes test cases in a child process, - avoiding a python-internal bug of _curses: setupterm() - may not be called more than once per process. - """ + """This helper executes test cases in a child process, avoiding a python-internal bug of + _curses: setupterm() may not be called more than once per process.""" _CHILD_PID = 0 encoding = 'utf8' @@ -147,12 +148,12 @@ def __call__(self, *args, **kwargs): def read_until_semaphore(fd, semaphore=RECV_SEMAPHORE, encoding='utf8', timeout=10): - """Read file descriptor ``fd`` until ``semaphore`` is found. + """ + Read file descriptor ``fd`` until ``semaphore`` is found. - Used to ensure the child process is awake and ready. For timing - tests; without a semaphore, the time to fork() would be (incorrectly) - included in the duration of the test, which can be very length on - continuous integration servers (such as Travis-CI). + Used to ensure the child process is awake and ready. For timing tests; without a semaphore, the + time to fork() would be (incorrectly) included in the duration of the test, which can be very + length on continuous integration servers (such as Travis-CI). """ # note that when a child process writes xyz\\n, the parent # process will read xyz\\r\\n -- this is how pseudo terminals @@ -176,7 +177,11 @@ def read_until_semaphore(fd, semaphore=RECV_SEMAPHORE, def read_until_eof(fd, encoding='utf8'): - """Read file descriptor ``fd`` until EOF. Return decoded string.""" + """ + Read file descriptor ``fd`` until EOF. + + Return decoded string. + """ decoder = codecs.getincrementaldecoder(encoding)() outp = six.text_type() while True: diff --git a/docs/api.rst b/docs/api.rst index f6e39ab8..77f24cec 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,14 +1,21 @@ API Documentation ================= -terminal.py ------------ +color.py +-------- -.. automodule:: blessed.terminal +.. automodule:: blessed.color :members: :undoc-members: - :special-members: __getattr__ -.. autodata:: _CUR_TERM + :private-members: + +colorspace.py +------------- + +.. automodule:: blessed.colorspace + :members: + :undoc-members: + :private-members: formatters.py ------------- @@ -32,6 +39,7 @@ keyboard.py .. autofunction:: _alternative_left_right .. autodata:: DEFAULT_SEQUENCE_MIXIN .. autodata:: CURSES_KEYCODE_OVERRIDE_MIXIN +.. autodata:: _CURSES_KEYCODE_ADDINS sequences.py ------------ @@ -40,3 +48,12 @@ sequences.py :members: :undoc-members: :private-members: + +terminal.py +----------- + +.. automodule:: blessed.terminal + :members: + :undoc-members: + :special-members: __getattr__ +.. autodata:: _CUR_TERM diff --git a/requirements-about.txt b/requirements-about.txt deleted file mode 100644 index 808fb07a..00000000 --- a/requirements-about.txt +++ /dev/null @@ -1 +0,0 @@ -pexpect diff --git a/requirements-analysis.txt b/requirements-analysis.txt deleted file mode 100644 index 1f5e69b4..00000000 --- a/requirements-analysis.txt +++ /dev/null @@ -1,4 +0,0 @@ -prospector[with_pyroma] -restructuredtext_lint -doc8 -Pygments diff --git a/requirements-docs.txt b/requirements-docs.txt deleted file mode 100644 index 247b04ff..00000000 --- a/requirements-docs.txt +++ /dev/null @@ -1,3 +0,0 @@ -sphinx -sphinx_rtd_theme -sphinx-paramlinks diff --git a/requirements-tests.txt b/requirements-tests.txt deleted file mode 100644 index c1d3f57f..00000000 --- a/requirements-tests.txt +++ /dev/null @@ -1,4 +0,0 @@ -pytest-xdist -pytest-cov -pytest -mock diff --git a/tools/bump-version.py b/tools/bump-version.py deleted file mode 100755 index f3f35978..00000000 --- a/tools/bump-version.py +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env python3 -import json -import sys -import os - -json_version = os.path.join( - os.path.dirname(__file__), os.path.pardir, 'version.json') - -init_file = os.path.join( - os.path.dirname(__file__), os.path.pardir, 'blessed', '__init__.py') - -def main(bump_arg): - assert bump_arg in ('--minor', '--major', '--release'), bump_arg - - with open(json_version, 'r') as fin: - data = json.load(fin) - - release, major, minor = map(int, data['version'].split('.')) - release = release + 1 if bump_arg == '--release' else release - major = major + 1 if bump_arg == '--major' else major - minor = minor + 1 if bump_arg == '--minor' else minor - new_version = '.'.join(map(str, [release, major, minor])) - new_data = {'version': new_version} - - with open(json_version, 'w') as fout: - json.dump(new_data, fout) - - with open(init_file, 'r') as fin: - file_contents = fin.readlines() - - new_contents = [] - for line in file_contents: - if line.startswith('__version__ = '): - line = '__version__ = {!r}\n'.format(new_version) - new_contents.append(line) - - with open(init_file, 'w') as fout: - fout.writelines(new_contents) - -if __name__ == '__main__': - main(sys.argv[1]) diff --git a/tools/custom-combine.py b/tools/custom-combine.py deleted file mode 100755 index f5805be5..00000000 --- a/tools/custom-combine.py +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env python -"""Simple script provides coverage combining across build chains.""" -# pylint: disable=invalid-name -from __future__ import print_function - -# local -import subprocess -import tempfile -import shutil -import glob -import os - -# 3rd-party -import coverage -import six - -PROJ_ROOT = os.path.join(os.path.dirname(__file__), os.pardir) -COVERAGERC = os.path.join(PROJ_ROOT, '.coveragerc') - -def main(): - """Program entry point.""" - cov = coverage.Coverage(config_file=COVERAGERC) - cov.combine() - - # we must duplicate these files, coverage.py unconditionally - # deletes them on .combine(). - _data_paths = glob.glob(os.path.join(PROJ_ROOT, '._coverage.*')) - dst_folder = tempfile.mkdtemp() - data_paths = [] - for src in _data_paths: - dst = os.path.join(dst_folder, os.path.basename(src)) - shutil.copy(src, dst) - data_paths.append(dst) - - print("combining coverage: {0}".format(data_paths)) - cov.combine(data_paths=data_paths) - cov.load() - cov.html_report(ignore_errors=True) - print("--> open {0}/htmlcov/index.html for review." - .format(os.path.relpath(PROJ_ROOT))) - - fout = six.StringIO() - cov.report(file=fout, ignore_errors=True) - for line in fout.getvalue().splitlines(): - if u'TOTAL' in line: - total_line = line - break - else: - raise ValueError("'TOTAL' summary not found in summary output") - - _, no_stmts, no_miss, _ = total_line.split(None, 3) - no_covered = int(no_stmts) - int(no_miss) - print(fout.getvalue()) - print("##teamcity[buildStatisticValue " - "key='CodeCoverageAbsLTotal' " - "value='{0}']".format(no_stmts)) - print("##teamcity[buildStatisticValue " - "key='CodeCoverageAbsLCovered' " - "value='{0}']".format(no_covered)) - -if __name__ == '__main__': - main() diff --git a/tox.ini b/tox.ini index a574d25b..2e5de0a6 100644 --- a/tox.ini +++ b/tox.ini @@ -1,85 +1,156 @@ [tox] -envlist = about, sa, sphinx, py{26,27,34,35,36,37} +envlist = about + autopep8 + docformatter + isort + pylint + flake8 + sphinx + py{26,27,34,35,36,37,38} skip_missing_interpreters = true [testenv] -whitelist_externals = cp -setenv = PYTHONIOENCODING=UTF8 -passenv = TEST_QUICK TEST_FULL TRAVIS -deps = -rrequirements-tests.txt -commands = {envbindir}/py.test {posargs:\ - --strict --verbose --verbose --color=yes \ - --junit-xml=results.{envname}.xml \ - --cov blessed blessed/tests} - cp {toxinidir}/.coverage \ - {toxinidir}/._coverage.{envname}.{env:COVERAGE_ID:local} - {toxinidir}/tools/custom-combine.py - -# CI buildchain target -[testenv:coverage] -deps = coverage - six -commands = {toxinidir}/tools/custom-combine.py - -# CI buildhcain target -[testenv:coveralls] -passenv = COVERALLS_REPO_TOKEN -deps = coveralls -commands = coveralls +basepython = python3.8 +looponfailroots = blessed +norecursedirs = .git .tox build +deps = pytest==5.3.2 + pytest-cov==2.8.1 + pytest-xdist==1.31.0 + mock==3.0.5 +addopts = --cov-append --cov-report=html --color=yes --ignore=setup.py --ignore=.tox + --log-format='%(levelname)s %(relativeCreated)2.2f %(filename)s:%(lineno)d %(message)s' + --cov=blessed +junit_family = xunit1 +commands = {envbindir}/py.test \ + --disable-pytest-warnings \ + --cov-config={toxinidir}/tox.ini \ + {posargs:\ + --strict --verbose \ + --junit-xml=.tox/results.{envname}.xml \ + --durations=3 \ + } \ + blessed/tests -[testenv:about] -deps = -rrequirements-about.txt -basepython = python3.7 -commands = python {toxinidir}/bin/display-sighandlers.py - python {toxinidir}/bin/display-terminalinfo.py - python {toxinidir}/bin/display-fpathconf.py - # Temporarily disable until limits are added to MAX_CANON logic - # python {toxinidir}/bin/display-maxcanon.py +[coverage:run] +branch = True +source = blessed +parallel = True -[testenv:sa] -basepython = python3.7 -deps = -rrequirements-analysis.txt - -rrequirements-about.txt -commands = python -m compileall -fq {toxinidir}/blessed - {envbindir}/prospector \ - --die-on-tool-error \ - --no-external-config \ - {toxinidir} - {envbindir}/rst-lint README.rst - {envbindir}/doc8 --ignore-path docs/_build --ignore D000 docs +[coverage:report] +omit = blessed/tests/* +exclude_lines = pragma: no cover +precision = 1 -[testenv:sphinx] -whitelist_externals = echo -basepython = python3.7 -deps = -rrequirements-docs.txt -commands = {envbindir}/sphinx-build -v -W \ - -d {toxinidir}/docs/_build/doctrees \ - {posargs:-b html} docs \ - {toxinidir}/docs/_build/html - echo "--> open docs/_build/html/index.html for review." +[coverage:paths] +source = blessed/ -[testenv:py35] -# there is not much difference of py34 vs. 35 in blessed -# library; prefer testing integration against py35, and -# just do a 'quick' on py34, if exists. +[flake8] +# E501: line too long (other tools check line length) +max-line-length = 100 +exclude = .tox +ignore = E501 + +[testenv:py26] setenv = TEST_QUICK=1 +basepython = python2.6 + +[testenv:py27] +basepython = python2.7 +deps = pytest==4.6.9 + pytest-cov==2.8.1 + pytest-xdist==1.31.0 + mock==3.0.5 [testenv:py34] -# there is not much difference of py34 vs. 35 in blessed -# library; prefer testing integration against py35, and -# just do a 'quick' on py34, if exists. setenv = TEST_QUICK=1 +basepython = python3.4 +deps = pytest==4.6.9 + pytest-cov==2.8.1 + pytest-xdist==1.31.0 + mock==3.0.5 -[testenv:py26] -# and python2.6 really only tests 'orderedict' and some various -# backports of import fallback of features +[testenv:py35] setenv = TEST_QUICK=1 +basepython = python3.5 -[pytest] -looponfailroots = blessed -norecursedirs = .git .tox build -junit_family = xunit1 +[testenv:py36] +setenv = TEST_QUICK=1 +basepython = python3.6 + +[testenv:py37] +setenv = TEST_QUICK=1 +basepython = python3.7 + +[testenv:develop] +commands = {posargs} + +[testenv:about] +commands = python {toxinidir}/bin/display-sighandlers.py + python {toxinidir}/bin/display-terminalinfo.py + python {toxinidir}/bin/display-fpathconf.py + +[testenv:autopep8] +deps = autopep8==1.4.4 +commands = + {envbindir}/autopep8 \ + --in-place \ + --recursive \ + --aggressive \ + --aggressive \ + blessed/ bin/ docs/conf.py setup.py + +[testenv:docformatter] +deps = + docformatter==1.3.1 + untokenize==0.1.1 +commands = + {envbindir}/docformatter \ + --in-place \ + --recursive \ + --pre-summary-newline \ + --wrap-summaries=100 \ + --wrap-descriptions=100 \ + {toxinidir}/blessed/ \ + {toxinidir}/bin \ + {toxinidir}/setup.py \ + {toxinidir}/docs/conf.py -[coverage] -rcfile = {toxinidir}/.coveragerc -rc = --rcfile={[coverage]rcfile} +[testenv:isort] +deps = isort==4.3.17 +commands = {envbindir}/isort --quiet --apply --recursive \ + blessed + +[testenv:pylint] +deps = pylint==2.4.4 +commands = {envbindir}/pylint --rcfile={toxinidir}/.pylintrc \ + --ignore=tests,docs,setup.py,conf.py,build,distutils,.pyenv,.git,.tox \ + {posargs:{toxinidir}}/blessed + +[testenv:flake8] +description = Quick and basic Lint using 'flake8' tool +deps = flake8==3.7.9 +commands = {envbindir}/flake8 {toxinidir} + + +[testenv:pydocstyle] +deps = pydocstyle==3.0.0 + restructuredtext_lint + doc8 +commands = {envbindir}/pydocstyle --source --explain \ + --ignore=D101,D212,D203,D204,D401 \ + {toxinidir}/blessed + {envbindir}/rst-lint README.rst + {envbindir}/doc8 --ignore-path docs/_build --ignore D000 docs + +[testenv:sphinx] +deps = Sphinx==2.3.1 + sphinx_rtd_theme + sphinx-paramlinks + jinxed>=0.5.4 +commands = {envbindir}/sphinx-build \ + {posargs:-v -W -d {toxinidir}/docs/_build/doctrees -b html docs {toxinidir}/docs/_build/html} + +[testenv:coveralls] +passenv = TRAVIS TRAVIS_* +deps = coveralls +commands = coveralls diff --git a/version.json b/version.json index f723c478..8c3caff7 100644 --- a/version.json +++ b/version.json @@ -1 +1 @@ -{"version": "2.0.0"} +{"version": "1.17.0"} From 557b108e31177f7397a628fbfdb2372666fc14e5 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Wed, 15 Jan 2020 16:17:25 -0800 Subject: [PATCH 437/459] doc #94 and #101 fixes by @avylove --- docs/history.rst | 4 ++++ docs/intro.rst | 10 +++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/history.rst b/docs/history.rst index b40ce686..8a0a4118 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -4,6 +4,10 @@ Version History * introduced: 24-bit color support, detected by ``term.number_of_colors == 1 << 24``, and 24-bit color foreground method :meth:`~Terminal.color_rgb` and background method :meth:`~Terminal.on_color_rgb`. + * bugfix: off-by-one error in :meth:`~.Terminal.get_location`, now accounts for + ``%i`` in cursor_report, :ghissue:`94`. + * bugfix :meth:`~Terminal.split_seqs` and related functions failed to match when the + color index was greater than 15, :ghissue:`101`. * bugfix: Context Managers, :meth:`~.Terminal.fullscreen`, :meth:`~.Terminal.hidden_cursor`, and :meth:`~Terminal.keypad` now flush the stream after writing their sequences. diff --git a/docs/intro.rst b/docs/intro.rst index ae72b3ec..803382c3 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -10,7 +10,7 @@ :alt: Travis Continuous Integration :target: https://travis-ci.org/jquast/blessed/ -.. |coveralls| image:: https://img.shields.io/coveralls/github/jquast/blessed/master?logo=coveralls +.. |coveralls| image:: https://coveralls.io/repos/github/jquast/blessed/badge.svg?branch=master :alt: Coveralls Code Coverage :target: https://coveralls.io/github/jquast/blessed?branch=master @@ -28,19 +28,19 @@ .. |linux| image:: https://img.shields.io/badge/Linux-yes-success?logo=linux :alt: Linux supported - :target: https://pypi.python.org/pypi/enlighten + :target: https://pypi.python.org/pypi/blessed .. |windows| image:: https://img.shields.io/badge/Windows-NEW-success?logo=windows :alt: Windows supported - :target: https://pypi.python.org/pypi/enlighten + :target: https://pypi.python.org/pypi/blessed .. |mac| image:: https://img.shields.io/badge/MacOS-yes-success?logo=apple :alt: MacOS supported - :target: https://pypi.python.org/pypi/enlighten + :target: https://pypi.python.org/pypi/blessed .. |bsd| image:: https://img.shields.io/badge/BSD-yes-success?logo=freebsd :alt: BSD supported - :target: https://pypi.python.org/pypi/enlighten + :target: https://pypi.python.org/pypi/blessed Introduction ============ From 7ea7bba3283bcba3e78dd333fefb808799154e81 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Wed, 15 Jan 2020 17:09:28 -0800 Subject: [PATCH 438/459] Robot code cleanup: module import order (#120) - some changes to, and result of ``tox -e isort`` with some human fixes mixed in - delete legacy docs/make.bat and docs/Makefile, use tox. - move pylint module-length exception to `.pylintrc` --- .editorconfig | 18 +-- .pylintrc | 2 +- bin/colorchart.py | 3 +- bin/detect-multibyte.py | 9 +- bin/display-fpathconf.py | 4 +- bin/display-maxcanon.py | 5 +- bin/display-sighandlers.py | 2 + bin/display-terminalinfo.py | 8 +- bin/editor.py | 5 +- bin/keymatrix.py | 5 +- bin/on_resize.py | 3 + bin/plasma.py | 8 +- bin/progress_bar.py | 3 + bin/resize.py | 5 +- bin/strip.py | 2 + bin/tprint.py | 2 + bin/worms.py | 8 +- bin/x11_colorpicker.py | 4 + blessed/__init__.py | 1 - blessed/color.py | 3 +- blessed/colorspace.py | 1 + blessed/formatters.py | 10 +- blessed/keyboard.py | 4 +- blessed/sequences.py | 14 +-- blessed/terminal.py | 65 +++++----- blessed/tests/accessories.py | 3 +- blessed/tests/test_core.py | 31 ++--- blessed/tests/test_formatters.py | 4 +- blessed/tests/test_keyboard.py | 48 ++++---- blessed/tests/test_length_sequence.py | 26 ++-- blessed/tests/test_sequences.py | 28 ++--- blessed/tests/test_wrap.py | 12 +- blessed/win_terminal.py | 10 +- docs/Makefile | 130 -------------------- docs/conf.py | 12 +- docs/make.bat | 170 -------------------------- docs/sphinxext/github.py | 5 +- setup.py | 3 + tox.ini | 24 +++- 39 files changed, 205 insertions(+), 495 deletions(-) delete mode 100644 docs/Makefile delete mode 100644 docs/make.bat diff --git a/.editorconfig b/.editorconfig index 616d4b9a..9109cd45 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,9 +1,7 @@ root = true -[*] +[*.json] charset = utf-8 - -[*.sh,*.json,*.ini] tab_width = 4 indent_size = tab indent_space = space @@ -11,21 +9,9 @@ trim_trailing_whitespace = true insert_final_newline = true [*.py] +charset = utf-8 tab_width = 4 indent_size = tab indent_space = space trim_trailing_whitespace = true insert_final_newline = true - -# isort -# https://github.com/timothycrosley/isort/wiki/isort-Settings -line_length = 100 -indent = ' ' -multi_line_output = 1 -length_sort = 1 -sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER -import_heading_stdlib = std imports -import_heading_thirdparty = 3rd party -import_heading_firstparty = local -atomic = true -known_third_party=rapidjson diff --git a/.pylintrc b/.pylintrc index 52f1f1ba..9714cf85 100644 --- a/.pylintrc +++ b/.pylintrc @@ -33,7 +33,7 @@ max-attributes=7 max-branches=12 max-complexity=11 max-locals=15 -max-module-lines=1000 +max-module-lines=1300 max-parents=7 max-public-methods=20 max-returns=6 diff --git a/bin/colorchart.py b/bin/colorchart.py index f83eb71f..ba618439 100644 --- a/bin/colorchart.py +++ b/bin/colorchart.py @@ -1,9 +1,10 @@ +# std imports import re +# local import blessed from blessed.colorspace import X11_COLORNAMES_TO_RGB - RE_NATURAL = re.compile(r'(dark|light|)(.+?)(\d*)$') diff --git a/bin/detect-multibyte.py b/bin/detect-multibyte.py index a085d138..acd79222 100755 --- a/bin/detect-multibyte.py +++ b/bin/detect-multibyte.py @@ -28,14 +28,17 @@ .. image:: _static/soulburner-ru-family-encodings.jpg :alt: Cyrillic encodings flowchart """ + + # pylint: disable=invalid-name # Invalid module name "detect-multibyte" -# std imports from __future__ import print_function -import collections + +# std imports import sys +import collections -# local, +# local from blessed import Terminal diff --git a/bin/display-fpathconf.py b/bin/display-fpathconf.py index 84ced9c8..34ca525a 100755 --- a/bin/display-fpathconf.py +++ b/bin/display-fpathconf.py @@ -3,8 +3,10 @@ # pylint: disable=invalid-name # Invalid module name "display-sighandlers" from __future__ import print_function -import sys + +# std imports import os +import sys def display_fpathconf(): diff --git a/bin/display-maxcanon.py b/bin/display-maxcanon.py index adc9a33a..09a1a7f5 100755 --- a/bin/display-maxcanon.py +++ b/bin/display-maxcanon.py @@ -24,10 +24,11 @@ """ # pylint: disable=invalid-name # Invalid module name "display-sighandlers" -# std import from __future__ import print_function -import sys + +# std imports import os +import sys def detect_maxcanon(): diff --git a/bin/display-sighandlers.py b/bin/display-sighandlers.py index fbb4c800..aaf09e2f 100755 --- a/bin/display-sighandlers.py +++ b/bin/display-sighandlers.py @@ -3,6 +3,8 @@ # pylint: disable=invalid-name # Invalid module name "display-sighandlers" from __future__ import print_function + +# std imports import signal diff --git a/bin/display-terminalinfo.py b/bin/display-terminalinfo.py index 861ee6c0..fc8116a3 100755 --- a/bin/display-terminalinfo.py +++ b/bin/display-terminalinfo.py @@ -3,10 +3,12 @@ # pylint: disable=invalid-name # Invalid module name "display-terminalinfo" from __future__ import print_function -import termios -import locale -import sys + +# std imports import os +import sys +import locale +import termios BITMAP_IFLAG = { 'IGNBRK': 'ignore BREAK condition', diff --git a/bin/editor.py b/bin/editor.py index ac626dad..fb5f64b1 100755 --- a/bin/editor.py +++ b/bin/editor.py @@ -20,9 +20,12 @@ save """ from __future__ import division, print_function -import collections + +# std imports import functools +import collections +# local from blessed import Terminal # python 2/3 compatibility, provide 'echo' function as an diff --git a/bin/keymatrix.py b/bin/keymatrix.py index 9496a959..5104dcfe 100755 --- a/bin/keymatrix.py +++ b/bin/keymatrix.py @@ -6,9 +6,12 @@ As each key is pressed on input, it is lit up and points are scored. """ from __future__ import division, print_function -import functools + +# std imports import sys +import functools +# local from blessed import Terminal # python 2/3 compatibility, provide 'echo' function as an diff --git a/bin/on_resize.py b/bin/on_resize.py index a5cabfe7..110f6df0 100755 --- a/bin/on_resize.py +++ b/bin/on_resize.py @@ -8,8 +8,11 @@ term.inkey(). """ from __future__ import print_function + +# std imports import signal +# local from blessed import Terminal diff --git a/bin/plasma.py b/bin/plasma.py index 0d88d605..c2036dd3 100755 --- a/bin/plasma.py +++ b/bin/plasma.py @@ -1,12 +1,12 @@ #!/usr/bin/env python # std imports +import sys import math +import time +import timeit import colorsys -import collections import contextlib -import timeit -import time -import sys +import collections # local import blessed diff --git a/bin/progress_bar.py b/bin/progress_bar.py index 0bbca0fe..94912475 100755 --- a/bin/progress_bar.py +++ b/bin/progress_bar.py @@ -9,8 +9,11 @@ provides a proxy. """ from __future__ import print_function + +# std imports import sys +# local from blessed import Terminal diff --git a/bin/resize.py b/bin/resize.py index 4584e7ed..8a3a98a8 100755 --- a/bin/resize.py +++ b/bin/resize.py @@ -29,10 +29,11 @@ `_ provided by the xterm package. """ -# std imports from __future__ import print_function -import collections + +# std imports import sys +import collections # local from blessed import Terminal diff --git a/bin/strip.py b/bin/strip.py index 9a47e7fb..1229ab73 100755 --- a/bin/strip.py +++ b/bin/strip.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 """Example scrip that strips input of terminal sequences.""" +# std imports import sys +# local import blessed diff --git a/bin/tprint.py b/bin/tprint.py index 8bc1488a..5e0935e9 100755 --- a/bin/tprint.py +++ b/bin/tprint.py @@ -9,6 +9,8 @@ """ # std from __future__ import print_function + +# std imports import argparse # local diff --git a/bin/worms.py b/bin/worms.py index 32b56ff5..636992be 100755 --- a/bin/worms.py +++ b/bin/worms.py @@ -6,13 +6,15 @@ """ from __future__ import division, print_function -from collections import namedtuple -from functools import partial + +# std imports from random import randrange +from functools import partial +from collections import namedtuple +# local from blessed import Terminal - # python 2/3 compatibility, provide 'echo' function as an # alias for "print without newline and flush" try: diff --git a/bin/x11_colorpicker.py b/bin/x11_colorpicker.py index 37562d62..e41ed8f2 100644 --- a/bin/x11_colorpicker.py +++ b/bin/x11_colorpicker.py @@ -1,6 +1,10 @@ +# std imports import colorsys + +# local import blessed + def sort_colors(): colors = {} for color_name, rgb_color in blessed.colorspace.X11_COLORNAMES_TO_RGB.items(): diff --git a/blessed/__init__.py b/blessed/__init__.py index e4fec9fc..ad307dfa 100644 --- a/blessed/__init__.py +++ b/blessed/__init__.py @@ -6,7 +6,6 @@ # std imports import platform as _platform -# local if _platform.system() == 'Windows': from blessed.win_terminal import Terminal else: diff --git a/blessed/color.py b/blessed/color.py index 3c429ee9..bb595c51 100644 --- a/blessed/color.py +++ b/blessed/color.py @@ -9,7 +9,8 @@ """ -from math import atan2, cos, exp, sin, sqrt +# std imports +from math import cos, exp, sin, sqrt, atan2 try: from functools import lru_cache diff --git a/blessed/colorspace.py b/blessed/colorspace.py index c40a41f7..6beef4b9 100644 --- a/blessed/colorspace.py +++ b/blessed/colorspace.py @@ -9,6 +9,7 @@ - https://devblogs.microsoft.com/commandline/24-bit-color-in-the-windows-console/ - http://jdebp.eu/Softwares/nosh/guide/TerminalCapabilities.html """ +# std imports import collections __all__ = ( diff --git a/blessed/formatters.py b/blessed/formatters.py index 629f37db..6097bda8 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -1,13 +1,13 @@ """Sub-module providing sequence-formatting functions.""" -# standard imports +# std imports import platform -# local -from blessed.colorspace import X11_COLORNAMES_TO_RGB, CGA_COLORS - -# 3rd-party +# 3rd party import six +# local +from blessed.colorspace import CGA_COLORS, X11_COLORNAMES_TO_RGB + # curses if platform.system() == 'Windows': import jinxed as curses # pylint: disable=import-error diff --git a/blessed/keyboard.py b/blessed/keyboard.py index d96e6d17..78e5d1f4 100644 --- a/blessed/keyboard.py +++ b/blessed/keyboard.py @@ -1,9 +1,9 @@ """Sub-module providing 'keyboard awareness'.""" # std imports -import platform -import time import re +import time +import platform # 3rd party import six diff --git a/blessed/sequences.py b/blessed/sequences.py index e2aef824..0bf318f0 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -1,17 +1,17 @@ # encoding: utf-8 """Module providing 'sequence awareness'.""" # std imports -import functools -import textwrap -import math import re - -# local -from blessed._capabilities import CAPABILITIES_CAUSE_MOVEMENT +import math +import textwrap +import functools # 3rd party -import wcwidth import six +import wcwidth + +# local +from blessed._capabilities import CAPABILITIES_CAUSE_MOVEMENT __all__ = ('Sequence', 'SequenceTextWrapper', 'iter_parse', 'measure_length') diff --git a/blessed/terminal.py b/blessed/terminal.py index 389faab1..2cd8270d 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -1,21 +1,38 @@ # encoding: utf-8 """Module containing :class:`Terminal`, the primary API entry point.""" -# pylint: disable=too-many-lines -# Too many lines in module (1027/1000) -import codecs -import collections -import contextlib -import functools +# std imports import io -import locale import os -import platform -import select -import struct +import re import sys import time +import codecs +import locale +import select +import struct +import platform import warnings -import re +import functools +import contextlib +import collections + +# local +from .color import COLOR_DISTANCE_ALGORITHMS +from .keyboard import (_time_left, + _read_until, + resolve_sequence, + get_keyboard_codes, + get_leading_prefixes, + get_keyboard_sequences) +from .sequences import Termcap, Sequence, SequenceTextWrapper +from .colorspace import RGB_256TABLE +from .formatters import (COLORS, + FormattingString, + NullCallableString, + ParameterizingString, + resolve_attribute, + resolve_capability) +from ._capabilities import CAPABILITY_DATABASE, CAPABILITIES_ADDITIVES, CAPABILITIES_RAW_MIXIN try: InterruptedError @@ -33,36 +50,10 @@ # Unable to import 'ordereddict' from ordereddict import OrderedDict -# local imports -from .formatters import (ParameterizingString, - NullCallableString, - resolve_capability, - resolve_attribute, - FormattingString, - COLORS, - ) -from ._capabilities import ( - CAPABILITIES_RAW_MIXIN, - CAPABILITIES_ADDITIVES, - CAPABILITY_DATABASE, -) -from .sequences import (SequenceTextWrapper, - Sequence, - Termcap, - ) -from .keyboard import (get_keyboard_sequences, - get_leading_prefixes, - get_keyboard_codes, - resolve_sequence, - _read_until, - _time_left, - ) -from .color import COLOR_DISTANCE_ALGORITHMS -from .colorspace import RGB_256TABLE if platform.system() == 'Windows': import jinxed as curses # pylint: disable=import-error diff --git a/blessed/tests/accessories.py b/blessed/tests/accessories.py index ed22f254..0e3a5c93 100644 --- a/blessed/tests/accessories.py +++ b/blessed/tests/accessories.py @@ -17,10 +17,9 @@ # 3rd party import six +import pytest # local -# 3rd-party -import pytest from blessed import Terminal TestTerminal = functools.partial(Terminal, kind='xterm-256color') diff --git a/blessed/tests/test_core.py b/blessed/tests/test_core.py index b9fc7da4..0deda370 100644 --- a/blessed/tests/test_core.py +++ b/blessed/tests/test_core.py @@ -1,31 +1,26 @@ # -*- coding: utf-8 -*- "Core blessed Terminal() tests." -# std -import collections -import warnings -import platform -import locale -import time -import math -import sys -import os +# std imports import io - -# local -from .accessories import ( - as_subprocess, - TestTerminal, - unicode_cap, - all_terms -) +import os +import sys +import math +import time +import locale +import platform +import warnings +import collections # 3rd party +import six import mock import pytest -import six from six.moves import reload_module +# local +from .accessories import TestTerminal, all_terms, unicode_cap, as_subprocess + def test_export_only_Terminal(): "Ensure only Terminal instance is exported for import * statements." diff --git a/blessed/tests/test_formatters.py b/blessed/tests/test_formatters.py index fa439d33..35036095 100644 --- a/blessed/tests/test_formatters.py +++ b/blessed/tests/test_formatters.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- """Tests string formatting functions.""" -# std +# std imports import curses -# 3rd-party +# 3rd party import mock import pytest diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index c61d6393..72276eae 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -1,35 +1,33 @@ # -*- coding: utf-8 -*- "Tests for keyboard support." # std imports -import functools -import tempfile -import signal -import curses -#import time -import math -import tty # NOQA +import os import pty import sys -import os +import tty # NOQA +#import time +import math +import curses +import signal +import tempfile +import functools -# local -from .accessories import ( - init_subproc_coverage, - read_until_eof, - read_until_semaphore, - SEND_SEMAPHORE, - RECV_SEMAPHORE, - as_subprocess, - TestTerminal, - SEMAPHORE, - all_terms, - echo_off, -) - -# 3rd-party -import pytest -import mock +# 3rd party import six +import mock +import pytest + +# local +from .accessories import (SEMAPHORE, + RECV_SEMAPHORE, + SEND_SEMAPHORE, + TestTerminal, + echo_off, + all_terms, + as_subprocess, + read_until_eof, + read_until_semaphore, + init_subproc_coverage) if sys.version_info[0] == 3: unichr = chr diff --git a/blessed/tests/test_length_sequence.py b/blessed/tests/test_length_sequence.py index 8efa697a..d3439cfc 100644 --- a/blessed/tests/test_length_sequence.py +++ b/blessed/tests/test_length_sequence.py @@ -1,25 +1,20 @@ # encoding: utf-8 # std imports -import itertools -import termios -import struct -import fcntl -import sys import os - -# local -from blessed.tests.accessories import ( - all_terms, - as_subprocess, - TestTerminal, - many_columns, - many_lines, -) +import sys +import fcntl +import struct +import termios +import itertools # 3rd party -import pytest import six +# local +from blessed.tests.accessories import ( # isort:skip + TestTerminal, as_subprocess, all_terms, many_lines, many_columns +) + def test_length_cjk(): @as_subprocess @@ -40,7 +35,6 @@ def test_length_ansiart(): @as_subprocess def child(): import codecs - from blessed.sequences import Sequence term = TestTerminal(kind='xterm-256color') # this 'ansi' art contributed by xzip!impure for another project, # unlike most CP-437 DOS ansi art, this is actually utf-8 encoded. diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index f1e75280..c19300ae 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -1,25 +1,23 @@ # -*- coding: utf-8 -*- """Tests for Terminal() sequences and sequence-awareness.""" # std imports -import platform -import random -import sys import os - -# local -from .accessories import ( - all_terms, - as_subprocess, - TestTerminal, - unicode_parm, - many_columns, - unicode_cap, -) +import sys +import random +import platform # 3rd party -import pytest -import mock import six +import mock +import pytest + +# local +from .accessories import (TestTerminal, + all_terms, + unicode_cap, + many_columns, + unicode_parm, + as_subprocess) def test_capability(): diff --git a/blessed/tests/test_wrap.py b/blessed/tests/test_wrap.py index f23559c7..1c6f3df5 100644 --- a/blessed/tests/test_wrap.py +++ b/blessed/tests/test_wrap.py @@ -1,17 +1,13 @@ -# std +# std imports import os import textwrap -# local -from .accessories import ( - as_subprocess, - TestTerminal, - many_columns, -) - # 3rd party import pytest +# local +from .accessories import TestTerminal, many_columns, as_subprocess + TEXTWRAP_KEYWORD_COMBINATIONS = [ dict(break_long_words=False, drop_whitespace=False, diff --git a/blessed/win_terminal.py b/blessed/win_terminal.py index 58c51a62..68cdabe2 100644 --- a/blessed/win_terminal.py +++ b/blessed/win_terminal.py @@ -3,13 +3,17 @@ from __future__ import absolute_import -import contextlib -import msvcrt # pylint: disable=import-error +# std imports import time +import msvcrt # pylint: disable=import-error +import contextlib +# 3rd party import jinxed.win32 as win32 # pylint: disable=import-error -from .terminal import WINSZ, Terminal as _Terminal +# local +from .terminal import WINSZ +from .terminal import Terminal as _Terminal class Terminal(_Terminal): diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index 47febae6..00000000 --- a/docs/Makefile +++ /dev/null @@ -1,130 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = _build - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - -rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/blessed.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/blessed.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/blessed" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/blessed" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - make -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." diff --git a/docs/conf.py b/docs/conf.py index 7625622f..b4448885 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,9 +1,10 @@ # std imports -import sys import os +import sys import json +import functools -# 3rd-party +# local import sphinx_rtd_theme import sphinx.environment from docutils.utils import get_source_line @@ -37,23 +38,22 @@ def _warn_node(self, msg, node): # Monkey-patch functools.wraps and contextlib.wraps # https://github.com/sphinx-doc/sphinx/issues/1711#issuecomment-93126473 -import functools def no_op_wraps(func): """ Replaces functools.wraps in order to undo wrapping when generating Sphinx documentation """ - import sys if func.__module__ is None or 'blessed' not in func.__module__: return functools.orig_wraps(func) def wrapper(decorator): sys.stderr.write('patched for function signature: {0!r}\n'.format(func)) return func return wrapper + functools.orig_wraps = functools.wraps functools.wraps = no_op_wraps -import contextlib +import contextlib # isort:skip contextlib.wraps = no_op_wraps -from blessed.terminal import * +from blessed.terminal import * # isort:skip # -- General configuration ---------------------------------------------------- diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 21ff37b3..00000000 --- a/docs/make.bat +++ /dev/null @@ -1,170 +0,0 @@ -@ECHO OFF - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set BUILDDIR=_build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% -) - -if "%1" == "" goto help - -if "%1" == "help" ( - :help - echo.Please use `make ^` where ^ is one of - echo. html to make standalone HTML files - echo. dirhtml to make HTML files named index.html in directories - echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. changes to make an overview over all changed/added/deprecated items - echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end -) - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\blessed.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\blessed.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - if errorlevel 1 exit /b 1 - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - -:end diff --git a/docs/sphinxext/github.py b/docs/sphinxext/github.py index 7533e4f1..40ae5339 100644 --- a/docs/sphinxext/github.py +++ b/docs/sphinxext/github.py @@ -17,11 +17,10 @@ # Original Copyright (c) 2010 Doug Hellmann. All rights reserved. # +# local from docutils import nodes, utils -from docutils.parsers.rst.roles import set_classes - from sphinx.util import logging - +from docutils.parsers.rst.roles import set_classes LOGGER = logging.getLogger(__name__) diff --git a/setup.py b/setup.py index b76c20a7..86bcd83d 100755 --- a/setup.py +++ b/setup.py @@ -1,6 +1,9 @@ #!/usr/bin/env python """Distutils setup script.""" +# std imports import os + +# 3rd party import setuptools diff --git a/tox.ini b/tox.ini index 2e5de0a6..21aa2363 100644 --- a/tox.ini +++ b/tox.ini @@ -44,11 +44,23 @@ precision = 1 [coverage:paths] source = blessed/ +[isort] +line_length = 100 +indent = ' ' +multi_line_output = 1 +length_sort = 1 +import_heading_stdlib = std imports +import_heading_thirdparty = 3rd party +import_heading_firstparty = local +import_heading_localfolder = local +sections=FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER +no_lines_before=LOCALFOLDER +known_third_party = jinxed +atomic = true + [flake8] -# E501: line too long (other tools check line length) max-line-length = 100 exclude = .tox -ignore = E501 [testenv:py26] setenv = TEST_QUICK=1 @@ -97,7 +109,7 @@ commands = --recursive \ --aggressive \ --aggressive \ - blessed/ bin/ docs/conf.py setup.py + blessed/ bin/ setup.py [testenv:docformatter] deps = @@ -116,9 +128,9 @@ commands = {toxinidir}/docs/conf.py [testenv:isort] -deps = isort==4.3.17 -commands = {envbindir}/isort --quiet --apply --recursive \ - blessed +deps = {[testenv]deps} + isort==4.3.21 +commands = {envbindir}/isort --quiet --apply --recursive [testenv:pylint] deps = pylint==2.4.4 From 42eebecac80e921e503f9e9d6561bdabc87dfbc8 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Wed, 15 Jan 2020 17:12:21 -0800 Subject: [PATCH 439/459] tox -e autopep8 results --- bin/plasma.py | 46 ++- bin/x11_colorpicker.py | 4 +- blessed/colorspace.py | 2 + blessed/formatters.py | 3 + blessed/keyboard.py | 22 +- blessed/sequences.py | 1 + blessed/terminal.py | 6 - blessed/tests/test_core.py | 2 +- blessed/tests/test_formatters.py | 46 +-- blessed/tests/test_keyboard.py | 567 +------------------------- blessed/tests/test_length_sequence.py | 23 +- blessed/tests/test_sequences.py | 3 +- blessed/tests/test_wrap.py | 2 +- 13 files changed, 98 insertions(+), 629 deletions(-) diff --git a/bin/plasma.py b/bin/plasma.py index c2036dd3..4a8a7384 100755 --- a/bin/plasma.py +++ b/bin/plasma.py @@ -11,29 +11,33 @@ # local import blessed -scale_255 = lambda val: int(round(val * 255)) + +def scale_255(val): return int(round(val * 255)) + def rgb_at_xy(term, x, y, t): h, w = term.height, term.width hue = 4.0 + ( math.sin(x / 16.0) - + math.sin(y / 32.0) - + math.sin(math.sqrt( - ((x - w / 2.0) * (x - w / 2.0) + - (y - h / 2.0) * (y - h / 2.0)) - ) / 8.0 + t*3) + + math.sin(y / 32.0) + + math.sin(math.sqrt( + ((x - w / 2.0) * (x - w / 2.0) + + (y - h / 2.0) * (y - h / 2.0)) + ) / 8.0 + t * 3) ) + math.sin(math.sqrt((x * x + y * y)) / 8.0) saturation = y / h lightness = x / w return tuple(map(scale_255, colorsys.hsv_to_rgb(hue / 8.0, saturation, lightness))) + def screen_plasma(term, plasma_fn, t): result = '' for y in range(term.height - 1): - for x in range (term.width): + for x in range(term.width): result += term.on_color_rgb(*plasma_fn(term, x, y, t)) + ' ' return result + @contextlib.contextmanager def elapsed_timer(): """Timer pattern, from https://stackoverflow.com/a/30024601.""" @@ -44,20 +48,23 @@ def elapser(): # pylint: disable=unnecessary-lambda yield lambda: elapser() - + + def show_please_wait(term): txt_wait = 'please wait ...' - outp = term.move(term.height-1, 0) + term.clear_eol + term.center(txt_wait) + outp = term.move(term.height - 1, 0) + term.clear_eol + term.center(txt_wait) print(outp, end='') sys.stdout.flush() + def show_paused(term): txt_paused = 'paused' - outp = term.move(term.height-1, int(term.width/2 - len(txt_paused)/2)) + outp = term.move(term.height - 1, int(term.width / 2 - len(txt_paused) / 2)) outp += txt_paused print(outp, end='') sys.stdout.flush() + def next_algo(algo, forward): algos = tuple(sorted(blessed.color.COLOR_DISTANCE_ALGORITHMS)) next_index = algos.index(algo) + (1 if forward else -1) @@ -65,6 +72,7 @@ def next_algo(algo, forward): next_index = 0 return algos[next_index] + def next_color(color, forward): colorspaces = (4, 8, 16, 256, 1 << 24) next_index = colorspaces.index(color) + (1 if forward else -1) @@ -72,13 +80,15 @@ def next_color(color, forward): next_index = 0 return colorspaces[next_index] + def status(term, elapsed): left_txt = (f'{term.number_of_colors} colors - ' f'{term.color_distance_algorithm} - ?: help ') right_txt = f'fps: {1 / elapsed:2.2f}' return ('\n' + term.normal + - term.white_on_blue + term.clear_eol + left_txt + - term.rjust(right_txt, term.width-len(left_txt))) + term.white_on_blue + term.clear_eol + left_txt + + term.rjust(right_txt, term.width - len(left_txt))) + def main(term): with term.cbreak(), term.hidden_cursor(), term.fullscreen(): @@ -98,19 +108,23 @@ def main(term): show_paused(term) inp = term.inkey(timeout=0.01 if not pause else None) - if inp == '?': assert False, "don't panic" - if inp == '\x0c': dirty = True + if inp == '?': + assert False, "don't panic" + if inp == '\x0c': + dirty = True if inp in ('[', ']'): term.color_distance_algorithm = next_algo( term.color_distance_algorithm, inp == '[') show_please_wait(term) dirty = True - if inp == ' ': pause = not pause + if inp == ' ': + pause = not pause if inp.code in (term.KEY_TAB, term.KEY_BTAB): term.number_of_colors = next_color( - term.number_of_colors, inp.code==term.KEY_TAB) + term.number_of_colors, inp.code == term.KEY_TAB) show_please_wait(term) dirty = True + if __name__ == "__main__": exit(main(blessed.Terminal())) diff --git a/bin/x11_colorpicker.py b/bin/x11_colorpicker.py index e41ed8f2..acb45c9a 100644 --- a/bin/x11_colorpicker.py +++ b/bin/x11_colorpicker.py @@ -39,6 +39,7 @@ def render(term, idx): result += term.on_color_rgb(*rgb_color)(' \b') return result + def next_algo(algo, forward): algos = tuple(sorted(blessed.color.COLOR_DISTANCE_ALGORITHMS)) next_index = algos.index(algo) + (1 if forward else -1) @@ -77,7 +78,7 @@ def main(): idx += 1 elif inp.code in (term.KEY_TAB, term.KEY_BTAB): term.number_of_colors = next_color( - term.number_of_colors, inp.code==term.KEY_TAB) + term.number_of_colors, inp.code == term.KEY_TAB) elif inp in ('[', ']'): term.color_distance_algorithm = next_algo( term.color_distance_algorithm, inp == '[') @@ -91,5 +92,6 @@ def main(): while idx >= len(HSV_SORTED_COLORS): idx -= len(HSV_SORTED_COLORS) + if __name__ == '__main__': main() diff --git a/blessed/colorspace.py b/blessed/colorspace.py index 6beef4b9..0b0e972f 100644 --- a/blessed/colorspace.py +++ b/blessed/colorspace.py @@ -22,10 +22,12 @@ CGA_COLORS = set( ('black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white')) + class RGBColor(collections.namedtuple("RGBColor", ["red", "green", "blue"])): def __str__(self): return '#{0:02x}{1:02x}{2:02x}'.format(*self) + #: X11 Color names to (XTerm-defined) RGB values from xorg-rgb/rgb.txt X11_COLORNAMES_TO_RGB = { 'aliceblue': RGBColor(240, 248, 255), diff --git a/blessed/formatters.py b/blessed/formatters.py index 6097bda8..309abea9 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -14,6 +14,7 @@ else: import curses + def _make_colors(): """ Return set of valid colors and their derivatives. @@ -35,6 +36,7 @@ def _make_colors(): colors.add('on_' + vga_color) return colors + #: Valid colors and their background (on), bright, and bright-background #: derivatives. COLORS = _make_colors() @@ -43,6 +45,7 @@ def _make_colors(): #: 'reverse_indigo'. COMPOUNDABLES = set('bold underline reverse blink italic standout'.split()) + class ParameterizingString(six.text_type): r""" A Unicode string which can be called as a parameterizing termcap. diff --git a/blessed/keyboard.py b/blessed/keyboard.py index 78e5d1f4..cf90cc4c 100644 --- a/blessed/keyboard.py +++ b/blessed/keyboard.py @@ -329,9 +329,25 @@ def _read_until(term, pattern, timeout): #: Furthermore, many key-names for application keys enabled only by context #: manager :meth:`~.Terminal.keypad` are surprisingly absent. We inject them #: here directly into the curses module. -_CURSES_KEYCODE_ADDINS = ('TAB', 'KP_MULTIPLY', 'KP_ADD', 'KP_SEPARATOR', 'KP_SUBTRACT', 'KP_DECIMAL', - 'KP_DIVIDE', 'KP_EQUAL', 'KP_0', 'KP_1', 'KP_2', 'KP_3', 'KP_4', 'KP_5', 'KP_6', - 'KP_7', 'KP_8', 'KP_9') +_CURSES_KEYCODE_ADDINS = ( + 'TAB', + 'KP_MULTIPLY', + 'KP_ADD', + 'KP_SEPARATOR', + 'KP_SUBTRACT', + 'KP_DECIMAL', + 'KP_DIVIDE', + 'KP_EQUAL', + 'KP_0', + 'KP_1', + 'KP_2', + 'KP_3', + 'KP_4', + 'KP_5', + 'KP_6', + 'KP_7', + 'KP_8', + 'KP_9') _lastval = max(get_curses_keycodes().values()) for keycode_name in _CURSES_KEYCODE_ADDINS: diff --git a/blessed/sequences.py b/blessed/sequences.py index 0bf318f0..191d7f8b 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -18,6 +18,7 @@ class Termcap(object): """Terminal capability of given variable name and pattern.""" + def __init__(self, name, pattern, attribute): """ Class initializer. diff --git a/blessed/terminal.py b/blessed/terminal.py index 2cd8270d..6fb6cca4 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -51,10 +51,6 @@ from ordereddict import OrderedDict - - - - if platform.system() == 'Windows': import jinxed as curses # pylint: disable=import-error HAS_TTY = True @@ -771,7 +767,6 @@ def number_of_colors(self, value): self._number_of_colors = value self.__clear_color_capabilities() - @property def color_distance_algorithm(self): """ @@ -788,7 +783,6 @@ def color_distance_algorithm(self, value): self._color_distance_algorithm = value self.__clear_color_capabilities() - @property def _foreground_color(self): """ diff --git a/blessed/tests/test_core.py b/blessed/tests/test_core.py index 0deda370..1c7b9c24 100644 --- a/blessed/tests/test_core.py +++ b/blessed/tests/test_core.py @@ -472,7 +472,7 @@ def test_time_left_infinite_None(): def test_termcap_repr(): "Ensure ``hidden_cursor()`` writes hide_cursor and normal_cursor." - given_ttype='vt220' + given_ttype = 'vt220' given_capname = 'cursor_up' expected = [r"", r"", diff --git a/blessed/tests/test_formatters.py b/blessed/tests/test_formatters.py index 35036095..3ee0cf21 100644 --- a/blessed/tests/test_formatters.py +++ b/blessed/tests/test_formatters.py @@ -29,13 +29,13 @@ def test_parameterizing_string_args_unspecified(monkeypatch): # exercise __call__ zero = pstr(0) - assert type(zero) is FormattingString + assert isinstance(zero, FormattingString) assert zero == u'~0' assert zero('text') == u'~0text' # exercise __call__ with multiple args onetwo = pstr(1, 2) - assert type(onetwo) is FormattingString + assert isinstance(onetwo, FormattingString) assert onetwo == u'~1~2' assert onetwo('text') == u'~1~2text' @@ -62,13 +62,13 @@ def test_parameterizing_string_args(monkeypatch): # exercise __call__ zero = pstr(0) - assert type(zero) is FormattingString + assert isinstance(zero, FormattingString) assert zero == u'cap~0' assert zero('text') == u'cap~0textnorm' # exercise __call__ with multiple args onetwo = pstr(1, 2) - assert type(onetwo) is FormattingString + assert isinstance(onetwo, FormattingString) assert onetwo == u'cap~1~2' assert onetwo('text') == u'cap~1~2textnorm' @@ -188,7 +188,7 @@ def test_resolve_capability(monkeypatch): from blessed.formatters import resolve_capability # given, always returns a b'seq' - tigetstr = lambda attr: ('seq-%s' % (attr,)).encode('latin1') + def tigetstr(attr): return ('seq-%s' % (attr,)).encode('latin1') monkeypatch.setattr(curses, 'tigetstr', tigetstr) term = mock.Mock() term._sugar = dict(mnemonic='xyz') @@ -198,7 +198,7 @@ def test_resolve_capability(monkeypatch): assert resolve_capability(term, 'natural') == u'seq-natural' # given, where tigetstr returns None - tigetstr_none = lambda attr: None + def tigetstr_none(attr): return None monkeypatch.setattr(curses, 'tigetstr', tigetstr_none) # exercise, @@ -217,10 +217,10 @@ def raises_exception(*args): def test_resolve_color(monkeypatch): """Test formatters.resolve_color.""" from blessed.formatters import (resolve_color, - FormattingString, - NullCallableString) + FormattingString, + NullCallableString) - color_cap = lambda digit: 'seq-%s' % (digit,) + def color_cap(digit): return 'seq-%s' % (digit,) monkeypatch.setattr(curses, 'COLOR_RED', 1984) # given, terminal with color capabilities @@ -232,13 +232,13 @@ def test_resolve_color(monkeypatch): # exercise, red = resolve_color(term, 'red') - assert type(red) == FormattingString + assert isinstance(red, FormattingString) assert red == u'seq-1984' assert red('text') == u'seq-1984textseq-normal' # exercise bold, +8 bright_red = resolve_color(term, 'bright_red') - assert type(bright_red) == FormattingString + assert isinstance(bright_red, FormattingString) assert bright_red == u'seq-1992' assert bright_red('text') == u'seq-1992textseq-normal' @@ -247,13 +247,13 @@ def test_resolve_color(monkeypatch): # exercise, red = resolve_color(term, 'red') - assert type(red) == NullCallableString + assert isinstance(red, NullCallableString) assert red == u'' assert red('text') == u'text' # exercise bold, bright_red = resolve_color(term, 'bright_red') - assert type(bright_red) == NullCallableString + assert isinstance(bright_red, NullCallableString) assert bright_red == u'' assert bright_red('text') == u'text' @@ -263,7 +263,7 @@ def test_resolve_attribute_as_color(monkeypatch): import blessed from blessed.formatters import resolve_attribute - resolve_color = lambda term, digit: 'seq-%s' % (digit,) + def resolve_color(term, digit): return 'seq-%s' % (digit,) COLORS = set(['COLORX', 'COLORY']) COMPOUNDABLES = set(['JOINT', 'COMPOUND']) monkeypatch.setattr(blessed.formatters, 'resolve_color', resolve_color) @@ -278,7 +278,7 @@ def test_resolve_attribute_as_compoundable(monkeypatch): import blessed from blessed.formatters import resolve_attribute, FormattingString - resolve_cap = lambda term, digit: 'seq-%s' % (digit,) + def resolve_cap(term, digit): return 'seq-%s' % (digit,) COMPOUNDABLES = set(['JOINT', 'COMPOUND']) monkeypatch.setattr(blessed.formatters, 'resolve_capability', @@ -288,7 +288,7 @@ def test_resolve_attribute_as_compoundable(monkeypatch): term.normal = 'seq-normal' compound = resolve_attribute(term, 'JOINT') - assert type(compound) is FormattingString + assert isinstance(compound, FormattingString) assert str(compound) == u'seq-JOINT' assert compound('text') == u'seq-JOINTtextseq-normal' @@ -297,8 +297,8 @@ def test_resolve_attribute_non_compoundables(monkeypatch): """ Test recursive compounding of resolve_attribute(). """ import blessed from blessed.formatters import resolve_attribute, ParameterizingString - uncompoundables = lambda attr: ['split', 'compound'] - resolve_cap = lambda term, digit: 'seq-%s' % (digit,) + def uncompoundables(attr): return ['split', 'compound'] + def resolve_cap(term, digit): return 'seq-%s' % (digit,) monkeypatch.setattr(blessed.formatters, 'split_compound', uncompoundables) @@ -315,7 +315,7 @@ def test_resolve_attribute_non_compoundables(monkeypatch): # given pstr = resolve_attribute(term, 'not-a-compoundable') - assert type(pstr) == ParameterizingString + assert isinstance(pstr, ParameterizingString) assert str(pstr) == u'seq-not-a-compoundable' # this is like calling term.move_x(3) assert pstr(3) == u'seq-not-a-compoundable~3' @@ -329,7 +329,7 @@ def test_resolve_attribute_recursive_compoundables(monkeypatch): from blessed.formatters import resolve_attribute, FormattingString # patch, - resolve_cap = lambda term, digit: 'seq-%s' % (digit,) + def resolve_cap(term, digit): return 'seq-%s' % (digit,) monkeypatch.setattr(blessed.formatters, 'resolve_capability', resolve_cap) @@ -340,7 +340,7 @@ def test_resolve_attribute_recursive_compoundables(monkeypatch): monkeypatch.setattr(curses, 'COLOR_RED', 6502) monkeypatch.setattr(curses, 'COLOR_BLUE', 6800) - color_cap = lambda digit: 'seq-%s' % (digit,) + def color_cap(digit): return 'seq-%s' % (digit,) term = mock.Mock() term._background_color = color_cap term._foreground_color = color_cap @@ -350,7 +350,7 @@ def test_resolve_attribute_recursive_compoundables(monkeypatch): pstr = resolve_attribute(term, 'bright_blue_on_red') # exercise, - assert type(pstr) == FormattingString + assert isinstance(pstr, FormattingString) assert str(pstr) == 'seq-6808seq-6502' assert pstr('text') == 'seq-6808seq-6502textseq-normal' @@ -410,7 +410,7 @@ def tparm(*args): pstr = ParameterizingString(u'cap', u'norm', u'seq-name') value = pstr(u'x') - assert type(value) is NullCallableString + assert isinstance(value, NullCallableString) def test_tparm_other_exception(monkeypatch): diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index 72276eae..fe6fc899 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -33,91 +33,6 @@ unichr = chr -#@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, -# reason="TEST_QUICK specified") -#def test_kbhit_interrupted(): -# "kbhit() should not be interrupted with a signal handler." -# pid, master_fd = pty.fork() -# if pid == 0: -# cov = init_subproc_coverage('test_kbhit_interrupted') -# -# # child pauses, writes semaphore and begins awaiting input -# global got_sigwinch -# got_sigwinch = False -# -# def on_resize(sig, action): -# global got_sigwinch -# got_sigwinch = True -# -# term = TestTerminal() -# signal.signal(signal.SIGWINCH, on_resize) -# read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) -# os.write(sys.__stdout__.fileno(), SEMAPHORE) -# with term.raw(): -# assert term.inkey(timeout=1.05) == u'' -# os.write(sys.__stdout__.fileno(), b'complete') -# assert got_sigwinch -# if cov is not None: -# cov.stop() -# cov.save() -# os._exit(0) -# -# with echo_off(master_fd): -# os.write(master_fd, SEND_SEMAPHORE) -# read_until_semaphore(master_fd) -# stime = time.time() -# os.kill(pid, signal.SIGWINCH) -# output = read_until_eof(master_fd) -# -# pid, status = os.waitpid(pid, 0) -# assert output == u'complete' -# assert os.WEXITSTATUS(status) == 0 -# assert math.floor(time.time() - stime) == 1.0 -# -# -#@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, -# reason="TEST_QUICK specified") -#def test_kbhit_interrupted_nonetype(): -# "kbhit() should also allow interruption with timeout of None." -# pid, master_fd = pty.fork() -# if pid == 0: -# cov = init_subproc_coverage('test_kbhit_interrupted_nonetype') -# -# # child pauses, writes semaphore and begins awaiting input -# global got_sigwinch -# got_sigwinch = False -# -# def on_resize(sig, action): -# global got_sigwinch -# got_sigwinch = True -# -# term = TestTerminal() -# signal.signal(signal.SIGWINCH, on_resize) -# read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) -# os.write(sys.__stdout__.fileno(), SEMAPHORE) -# with term.raw(): -# term.inkey(timeout=1) -# os.write(sys.__stdout__.fileno(), b'complete') -# assert got_sigwinch -# if cov is not None: -# cov.stop() -# cov.save() -# os._exit(0) -# -# with echo_off(master_fd): -# os.write(master_fd, SEND_SEMAPHORE) -# read_until_semaphore(master_fd) -# stime = time.time() -# time.sleep(0.05) -# os.kill(pid, signal.SIGWINCH) -# output = read_until_eof(master_fd) -# -# pid, status = os.waitpid(pid, 0) -# assert output == u'complete' -# assert os.WEXITSTATUS(status) == 0 -# assert math.floor(time.time() - stime) == 1.0 - - def test_break_input_no_kb(): "cbreak() should not call tty.setcbreak() without keyboard." @as_subprocess @@ -170,486 +85,6 @@ def child(): child() -#def test_kbhit_no_kb(): -# "kbhit() always immediately returns False without a keyboard." -# @as_subprocess -# def child(): -# term = TestTerminal(stream=six.StringIO()) -# stime = time.time() -# assert term._keyboard_fd is None -# assert not term.kbhit(timeout=1.1) -# assert math.floor(time.time() - stime) == 1.0 -# child() -# -# -#def test_keystroke_0s_cbreak_noinput(): -# "0-second keystroke without input; '' should be returned." -# @as_subprocess -# def child(): -# term = TestTerminal() -# with term.cbreak(): -# stime = time.time() -# inp = term.inkey(timeout=0) -# assert (inp == u'') -# assert (math.floor(time.time() - stime) == 0.0) -# child() -# -# -#def test_keystroke_0s_cbreak_noinput_nokb(): -# "0-second keystroke without data in input stream and no keyboard/tty." -# @as_subprocess -# def child(): -# term = TestTerminal(stream=six.StringIO()) -# with term.cbreak(): -# stime = time.time() -# inp = term.inkey(timeout=0) -# assert (inp == u'') -# assert (math.floor(time.time() - stime) == 0.0) -# child() -# -# -#@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, -# reason="TEST_QUICK specified") -#def test_keystroke_1s_cbreak_noinput(): -# "1-second keystroke without input; '' should be returned after ~1 second." -# @as_subprocess -# def child(): -# term = TestTerminal() -# with term.cbreak(): -# stime = time.time() -# inp = term.inkey(timeout=1) -# assert (inp == u'') -# assert (math.floor(time.time() - stime) == 1.0) -# child() -# -# -#@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, -# reason="TEST_QUICK specified") -#def test_keystroke_1s_cbreak_noinput_nokb(): -# "1-second keystroke without input or keyboard." -# @as_subprocess -# def child(): -# term = TestTerminal(stream=six.StringIO()) -# with term.cbreak(): -# stime = time.time() -# inp = term.inkey(timeout=1) -# assert (inp == u'') -# assert (math.floor(time.time() - stime) == 1.0) -# child() -# -# -#def test_keystroke_0s_cbreak_with_input(): -# "0-second keystroke with input; Keypress should be immediately returned." -# pid, master_fd = pty.fork() -# if pid == 0: -# cov = init_subproc_coverage('test_keystroke_0s_cbreak_with_input') -# # child pauses, writes semaphore and begins awaiting input -# term = TestTerminal() -# read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) -# os.write(sys.__stdout__.fileno(), SEMAPHORE) -# with term.cbreak(): -# inp = term.inkey(timeout=0) -# os.write(sys.__stdout__.fileno(), inp.encode('utf-8')) -# if cov is not None: -# cov.stop() -# cov.save() -# os._exit(0) -# -# with echo_off(master_fd): -# os.write(master_fd, SEND_SEMAPHORE) -# os.write(master_fd, u'x'.encode('ascii')) -# read_until_semaphore(master_fd) -# stime = time.time() -# output = read_until_eof(master_fd) -# -# pid, status = os.waitpid(pid, 0) -# assert output == u'x' -# assert os.WEXITSTATUS(status) == 0 -# assert math.floor(time.time() - stime) == 0.0 -# -# -#def test_keystroke_cbreak_with_input_slowly(): -# "0-second keystroke with input; Keypress should be immediately returned." -# pid, master_fd = pty.fork() -# if pid == 0: -# cov = init_subproc_coverage('test_keystroke_cbreak_with_input_slowly') -# # child pauses, writes semaphore and begins awaiting input -# term = TestTerminal() -# read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) -# os.write(sys.__stdout__.fileno(), SEMAPHORE) -# with term.cbreak(): -# while True: -# inp = term.inkey(timeout=0.5) -# os.write(sys.__stdout__.fileno(), inp.encode('utf-8')) -# if inp == 'X': -# break -# if cov is not None: -# cov.stop() -# cov.save() -# os._exit(0) -# -# with echo_off(master_fd): -# os.write(master_fd, SEND_SEMAPHORE) -# os.write(master_fd, u'a'.encode('ascii')) -# time.sleep(0.1) -# os.write(master_fd, u'b'.encode('ascii')) -# time.sleep(0.1) -# os.write(master_fd, u'cdefgh'.encode('ascii')) -# time.sleep(0.1) -# os.write(master_fd, u'X'.encode('ascii')) -# read_until_semaphore(master_fd) -# stime = time.time() -# output = read_until_eof(master_fd) -# -# pid, status = os.waitpid(pid, 0) -# assert output == u'abcdefghX' -# assert os.WEXITSTATUS(status) == 0 -# assert math.floor(time.time() - stime) == 0.0 -# -# -#def test_keystroke_0s_cbreak_multibyte_utf8(): -# "0-second keystroke with multibyte utf-8 input; should decode immediately." -# # utf-8 bytes represent "latin capital letter upsilon". -# pid, master_fd = pty.fork() -# if pid == 0: # child -# cov = init_subproc_coverage('test_keystroke_0s_cbreak_multibyte_utf8') -# term = TestTerminal() -# read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) -# os.write(sys.__stdout__.fileno(), SEMAPHORE) -# with term.cbreak(): -# inp = term.inkey(timeout=0) -# os.write(sys.__stdout__.fileno(), inp.encode('utf-8')) -# if cov is not None: -# cov.stop() -# cov.save() -# os._exit(0) -# -# with echo_off(master_fd): -# os.write(master_fd, SEND_SEMAPHORE) -# os.write(master_fd, u'\u01b1'.encode('utf-8')) -# read_until_semaphore(master_fd) -# stime = time.time() -# output = read_until_eof(master_fd) -# pid, status = os.waitpid(pid, 0) -# assert output == u'Ʊ' -# assert os.WEXITSTATUS(status) == 0 -# assert math.floor(time.time() - stime) == 0.0 -# -# -#@pytest.mark.skipif(os.environ.get('TRAVIS', None) is not None, -# reason="travis-ci does not handle ^C very well.") -#def test_keystroke_0s_raw_input_ctrl_c(): -# "0-second keystroke with raw allows receiving ^C." -# pid, master_fd = pty.fork() -# if pid == 0: # child -# cov = init_subproc_coverage('test_keystroke_0s_raw_input_ctrl_c') -# term = TestTerminal() -# read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) -# with term.raw(): -# os.write(sys.__stdout__.fileno(), RECV_SEMAPHORE) -# inp = term.inkey(timeout=0) -# os.write(sys.__stdout__.fileno(), inp.encode('latin1')) -# if cov is not None: -# cov.stop() -# cov.save() -# os._exit(0) -# -# with echo_off(master_fd): -# os.write(master_fd, SEND_SEMAPHORE) -# # ensure child is in raw mode before sending ^C, -# read_until_semaphore(master_fd) -# os.write(master_fd, u'\x03'.encode('latin1')) -# stime = time.time() -# output = read_until_eof(master_fd) -# pid, status = os.waitpid(pid, 0) -# assert (output == u'\x03' or -# output == u'' and not os.isatty(0)) -# assert os.WEXITSTATUS(status) == 0 -# assert math.floor(time.time() - stime) == 0.0 -# -# -#def test_keystroke_0s_cbreak_sequence(): -# "0-second keystroke with multibyte sequence; should decode immediately." -# pid, master_fd = pty.fork() -# if pid == 0: # child -# cov = init_subproc_coverage('test_keystroke_0s_cbreak_sequence') -# term = TestTerminal() -# os.write(sys.__stdout__.fileno(), SEMAPHORE) -# with term.cbreak(): -# inp = term.inkey(timeout=0) -# os.write(sys.__stdout__.fileno(), inp.name.encode('ascii')) -# sys.stdout.flush() -# if cov is not None: -# cov.stop() -# cov.save() -# os._exit(0) -# -# with echo_off(master_fd): -# os.write(master_fd, u'\x1b[D'.encode('ascii')) -# read_until_semaphore(master_fd) -# stime = time.time() -# output = read_until_eof(master_fd) -# pid, status = os.waitpid(pid, 0) -# assert output == u'KEY_LEFT' -# assert os.WEXITSTATUS(status) == 0 -# assert math.floor(time.time() - stime) == 0.0 -# -# -#@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, -# reason="TEST_QUICK specified") -#def test_keystroke_1s_cbreak_with_input(): -# "1-second keystroke w/multibyte sequence; should return after ~1 second." -# pid, master_fd = pty.fork() -# if pid == 0: # child -# cov = init_subproc_coverage('test_keystroke_1s_cbreak_with_input') -# term = TestTerminal() -# os.write(sys.__stdout__.fileno(), SEMAPHORE) -# with term.cbreak(): -# inp = term.inkey(timeout=3) -# os.write(sys.__stdout__.fileno(), inp.name.encode('utf-8')) -# sys.stdout.flush() -# if cov is not None: -# cov.stop() -# cov.save() -# os._exit(0) -# -# with echo_off(master_fd): -# read_until_semaphore(master_fd) -# stime = time.time() -# time.sleep(1) -# os.write(master_fd, u'\x1b[C'.encode('ascii')) -# output = read_until_eof(master_fd) -# -# pid, status = os.waitpid(pid, 0) -# assert output == u'KEY_RIGHT' -# assert os.WEXITSTATUS(status) == 0 -# assert math.floor(time.time() - stime) == 1.0 -# -# -#@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, -# reason="TEST_QUICK specified") -#def test_esc_delay_cbreak_035(): -# "esc_delay will cause a single ESC (\\x1b) to delay for 0.35." -# pid, master_fd = pty.fork() -# if pid == 0: # child -# cov = init_subproc_coverage('test_esc_delay_cbreak_035') -# term = TestTerminal() -# os.write(sys.__stdout__.fileno(), SEMAPHORE) -# with term.cbreak(): -# stime = time.time() -# inp = term.inkey(timeout=5) -# measured_time = (time.time() - stime) * 100 -# os.write(sys.__stdout__.fileno(), ( -# '%s %i' % (inp.name, measured_time,)).encode('ascii')) -# sys.stdout.flush() -# if cov is not None: -# cov.stop() -# cov.save() -# os._exit(0) -# -# with echo_off(master_fd): -# read_until_semaphore(master_fd) -# stime = time.time() -# os.write(master_fd, u'\x1b'.encode('ascii')) -# key_name, duration_ms = read_until_eof(master_fd).split() -# -# pid, status = os.waitpid(pid, 0) -# assert key_name == u'KEY_ESCAPE' -# assert os.WEXITSTATUS(status) == 0 -# assert math.floor(time.time() - stime) == 0.0 -# assert 34 <= int(duration_ms) <= 45, duration_ms -# -# -#@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, -# reason="TEST_QUICK specified") -#def test_esc_delay_cbreak_135(): -# "esc_delay=1.35 will cause a single ESC (\\x1b) to delay for 1.35." -# pid, master_fd = pty.fork() -# if pid == 0: # child -# cov = init_subproc_coverage('test_esc_delay_cbreak_135') -# term = TestTerminal() -# os.write(sys.__stdout__.fileno(), SEMAPHORE) -# with term.cbreak(): -# stime = time.time() -# inp = term.inkey(timeout=5, esc_delay=1.35) -# measured_time = (time.time() - stime) * 100 -# os.write(sys.__stdout__.fileno(), ( -# '%s %i' % (inp.name, measured_time,)).encode('ascii')) -# sys.stdout.flush() -# if cov is not None: -# cov.stop() -# cov.save() -# os._exit(0) -# -# with echo_off(master_fd): -# read_until_semaphore(master_fd) -# stime = time.time() -# os.write(master_fd, u'\x1b'.encode('ascii')) -# key_name, duration_ms = read_until_eof(master_fd).split() -# -# pid, status = os.waitpid(pid, 0) -# assert key_name == u'KEY_ESCAPE' -# assert os.WEXITSTATUS(status) == 0 -# assert math.floor(time.time() - stime) == 1.0 -# assert 134 <= int(duration_ms) <= 145, int(duration_ms) -# -# -#def test_esc_delay_cbreak_timout_0(): -# """esc_delay still in effect with timeout of 0 ("nonblocking").""" -# pid, master_fd = pty.fork() -# if pid == 0: # child -# cov = init_subproc_coverage('test_esc_delay_cbreak_timout_0') -# term = TestTerminal() -# os.write(sys.__stdout__.fileno(), SEMAPHORE) -# with term.cbreak(): -# stime = time.time() -# inp = term.inkey(timeout=0) -# measured_time = (time.time() - stime) * 100 -# os.write(sys.__stdout__.fileno(), ( -# '%s %i' % (inp.name, measured_time,)).encode('ascii')) -# sys.stdout.flush() -# if cov is not None: -# cov.stop() -# cov.save() -# os._exit(0) -# -# with echo_off(master_fd): -# os.write(master_fd, u'\x1b'.encode('ascii')) -# read_until_semaphore(master_fd) -# stime = time.time() -# key_name, duration_ms = read_until_eof(master_fd).split() -# -# pid, status = os.waitpid(pid, 0) -# assert key_name == u'KEY_ESCAPE' -# assert os.WEXITSTATUS(status) == 0 -# assert math.floor(time.time() - stime) == 0.0 -# assert 34 <= int(duration_ms) <= 45, int(duration_ms) -# -# -#def test_esc_delay_cbreak_nonprefix_sequence(): -# "ESC a (\\x1ba) will return an ESC immediately" -# pid, master_fd = pty.fork() -# if pid is 0: # child -# cov = init_subproc_coverage('test_esc_delay_cbreak_nonprefix_sequence') -# term = TestTerminal() -# os.write(sys.__stdout__.fileno(), SEMAPHORE) -# with term.cbreak(): -# stime = time.time() -# esc = term.inkey(timeout=5) -# inp = term.inkey(timeout=5) -# measured_time = (time.time() - stime) * 100 -# os.write(sys.__stdout__.fileno(), ( -# '%s %s %i' % (esc.name, inp, measured_time,)).encode('ascii')) -# sys.stdout.flush() -# if cov is not None: -# cov.stop() -# cov.save() -# os._exit(0) -# -# with echo_off(master_fd): -# read_until_semaphore(master_fd) -# stime = time.time() -# os.write(master_fd, u'\x1ba'.encode('ascii')) -# key1_name, key2, duration_ms = read_until_eof(master_fd).split() -# -# pid, status = os.waitpid(pid, 0) -# assert key1_name == u'KEY_ESCAPE' -# assert key2 == u'a' -# assert os.WEXITSTATUS(status) == 0 -# assert math.floor(time.time() - stime) == 0.0 -# assert -1 <= int(duration_ms) <= 15, duration_ms -# -# -#def test_esc_delay_cbreak_prefix_sequence(): -# "An unfinished multibyte sequence (\\x1b[) will delay an ESC by .35 " -# pid, master_fd = pty.fork() -# if pid is 0: # child -# cov = init_subproc_coverage('test_esc_delay_cbreak_prefix_sequence') -# term = TestTerminal() -# os.write(sys.__stdout__.fileno(), SEMAPHORE) -# with term.cbreak(): -# stime = time.time() -# esc = term.inkey(timeout=5) -# inp = term.inkey(timeout=5) -# measured_time = (time.time() - stime) * 100 -# os.write(sys.__stdout__.fileno(), ( -# '%s %s %i' % (esc.name, inp, measured_time,)).encode('ascii')) -# sys.stdout.flush() -# if cov is not None: -# cov.stop() -# cov.save() -# os._exit(0) -# -# with echo_off(master_fd): -# read_until_semaphore(master_fd) -# stime = time.time() -# os.write(master_fd, u'\x1b['.encode('ascii')) -# key1_name, key2, duration_ms = read_until_eof(master_fd).split() -# -# pid, status = os.waitpid(pid, 0) -# assert key1_name == u'KEY_ESCAPE' -# assert key2 == u'[' -# assert os.WEXITSTATUS(status) == 0 -# assert math.floor(time.time() - stime) == 0.0 -# assert 34 <= int(duration_ms) <= 45, duration_ms -# -# -#def test_get_location_0s(): -# "0-second get_location call without response." -# @as_subprocess -# def child(): -# term = TestTerminal(stream=six.StringIO()) -# stime = time.time() -# y, x = term.get_location(timeout=0) -# assert (math.floor(time.time() - stime) == 0.0) -# assert (y, x) == (-1, -1) -# child() -# -# -#def test_get_location_0s_under_raw(): -# "0-second get_location call without response under raw mode." -# @as_subprocess -# def child(): -# term = TestTerminal(stream=six.StringIO()) -# with term.raw(): -# stime = time.time() -# y, x = term.get_location(timeout=0) -# assert (math.floor(time.time() - stime) == 0.0) -# assert (y, x) == (-1, -1) -# child() -# -# -#def test_get_location_0s_reply_via_ungetch(): -# "0-second get_location call with response." -# @as_subprocess -# def child(): -# term = TestTerminal(stream=six.StringIO()) -# stime = time.time() -# # monkey patch in an invalid response ! -# term.ungetch(u'\x1b[10;10R') -# -# y, x = term.get_location(timeout=0.01) -# assert (math.floor(time.time() - stime) == 0.0) -# assert (y, x) == (10, 10) -# child() -# -# -#def test_get_location_0s_reply_via_ungetch_under_raw(): -# "0-second get_location call with response under raw mode." -# @as_subprocess -# def child(): -# term = TestTerminal(stream=six.StringIO()) -# with term.raw(): -# stime = time.time() -# # monkey patch in an invalid response ! -# term.ungetch(u'\x1b[10;10R') -# -# y, x = term.get_location(timeout=0.01) -# assert (math.floor(time.time() - stime) == 0.0) -# assert (y, x) == (10, 10) -# child() - - def test_keystroke_default_args(): "Test keyboard.Keystroke constructor with default arguments." from blessed.keyboard import Keystroke @@ -808,7 +243,7 @@ def test_resolve_sequence(): ks = resolve_sequence(u'', mapper, codes) assert ks == u'' assert ks.name is None - assert ks.code == None + assert ks.code is None assert not ks.is_sequence assert repr(ks) in ("u''", # py26, 27 "''",) # py33 diff --git a/blessed/tests/test_length_sequence.py b/blessed/tests/test_length_sequence.py index d3439cfc..8272583b 100644 --- a/blessed/tests/test_length_sequence.py +++ b/blessed/tests/test_length_sequence.py @@ -282,6 +282,7 @@ def child(kind, lines=25, cols=80): child(kind=all_terms) + def test_sequence_is_movement_false(all_terms): """Test parser about sequences that do not move the cursor.""" @as_subprocess @@ -319,6 +320,7 @@ def child(kind): child(all_terms) + def test_termcap_will_move_false(all_terms): """Test parser about sequences that do not move the cursor.""" @as_subprocess @@ -357,7 +359,6 @@ def child(kind): child(all_terms) - def test_sequence_is_movement_true(all_terms): """Test parsers about sequences that move the cursor.""" @as_subprocess @@ -370,26 +371,27 @@ def child(kind): assert (len(term.move(54)) == measure_length(term.move(54), term)) assert not term.cud1 or (len(term.cud1) == - measure_length(term.cud1, term)) + measure_length(term.cud1, term)) assert not term.cub1 or (len(term.cub1) == - measure_length(term.cub1, term)) + measure_length(term.cub1, term)) assert not term.cuf1 or (len(term.cuf1) == - measure_length(term.cuf1, term)) + measure_length(term.cuf1, term)) assert not term.cuu1 or (len(term.cuu1) == - measure_length(term.cuu1, term)) + measure_length(term.cuu1, term)) assert not term.cub or (len(term.cub(333)) == - measure_length(term.cub(333), term)) + measure_length(term.cub(333), term)) assert not term.cuf or (len(term.cuf(333)) == - measure_length(term.cuf(333), term)) + measure_length(term.cuf(333), term)) assert not term.home or (len(term.home) == - measure_length(term.home, term)) + measure_length(term.home, term)) assert not term.restore or (len(term.restore) == - measure_length(term.restore, term)) + measure_length(term.restore, term)) assert not term.clear or (len(term.clear) == - measure_length(term.clear, term)) + measure_length(term.clear, term)) child(all_terms) + def test_termcap_will_move_true(all_terms): """Test parser about sequences that move the cursor.""" @as_subprocess @@ -412,7 +414,6 @@ def child(kind): child(all_terms) - def test_foreign_sequences(): """Test parsers about sequences received from foreign sources.""" @as_subprocess diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index c19300ae..3fe96f29 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -391,7 +391,7 @@ def child(kind): ' off') expected = u''.join(( t.green, 'off ', t.underline, 'ON', - t.normal, t.green , ' off ', t.underline, 'ON', + t.normal, t.green, ' off ', t.underline, 'ON', t.normal, t.green, ' off', t.normal)) assert given == expected @@ -491,6 +491,7 @@ def child(): child() + def test_split_seqs(all_terms): """Test Terminal.split_seqs.""" @as_subprocess diff --git a/blessed/tests/test_wrap.py b/blessed/tests/test_wrap.py index 1c6f3df5..5f1aa435 100644 --- a/blessed/tests/test_wrap.py +++ b/blessed/tests/test_wrap.py @@ -102,7 +102,7 @@ def child(width, pgraph, kwargs): assert (len(internal_wrapped) == len(my_wrapped_colored)) child(width=many_columns, kwargs=kwargs, - pgraph=u' Z! a bc defghij klmnopqrstuvw<<>>xyz012345678900 '*2) + pgraph=u' Z! a bc defghij klmnopqrstuvw<<>>xyz012345678900 ' * 2) child(width=many_columns, kwargs=kwargs, pgraph=u'a bb ccc') From e29e39343f7ee2a4c42972699024969a7c39ee86 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Wed, 15 Jan 2020 17:13:09 -0800 Subject: [PATCH 440/459] tox -e docformatter result --- bin/editor.py | 6 ++--- bin/keymatrix.py | 4 +-- bin/on_resize.py | 7 +++--- bin/progress_bar.py | 9 +++---- bin/tprint.py | 1 - bin/worms.py | 8 +++--- blessed/color.py | 1 - blessed/formatters.py | 4 +-- blessed/sequences.py | 18 ++++++-------- blessed/terminal.py | 30 +++++++++++------------ blessed/tests/test_core.py | 42 ++++++++++++++++---------------- blessed/tests/test_formatters.py | 16 ++++++------ blessed/tests/test_keyboard.py | 30 +++++++++++------------ blessed/tests/test_sequences.py | 2 +- blessed/tests/test_wrap.py | 2 +- docs/conf.py | 4 +-- 16 files changed, 85 insertions(+), 99 deletions(-) diff --git a/bin/editor.py b/bin/editor.py index fb5f64b1..1fe873d6 100755 --- a/bin/editor.py +++ b/bin/editor.py @@ -49,10 +49,8 @@ def input_filter(keystroke): """ For given keystroke, return whether it should be allowed as input. - This somewhat requires that the interface use special - application keys to perform functions, as alphanumeric - input intended for persisting could otherwise be interpreted as a - command sequence. + This somewhat requires that the interface use special application keys to perform functions, as + alphanumeric input intended for persisting could otherwise be interpreted as a command sequence. """ if keystroke.is_sequence: # Namely, deny multi-byte sequences (such as '\x1b[A'), diff --git a/bin/keymatrix.py b/bin/keymatrix.py index 5104dcfe..4b3d2629 100755 --- a/bin/keymatrix.py +++ b/bin/keymatrix.py @@ -2,8 +2,8 @@ """ A simple "game": hit all application keys to win. -Display all known key capabilities that may match the terminal. -As each key is pressed on input, it is lit up and points are scored. +Display all known key capabilities that may match the terminal. As each key is pressed on input, it +is lit up and points are scored. """ from __future__ import division, print_function diff --git a/bin/on_resize.py b/bin/on_resize.py index 110f6df0..8749139c 100755 --- a/bin/on_resize.py +++ b/bin/on_resize.py @@ -2,10 +2,9 @@ """ Example application for the 'blessed' Terminal library for python. -Window size changes are caught by the 'on_resize' function using a traditional -signal handler. Meanwhile, blocking keyboard input is displayed to stdout. -If a resize event is discovered, an empty string is returned by -term.inkey(). +Window size changes are caught by the 'on_resize' function using a traditional signal handler. +Meanwhile, blocking keyboard input is displayed to stdout. If a resize event is discovered, an empty +string is returned by term.inkey(). """ from __future__ import print_function diff --git a/bin/progress_bar.py b/bin/progress_bar.py index 94912475..1139ac10 100755 --- a/bin/progress_bar.py +++ b/bin/progress_bar.py @@ -2,11 +2,10 @@ """ Example application for the 'blessed' Terminal library for python. -This isn't a real progress bar, just a sample "animated prompt" of sorts -that demonstrates the separate move_x() and move_y() functions, made -mainly to test the `hpa' compatibility for 'screen' terminal type which -fails to provide one, but blessed recognizes that it actually does, and -provides a proxy. +This isn't a real progress bar, just a sample "animated prompt" of sorts that demonstrates the +separate move_x() and move_y() functions, made mainly to test the `hpa' compatibility for 'screen' +terminal type which fails to provide one, but blessed recognizes that it actually does, and provides +a proxy. """ from __future__ import print_function diff --git a/bin/tprint.py b/bin/tprint.py index 5e0935e9..3bffde6d 100755 --- a/bin/tprint.py +++ b/bin/tprint.py @@ -5,7 +5,6 @@ For example:: $ python tprint.py bold A rather bold statement. - """ # std from __future__ import print_function diff --git a/bin/worms.py b/bin/worms.py index 636992be..853059e0 100755 --- a/bin/worms.py +++ b/bin/worms.py @@ -84,8 +84,7 @@ def next_bearing(term, inp_code, bearing): """ Return direction function for new bearing by inp_code. - If no inp_code matches a bearing direction, return - a function for the current bearing. + If no inp_code matches a bearing direction, return a function for the current bearing. """ return { term.KEY_LEFT: left_of, @@ -158,9 +157,8 @@ def next_nibble(term, nibble, head, worm): """ Provide the next nibble. - continuously generate a random new nibble so long as the current nibble - hits any location of the worm. Otherwise, return a nibble of the same - location and value as provided. + continuously generate a random new nibble so long as the current nibble hits any location of the + worm. Otherwise, return a nibble of the same location and value as provided. """ loc, val = nibble.location, nibble.value while hit_vany([head] + worm, nibble_locations(loc, val)): diff --git a/blessed/color.py b/blessed/color.py index bb595c51..9a42be4f 100644 --- a/blessed/color.py +++ b/blessed/color.py @@ -6,7 +6,6 @@ - https://en.wikipedia.org/wiki/Color_difference - http://www.easyrgb.com/en/math.php - Measuring Colour by R.W.G. Hunt and M.R. Pointer - """ # std imports diff --git a/blessed/formatters.py b/blessed/formatters.py index 309abea9..83bee881 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -266,8 +266,8 @@ class NullCallableString(six.text_type): """ A dummy callable Unicode alternative to :class:`FormattingString`. - This is used for colors on terminals that do not support colors, - it is just a basic form of unicode that may also act as a callable. + This is used for colors on terminals that do not support colors, it is just a basic form of + unicode that may also act as a callable. """ def __new__(cls): diff --git a/blessed/sequences.py b/blessed/sequences.py index 191d7f8b..dd15040c 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -152,11 +152,10 @@ def _wrap_chunks(self, chunks): """ Sequence-aware variant of :meth:`textwrap.TextWrapper._wrap_chunks`. - This simply ensures that word boundaries are not broken mid-sequence, - as standard python textwrap would incorrectly determine the length - of a string containing sequences, and may also break consider sequences - part of a "word" that may be broken by hyphen (``-``), where this - implementation corrects both. + This simply ensures that word boundaries are not broken mid-sequence, as standard python + textwrap would incorrectly determine the length of a string containing sequences, and may + also break consider sequences part of a "word" that may be broken by hyphen (``-``), where + this implementation corrects both. """ lines = [] if self.width <= 0 or not isinstance(self.width, int): @@ -199,11 +198,10 @@ def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width): """ Sequence-aware :meth:`textwrap.TextWrapper._handle_long_word`. - This simply ensures that word boundaries are not broken mid-sequence, - as standard python textwrap would incorrectly determine the length - of a string containing sequences, and may also break consider sequences - part of a "word" that may be broken by hyphen (``-``), where this - implementation corrects both. + This simply ensures that word boundaries are not broken mid-sequence, as standard python + textwrap would incorrectly determine the length of a string containing sequences, and may + also break consider sequences part of a "word" that may be broken by hyphen (``-``), where + this implementation corrects both. """ # Figure out when indent is larger than the specified width, and make # sure at least one character is stripped off on every pass diff --git a/blessed/terminal.py b/blessed/terminal.py index 6fb6cca4..ad0d618e 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -79,10 +79,10 @@ class Terminal(object): """ An abstraction for color, style, positioning, and input in the terminal. - This keeps the endless calls to ``tigetstr()`` and ``tparm()`` out of your - code, acts intelligently when somebody pipes your output to a non-terminal, - and abstracts over the complexity of unbuffered keyboard input. It uses the - terminfo database to remain portable across terminal types. + This keeps the endless calls to ``tigetstr()`` and ``tparm()`` out of your code, acts + intelligently when somebody pipes your output to a non-terminal, and abstracts over the + complexity of unbuffered keyboard input. It uses the terminfo database to remain portable across + terminal types. """ # pylint: disable=too-many-instance-attributes,too-many-public-methods # Too many public methods (28/20) @@ -443,7 +443,6 @@ def _height_and_width(self): - ``ws_col``: height of terminal by its number of character cells. - ``ws_xpixel``: width of terminal by pixels (not accurate). - ``ws_ypixel``: height of terminal by pixels (not accurate). - """ for fd in (self._init_descriptor, sys.__stdout__): try: @@ -703,7 +702,8 @@ def rgb_downconvert(self, red, green, blue): :arg int red: RGB value of Red (0-255). :arg int green: RGB value of Green (0-255). :arg int blue: RGB value of Blue (0-255). - :rtype: int """ + :rtype: int + """ # Though pre-computing all 1 << 24 options is memory-intensive, a pre-computed # "k-d tree" of 256 (x,y,z) vectors of a colorspace in 3 dimensions, such as a # cone of HSV, or simply 255x255x255 RGB square, any given rgb value is just a @@ -772,8 +772,8 @@ def color_distance_algorithm(self): """ Color distance algorithm used by :meth:`rgb_downconvert`. - The slowest, but most accurate, 'cie2000', is default. Other - available options are 'rgb', 'rgb-weighted', 'cie76', and 'cie94'. + The slowest, but most accurate, 'cie2000', is default. Other available options are 'rgb', + 'rgb-weighted', 'cie76', and 'cie94'. """ return self._color_distance_algorithm @@ -788,10 +788,9 @@ def _foreground_color(self): """ Convenience capability to support :attr:`~.on_color`. - Prefers returning sequence for capability ``setaf``, "Set foreground - color to #1, using ANSI escape". If the given terminal does not - support such sequence, fallback to returning attribute ``setf``, - "Set foreground color #1". + Prefers returning sequence for capability ``setaf``, "Set foreground color to #1, using ANSI + escape". If the given terminal does not support such sequence, fallback to returning + attribute ``setf``, "Set foreground color #1". """ return self.setaf or self.setf @@ -800,10 +799,9 @@ def _background_color(self): """ Convenience capability to support :attr:`~.on_color`. - Prefers returning sequence for capability ``setab``, "Set background - color to #1, using ANSI escape". If the given terminal does not - support such sequence, fallback to returning attribute ``setb``, - "Set background color #1". + Prefers returning sequence for capability ``setab``, "Set background color to #1, using ANSI + escape". If the given terminal does not support such sequence, fallback to returning + attribute ``setb``, "Set background color #1". """ return self.setab or self.setb diff --git a/blessed/tests/test_core.py b/blessed/tests/test_core.py index 1c7b9c24..b222b8ff 100644 --- a/blessed/tests/test_core.py +++ b/blessed/tests/test_core.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"Core blessed Terminal() tests." +"""Core blessed Terminal() tests.""" # std imports import io @@ -29,7 +29,7 @@ def test_export_only_Terminal(): def test_null_location(all_terms): - "Make sure ``location()`` with no args just does position restoration." + """Make sure ``location()`` with no args just does position restoration.""" @as_subprocess def child(kind): t = TestTerminal(stream=six.StringIO(), force_styling=True) @@ -43,7 +43,7 @@ def child(kind): def test_flipped_location_move(all_terms): - "``location()`` and ``move()`` receive counter-example arguments." + """``location()`` and ``move()`` receive counter-example arguments.""" @as_subprocess def child(kind): buf = six.StringIO() @@ -58,7 +58,7 @@ def child(kind): def test_yield_keypad(): - "Ensure ``keypad()`` writes keyboard_xmit and keyboard_local." + """Ensure ``keypad()`` writes keyboard_xmit and keyboard_local.""" @as_subprocess def child(kind): # given, @@ -76,7 +76,7 @@ def child(kind): def test_null_fileno(): - "Make sure ``Terminal`` works when ``fileno`` is ``None``." + """Make sure ``Terminal`` works when ``fileno`` is ``None``.""" @as_subprocess def child(): # This simulates piping output to another program. @@ -89,7 +89,7 @@ def child(): def test_number_of_colors_without_tty(): - "``number_of_colors`` should return 0 when there's no tty." + """``number_of_colors`` should return 0 when there's no tty.""" @as_subprocess def child_256_nostyle(): t = TestTerminal(stream=six.StringIO()) @@ -124,7 +124,7 @@ def child_0_forcestyle(): def test_number_of_colors_with_tty(): - "test ``number_of_colors`` 0, 8, and 256." + """test ``number_of_colors`` 0, 8, and 256.""" @as_subprocess def child_256(): t = TestTerminal() @@ -151,7 +151,7 @@ def child_0(): def test_init_descriptor_always_initted(all_terms): - "Test height and width with non-tty Terminals." + """Test height and width with non-tty Terminals.""" @as_subprocess def child(kind): t = TestTerminal(kind=kind, stream=six.StringIO()) @@ -165,7 +165,7 @@ def child(kind): def test_force_styling_none(all_terms): - "If ``force_styling=None`` is used, don't ever do styling." + """If ``force_styling=None`` is used, don't ever do styling.""" @as_subprocess def child(kind): t = TestTerminal(kind=kind, force_styling=None) @@ -177,7 +177,7 @@ def child(kind): def test_setupterm_singleton_issue33(): - "A warning is emitted if a new terminal ``kind`` is used per process." + """A warning is emitted if a new terminal ``kind`` is used per process.""" @as_subprocess def child(): warnings.filterwarnings("error", category=UserWarning) @@ -204,7 +204,7 @@ def child(): def test_setupterm_invalid_issue39(): - "A warning is emitted if TERM is invalid." + """A warning is emitted if TERM is invalid.""" # https://bugzilla.mozilla.org/show_bug.cgi?id=878089 # # if TERM is unset, defaults to 'unknown', which should @@ -231,7 +231,7 @@ def child(): def test_setupterm_invalid_has_no_styling(): - "An unknown TERM type does not perform styling." + """An unknown TERM type does not perform styling.""" # https://bugzilla.mozilla.org/show_bug.cgi?id=878089 # if TERM is unset, defaults to 'unknown', which should @@ -250,7 +250,7 @@ def child(): def test_missing_ordereddict_uses_module(monkeypatch): - "ordereddict module is imported when without collections.OrderedDict." + """ordereddict module is imported when without collections.OrderedDict.""" import blessed.keyboard if hasattr(collections, 'OrderedDict'): @@ -273,7 +273,7 @@ def test_missing_ordereddict_uses_module(monkeypatch): def test_python3_2_raises_exception(monkeypatch): - "Test python version 3.0 through 3.2 raises an exception." + """Test python version 3.0 through 3.2 raises an exception.""" import blessed monkeypatch.setattr('platform.python_version_tuple', @@ -292,13 +292,13 @@ def test_python3_2_raises_exception(monkeypatch): def test_without_dunder(): - "Ensure dunder does not remain in module (py2x InterruptedError test." + """Ensure dunder does not remain in module (py2x InterruptedError test.""" import blessed.terminal assert '_' not in dir(blessed.terminal) def test_IOUnsupportedOperation(): - "Ensure stream that throws IOUnsupportedOperation results in non-tty." + """Ensure stream that throws IOUnsupportedOperation results in non-tty.""" @as_subprocess def child(): import blessed.terminal @@ -335,7 +335,7 @@ def side_effect(fd): def test_yield_fullscreen(all_terms): - "Ensure ``fullscreen()`` writes enter_fullscreen and exit_fullscreen." + """Ensure ``fullscreen()`` writes enter_fullscreen and exit_fullscreen.""" @as_subprocess def child(kind): t = TestTerminal(stream=six.StringIO(), force_styling=True) @@ -350,7 +350,7 @@ def child(kind): def test_yield_hidden_cursor(all_terms): - "Ensure ``hidden_cursor()`` writes hide_cursor and normal_cursor." + """Ensure ``hidden_cursor()`` writes hide_cursor and normal_cursor.""" @as_subprocess def child(kind): t = TestTerminal(stream=six.StringIO(), force_styling=True) @@ -365,7 +365,7 @@ def child(kind): def test_no_preferredencoding_fallback_ascii(): - "Ensure empty preferredencoding value defaults to ascii." + """Ensure empty preferredencoding value defaults to ascii.""" @as_subprocess def child(): with mock.patch('locale.getpreferredencoding') as get_enc: @@ -393,7 +393,7 @@ def child(): def test_win32_missing_tty_modules(monkeypatch): - "Ensure dummy exception is used when io is without UnsupportedOperation." + """Ensure dummy exception is used when io is without UnsupportedOperation.""" @as_subprocess def child(): OLD_STYLE = False @@ -470,7 +470,7 @@ def test_time_left_infinite_None(): def test_termcap_repr(): - "Ensure ``hidden_cursor()`` writes hide_cursor and normal_cursor." + """Ensure ``hidden_cursor()`` writes hide_cursor and normal_cursor.""" given_ttype = 'vt220' given_capname = 'cursor_up' diff --git a/blessed/tests/test_formatters.py b/blessed/tests/test_formatters.py index 3ee0cf21..ae1eb217 100644 --- a/blessed/tests/test_formatters.py +++ b/blessed/tests/test_formatters.py @@ -157,7 +157,7 @@ def test_nested_formattingstring_type_error(monkeypatch): def test_nullcallablestring(monkeypatch): - """Test formatters.NullCallableString""" + """Test formatters.NullCallableString.""" from blessed.formatters import (NullCallableString) # given, with arg @@ -184,7 +184,7 @@ def test_split_compound(): def test_resolve_capability(monkeypatch): - """Test formatters.resolve_capability and term sugaring """ + """Test formatters.resolve_capability and term sugaring.""" from blessed.formatters import resolve_capability # given, always returns a b'seq' @@ -259,7 +259,7 @@ def color_cap(digit): return 'seq-%s' % (digit,) def test_resolve_attribute_as_color(monkeypatch): - """ Test simple resolve_attribte() given color name. """ + """Test simple resolve_attribte() given color name.""" import blessed from blessed.formatters import resolve_attribute @@ -274,7 +274,7 @@ def resolve_color(term, digit): return 'seq-%s' % (digit,) def test_resolve_attribute_as_compoundable(monkeypatch): - """ Test simple resolve_attribte() given a compoundable. """ + """Test simple resolve_attribte() given a compoundable.""" import blessed from blessed.formatters import resolve_attribute, FormattingString @@ -294,7 +294,7 @@ def resolve_cap(term, digit): return 'seq-%s' % (digit,) def test_resolve_attribute_non_compoundables(monkeypatch): - """ Test recursive compounding of resolve_attribute(). """ + """Test recursive compounding of resolve_attribute().""" import blessed from blessed.formatters import resolve_attribute, ParameterizingString def uncompoundables(attr): return ['split', 'compound'] @@ -324,7 +324,7 @@ def resolve_cap(term, digit): return 'seq-%s' % (digit,) def test_resolve_attribute_recursive_compoundables(monkeypatch): - """ Test recursive compounding of resolve_attribute(). """ + """Test recursive compounding of resolve_attribute().""" import blessed from blessed.formatters import resolve_attribute, FormattingString @@ -394,7 +394,7 @@ def test_pickled_parameterizing_string(monkeypatch): def test_tparm_returns_null(monkeypatch): - """ Test 'tparm() returned NULL' is caught (win32 PDCurses systems). """ + """Test 'tparm() returned NULL' is caught (win32 PDCurses systems).""" # on win32, any calls to tparm raises curses.error with message, # "tparm() returned NULL", function PyCurses_tparm of _cursesmodule.c from blessed.formatters import ParameterizingString, NullCallableString @@ -414,7 +414,7 @@ def tparm(*args): def test_tparm_other_exception(monkeypatch): - """ Test 'tparm() returned NULL' is caught (win32 PDCurses systems). """ + """Test 'tparm() returned NULL' is caught (win32 PDCurses systems).""" # on win32, any calls to tparm raises curses.error with message, # "tparm() returned NULL", function PyCurses_tparm of _cursesmodule.c from blessed.formatters import ParameterizingString, NullCallableString diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index fe6fc899..04ff7196 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"Tests for keyboard support." +"""Tests for keyboard support.""" # std imports import os import pty @@ -34,7 +34,7 @@ def test_break_input_no_kb(): - "cbreak() should not call tty.setcbreak() without keyboard." + """cbreak() should not call tty.setcbreak() without keyboard.""" @as_subprocess def child(): with tempfile.NamedTemporaryFile() as stream: @@ -47,7 +47,7 @@ def child(): def test_raw_input_no_kb(): - "raw should not call tty.setraw() without keyboard." + """raw should not call tty.setraw() without keyboard.""" @as_subprocess def child(): with tempfile.NamedTemporaryFile() as stream: @@ -60,7 +60,7 @@ def child(): def test_raw_input_with_kb(): - "raw should call tty.setraw() when with keyboard." + """raw should call tty.setraw() when with keyboard.""" @as_subprocess def child(): term = TestTerminal() @@ -72,7 +72,7 @@ def child(): def test_notty_kb_is_None(): - "term._keyboard_fd should be None when os.isatty returns False." + """term._keyboard_fd should be None when os.isatty returns False.""" # in this scenerio, stream is sys.__stdout__, # but os.isatty(0) is False, # such as when piping output to less(1) @@ -86,7 +86,7 @@ def child(): def test_keystroke_default_args(): - "Test keyboard.Keystroke constructor with default arguments." + """Test keyboard.Keystroke constructor with default arguments.""" from blessed.keyboard import Keystroke ks = Keystroke() assert ks._name is None @@ -100,7 +100,7 @@ def test_keystroke_default_args(): def test_a_keystroke(): - "Test keyboard.Keystroke constructor with set arguments." + """Test keyboard.Keystroke constructor with set arguments.""" from blessed.keyboard import Keystroke ks = Keystroke(ucs=u'x', code=1, name=u'the X') assert ks._name == u'the X' @@ -113,7 +113,7 @@ def test_a_keystroke(): def test_get_keyboard_codes(): - "Test all values returned by get_keyboard_codes are from curses." + """Test all values returned by get_keyboard_codes are from curses.""" import blessed.keyboard exemptions = dict(blessed.keyboard.CURSES_KEYCODE_OVERRIDE_MIXIN) for value, keycode in blessed.keyboard.get_keyboard_codes().items(): @@ -130,7 +130,7 @@ def test_get_keyboard_codes(): def test_alternative_left_right(): - "Test _alternative_left_right behavior for space/backspace." + """Test _alternative_left_right behavior for space/backspace.""" from blessed.keyboard import _alternative_left_right term = mock.Mock() term._cuf1 = u'' @@ -147,7 +147,7 @@ def test_alternative_left_right(): def test_cuf1_and_cub1_as_RIGHT_LEFT(all_terms): - "Test that cuf1 and cub1 are assigned KEY_RIGHT and KEY_LEFT." + """Test that cuf1 and cub1 are assigned KEY_RIGHT and KEY_LEFT.""" from blessed.keyboard import get_keyboard_sequences @as_subprocess @@ -168,7 +168,7 @@ def child(kind): def test_get_keyboard_sequences_sort_order(): - "ordereddict ensures sequences are ordered longest-first." + """ordereddict ensures sequences are ordered longest-first.""" @as_subprocess def child(kind): term = TestTerminal(kind=kind, force_styling=True) @@ -182,7 +182,7 @@ def child(kind): def test_get_keyboard_sequence(monkeypatch): - "Test keyboard.get_keyboard_sequence. " + """Test keyboard.get_keyboard_sequence.""" import blessed.keyboard (KEY_SMALL, KEY_LARGE, KEY_MIXIN) = range(3) @@ -223,7 +223,7 @@ def test_get_keyboard_sequence(monkeypatch): def test_resolve_sequence(): - "Test resolve_sequence for order-dependent mapping." + """Test resolve_sequence for order-dependent mapping.""" from blessed.keyboard import resolve_sequence, OrderedDict mapper = OrderedDict(((u'SEQ1', 1), (u'SEQ2', 2), @@ -285,7 +285,7 @@ def test_resolve_sequence(): def test_keyboard_prefixes(): - "Test keyboard.prefixes" + """Test keyboard.prefixes.""" from blessed.keyboard import get_leading_prefixes keys = ['abc', 'abdf', 'e', 'jkl'] pfs = get_leading_prefixes(keys) @@ -293,7 +293,7 @@ def test_keyboard_prefixes(): def test_keypad_mixins_and_aliases(): - """ Test PC-Style function key translations when in ``keypad`` mode.""" + """Test PC-Style function key translations when in ``keypad`` mode.""" # Key plain app modified # Up ^[[A ^[OA ^[[1;mA # Down ^[[B ^[OB ^[[1;mB diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index 3fe96f29..726e012b 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -478,7 +478,7 @@ def child(kind): def test_padd(): - """ Test Terminal.padd(seq). """ + """Test Terminal.padd(seq).""" @as_subprocess def child(): from blessed.sequences import Sequence diff --git a/blessed/tests/test_wrap.py b/blessed/tests/test_wrap.py index 5f1aa435..29537596 100644 --- a/blessed/tests/test_wrap.py +++ b/blessed/tests/test_wrap.py @@ -44,7 +44,7 @@ def test_SequenceWrapper_invalid_width(): - """Test exception thrown from invalid width""" + """Test exception thrown from invalid width.""" WIDTH = -3 @as_subprocess diff --git a/docs/conf.py b/docs/conf.py index b4448885..69e640f8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -39,9 +39,7 @@ def _warn_node(self, msg, node): # Monkey-patch functools.wraps and contextlib.wraps # https://github.com/sphinx-doc/sphinx/issues/1711#issuecomment-93126473 def no_op_wraps(func): - """ - Replaces functools.wraps in order to undo wrapping when generating Sphinx documentation - """ + """Replaces functools.wraps in order to undo wrapping when generating Sphinx documentation.""" if func.__module__ is None or 'blessed' not in func.__module__: return functools.orig_wraps(func) def wrapper(decorator): From 91f2485d9045fa7953745aeb63d5001074e7f9b8 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Wed, 15 Jan 2020 18:30:09 -0800 Subject: [PATCH 441/459] tox -eflake8, flake8_tests --- .travis.yml | 2 +- bin/plasma.py | 1 - blessed/keyboard.py | 38 +++++++++--------- blessed/tests/test_core.py | 2 - blessed/tests/test_formatters.py | 69 ++++++++++++++++---------------- blessed/tests/test_keyboard.py | 18 +-------- blessed/tests/test_sequences.py | 9 +---- docs/conf.py | 24 +++++------ tox.ini | 12 ++++-- 9 files changed, 78 insertions(+), 97 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3fe7e4ba..610f1b35 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ matrix: - python: 3.9-dev env: TOXENV=py39,coveralls TEST_QUICK=1 COVERAGE_ID=travis-ci - python: 3.8 - env: TOXENV=about,pylint,flake8,sphinx COVERAGE_ID=travis-ci + env: TOXENV=about,pylint,flake8,flake8_tests,sphinx COVERAGE_ID=travis-ci jobs: allow_failures: diff --git a/bin/plasma.py b/bin/plasma.py index 4a8a7384..acf1b668 100755 --- a/bin/plasma.py +++ b/bin/plasma.py @@ -6,7 +6,6 @@ import timeit import colorsys import contextlib -import collections # local import blessed diff --git a/blessed/keyboard.py b/blessed/keyboard.py index cf90cc4c..777c041e 100644 --- a/blessed/keyboard.py +++ b/blessed/keyboard.py @@ -375,7 +375,7 @@ def _read_until(term, pattern, timeout): (six.unichr(10), curses.KEY_ENTER), (six.unichr(13), curses.KEY_ENTER), (six.unichr(8), curses.KEY_BACKSPACE), - (six.unichr(9), KEY_TAB), + (six.unichr(9), KEY_TAB), # noqa (six.unichr(27), curses.KEY_EXIT), (six.unichr(127), curses.KEY_BACKSPACE), @@ -395,24 +395,24 @@ def _read_until(term, pattern, timeout): # http://fossies.org/linux/rxvt/doc/rxvtRef.html#KeyCodes # # keypad, numlock on - (u"\x1bOM", curses.KEY_ENTER), # return - (u"\x1bOj", KEY_KP_MULTIPLY), # * - (u"\x1bOk", KEY_KP_ADD), # + - (u"\x1bOl", KEY_KP_SEPARATOR), # , - (u"\x1bOm", KEY_KP_SUBTRACT), # - - (u"\x1bOn", KEY_KP_DECIMAL), # . - (u"\x1bOo", KEY_KP_DIVIDE), # / - (u"\x1bOX", KEY_KP_EQUAL), # = - (u"\x1bOp", KEY_KP_0), # 0 - (u"\x1bOq", KEY_KP_1), # 1 - (u"\x1bOr", KEY_KP_2), # 2 - (u"\x1bOs", KEY_KP_3), # 3 - (u"\x1bOt", KEY_KP_4), # 4 - (u"\x1bOu", KEY_KP_5), # 5 - (u"\x1bOv", KEY_KP_6), # 6 - (u"\x1bOw", KEY_KP_7), # 7 - (u"\x1bOx", KEY_KP_8), # 8 - (u"\x1bOy", KEY_KP_9), # 9 + (u"\x1bOM", curses.KEY_ENTER), # noqa return + (u"\x1bOj", KEY_KP_MULTIPLY), # noqa * + (u"\x1bOk", KEY_KP_ADD), # noqa + + (u"\x1bOl", KEY_KP_SEPARATOR), # noqa , + (u"\x1bOm", KEY_KP_SUBTRACT), # noqa - + (u"\x1bOn", KEY_KP_DECIMAL), # noqa . + (u"\x1bOo", KEY_KP_DIVIDE), # noqa / + (u"\x1bOX", KEY_KP_EQUAL), # noqa = + (u"\x1bOp", KEY_KP_0), # noqa 0 + (u"\x1bOq", KEY_KP_1), # noqa 1 + (u"\x1bOr", KEY_KP_2), # noqa 2 + (u"\x1bOs", KEY_KP_3), # noqa 3 + (u"\x1bOt", KEY_KP_4), # noqa 4 + (u"\x1bOu", KEY_KP_5), # noqa 5 + (u"\x1bOv", KEY_KP_6), # noqa 6 + (u"\x1bOw", KEY_KP_7), # noqa 7 + (u"\x1bOx", KEY_KP_8), # noqa 8 + (u"\x1bOy", KEY_KP_9), # noqa 9 # keypad, numlock off (u"\x1b[1~", curses.KEY_FIND), # find diff --git a/blessed/tests/test_core.py b/blessed/tests/test_core.py index b222b8ff..fa9b7d20 100644 --- a/blessed/tests/test_core.py +++ b/blessed/tests/test_core.py @@ -7,7 +7,6 @@ import sys import math import time -import locale import platform import warnings import collections @@ -15,7 +14,6 @@ # 3rd party import six import mock -import pytest from six.moves import reload_module # local diff --git a/blessed/tests/test_formatters.py b/blessed/tests/test_formatters.py index ae1eb217..eccc4776 100644 --- a/blessed/tests/test_formatters.py +++ b/blessed/tests/test_formatters.py @@ -8,16 +8,19 @@ import pytest +def fn_tparm(*args): + return u'~'.join( + arg.decode('latin1') if not num else '%s' % (arg,) + for num, arg in enumerate(args) + ).encode('latin1') + + def test_parameterizing_string_args_unspecified(monkeypatch): """Test default args of formatters.ParameterizingString.""" from blessed.formatters import ParameterizingString, FormattingString # first argument to tparm() is the sequence name, returned as-is; # subsequent arguments are usually Integers. - tparm = lambda *args: u'~'.join( - arg.decode('latin1') if not num else '%s' % (arg,) - for num, arg in enumerate(args)).encode('latin1') - - monkeypatch.setattr(curses, 'tparm', tparm) + monkeypatch.setattr(curses, 'tparm', fn_tparm) # given, pstr = ParameterizingString(u'') @@ -46,11 +49,7 @@ def test_parameterizing_string_args(monkeypatch): # first argument to tparm() is the sequence name, returned as-is; # subsequent arguments are usually Integers. - tparm = lambda *args: u'~'.join( - arg.decode('latin1') if not num else '%s' % (arg,) - for num, arg in enumerate(args)).encode('latin1') - - monkeypatch.setattr(curses, 'tparm', tparm) + monkeypatch.setattr(curses, 'tparm', fn_tparm) # given, pstr = ParameterizingString(u'cap', u'norm', u'seq-name') @@ -188,7 +187,8 @@ def test_resolve_capability(monkeypatch): from blessed.formatters import resolve_capability # given, always returns a b'seq' - def tigetstr(attr): return ('seq-%s' % (attr,)).encode('latin1') + def tigetstr(attr): + return ('seq-%s' % (attr,)).encode('latin1') monkeypatch.setattr(curses, 'tigetstr', tigetstr) term = mock.Mock() term._sugar = dict(mnemonic='xyz') @@ -198,7 +198,8 @@ def tigetstr(attr): return ('seq-%s' % (attr,)).encode('latin1') assert resolve_capability(term, 'natural') == u'seq-natural' # given, where tigetstr returns None - def tigetstr_none(attr): return None + def tigetstr_none(attr): + return None monkeypatch.setattr(curses, 'tigetstr', tigetstr_none) # exercise, @@ -220,7 +221,8 @@ def test_resolve_color(monkeypatch): FormattingString, NullCallableString) - def color_cap(digit): return 'seq-%s' % (digit,) + def color_cap(digit): + return 'seq-%s' % (digit,) monkeypatch.setattr(curses, 'COLOR_RED', 1984) # given, terminal with color capabilities @@ -263,7 +265,8 @@ def test_resolve_attribute_as_color(monkeypatch): import blessed from blessed.formatters import resolve_attribute - def resolve_color(term, digit): return 'seq-%s' % (digit,) + def resolve_color(term, digit): + return 'seq-%s' % (digit,) COLORS = set(['COLORX', 'COLORY']) COMPOUNDABLES = set(['JOINT', 'COMPOUND']) monkeypatch.setattr(blessed.formatters, 'resolve_color', resolve_color) @@ -278,7 +281,8 @@ def test_resolve_attribute_as_compoundable(monkeypatch): import blessed from blessed.formatters import resolve_attribute, FormattingString - def resolve_cap(term, digit): return 'seq-%s' % (digit,) + def resolve_cap(term, digit): + return 'seq-%s' % (digit,) COMPOUNDABLES = set(['JOINT', 'COMPOUND']) monkeypatch.setattr(blessed.formatters, 'resolve_capability', @@ -297,18 +301,20 @@ def test_resolve_attribute_non_compoundables(monkeypatch): """Test recursive compounding of resolve_attribute().""" import blessed from blessed.formatters import resolve_attribute, ParameterizingString - def uncompoundables(attr): return ['split', 'compound'] - def resolve_cap(term, digit): return 'seq-%s' % (digit,) + + def uncompoundables(attr): + return ['split', 'compound'] + + def resolve_cap(term, digit): + return 'seq-%s' % (digit,) + monkeypatch.setattr(blessed.formatters, 'split_compound', uncompoundables) monkeypatch.setattr(blessed.formatters, 'resolve_capability', resolve_cap) - tparm = lambda *args: u'~'.join( - arg.decode('latin1') if not num else '%s' % (arg,) - for num, arg in enumerate(args)).encode('latin1') - monkeypatch.setattr(curses, 'tparm', tparm) + monkeypatch.setattr(curses, 'tparm', fn_tparm) term = mock.Mock() term.normal = 'seq-normal' @@ -329,18 +335,17 @@ def test_resolve_attribute_recursive_compoundables(monkeypatch): from blessed.formatters import resolve_attribute, FormattingString # patch, - def resolve_cap(term, digit): return 'seq-%s' % (digit,) + def resolve_cap(term, digit): + return 'seq-%s' % (digit,) monkeypatch.setattr(blessed.formatters, 'resolve_capability', resolve_cap) - tparm = lambda *args: u'~'.join( - arg.decode('latin1') if not num else '%s' % (arg,) - for num, arg in enumerate(args)).encode('latin1') - monkeypatch.setattr(curses, 'tparm', tparm) + monkeypatch.setattr(curses, 'tparm', fn_tparm) monkeypatch.setattr(curses, 'COLOR_RED', 6502) monkeypatch.setattr(curses, 'COLOR_BLUE', 6800) - def color_cap(digit): return 'seq-%s' % (digit,) + def color_cap(digit): + return 'seq-%s' % (digit,) term = mock.Mock() term._background_color = color_cap term._foreground_color = color_cap @@ -357,7 +362,7 @@ def color_cap(digit): return 'seq-%s' % (digit,) def test_pickled_parameterizing_string(monkeypatch): """Test pickle-ability of a formatters.ParameterizingString.""" - from blessed.formatters import ParameterizingString, FormattingString + from blessed.formatters import ParameterizingString # simply send()/recv() over multiprocessing Pipe, a simple # pickle.loads(dumps(...)) did not reproduce this issue, @@ -366,11 +371,7 @@ def test_pickled_parameterizing_string(monkeypatch): # first argument to tparm() is the sequence name, returned as-is; # subsequent arguments are usually Integers. - tparm = lambda *args: u'~'.join( - arg.decode('latin1') if not num else '%s' % (arg,) - for num, arg in enumerate(args)).encode('latin1') - - monkeypatch.setattr(curses, 'tparm', tparm) + monkeypatch.setattr(curses, 'tparm', fn_tparm) # given, pstr = ParameterizingString(u'seqname', u'norm', u'cap-name') @@ -417,7 +418,7 @@ def test_tparm_other_exception(monkeypatch): """Test 'tparm() returned NULL' is caught (win32 PDCurses systems).""" # on win32, any calls to tparm raises curses.error with message, # "tparm() returned NULL", function PyCurses_tparm of _cursesmodule.c - from blessed.formatters import ParameterizingString, NullCallableString + from blessed.formatters import ParameterizingString def tparm(*args): raise curses.error("unexpected error in tparm()") diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index 04ff7196..fd8353fb 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -1,33 +1,17 @@ # -*- coding: utf-8 -*- """Tests for keyboard support.""" # std imports -import os -import pty import sys import tty # NOQA -#import time -import math import curses -import signal import tempfile import functools # 3rd party -import six import mock -import pytest # local -from .accessories import (SEMAPHORE, - RECV_SEMAPHORE, - SEND_SEMAPHORE, - TestTerminal, - echo_off, - all_terms, - as_subprocess, - read_until_eof, - read_until_semaphore, - init_subproc_coverage) +from .accessories import TestTerminal, all_terms, as_subprocess if sys.version_info[0] == 3: unichr = chr diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index 726e012b..cd3d1903 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -1,21 +1,16 @@ # -*- coding: utf-8 -*- """Tests for Terminal() sequences and sequence-awareness.""" # std imports -import os import sys -import random import platform # 3rd party import six -import mock -import pytest # local from .accessories import (TestTerminal, all_terms, unicode_cap, - many_columns, unicode_parm, as_subprocess) @@ -363,8 +358,8 @@ def child(kind): t.normal)) else: expected_output = u'meh' - assert (t.on_bright_red_bold_bright_green_underline('meh') - == expected_output) + very_long_cap = t.on_bright_red_bold_bright_green_underline + assert (very_long_cap('meh') == expected_output) child(all_terms) diff --git a/docs/conf.py b/docs/conf.py index 69e640f8..1af36bc9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,14 +9,9 @@ import sphinx.environment from docutils.utils import get_source_line -# This file is execfile()d with the current directory set to its -# containing dir. - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. +# for github.py HERE = os.path.dirname(__file__) -sys.path.insert(0, os.path.abspath('sphinxext')) # for github.py +sys.path.insert(0, os.path.abspath('sphinxext')) github_project_url = "https://github.com/jquast/blessed" @@ -34,24 +29,29 @@ def _warn_node(self, msg, node): if not msg.startswith('nonlocal image URI found:'): self._warnfunc(msg, '%s:%s' % get_source_line(node)) + + sphinx.environment.BuildEnvironment.warn_node = _warn_node -# Monkey-patch functools.wraps and contextlib.wraps -# https://github.com/sphinx-doc/sphinx/issues/1711#issuecomment-93126473 + def no_op_wraps(func): """Replaces functools.wraps in order to undo wrapping when generating Sphinx documentation.""" if func.__module__ is None or 'blessed' not in func.__module__: return functools.orig_wraps(func) + def wrapper(decorator): sys.stderr.write('patched for function signature: {0!r}\n'.format(func)) return func return wrapper + +# Monkey-patch functools.wraps and contextlib.wraps +# https://github.com/sphinx-doc/sphinx/issues/1711#issuecomment-93126473 functools.orig_wraps = functools.wraps functools.wraps = no_op_wraps -import contextlib # isort:skip +import contextlib # isort:skip # noqa contextlib.wraps = no_op_wraps -from blessed.terminal import * # isort:skip +from blessed.terminal import * # isort:skip # noqa # -- General configuration ---------------------------------------------------- @@ -164,7 +164,7 @@ def wrapper(decorator): # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -#html_static_path = ['_static'] +# html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. diff --git a/tox.ini b/tox.ini index 21aa2363..5759af47 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,7 @@ envlist = about isort pylint flake8 + flake8_tests sphinx py{26,27,34,35,36,37,38} skip_missing_interpreters = true @@ -60,7 +61,8 @@ atomic = true [flake8] max-line-length = 100 -exclude = .tox +exclude = .tox,build +deps = flake8==3.7.9 [testenv:py26] setenv = TEST_QUICK=1 @@ -139,10 +141,12 @@ commands = {envbindir}/pylint --rcfile={toxinidir}/.pylintrc \ {posargs:{toxinidir}}/blessed [testenv:flake8] -description = Quick and basic Lint using 'flake8' tool -deps = flake8==3.7.9 -commands = {envbindir}/flake8 {toxinidir} +deps = {[flake8]deps} +commands = {envbindir}/flake8 --exclude=blessed/tests,docs/sphinxext/github.py setup.py docs/ blessed/ bin/ +[testenv:flake8_tests] +deps = {[flake8]deps} +commands = {envbindir}/flake8 --ignore=W504,F811,F401 blessed/tests/ [testenv:pydocstyle] deps = pydocstyle==3.0.0 From ec55521df8f054f5c5a1fa62282e6d72fb971ef9 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Wed, 15 Jan 2020 19:03:33 -0800 Subject: [PATCH 442/459] fix coverage, remove deprecated _intr_continue --- blessed/terminal.py | 10 +--------- docs/history.rst | 3 +++ tox.ini | 14 ++++++++------ 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/blessed/terminal.py b/blessed/terminal.py index ad0d618e..7fae7558 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -987,7 +987,7 @@ def ungetch(self, text): """ self._keyboard_buf.extendleft(text) - def kbhit(self, timeout=None, **_kwargs): + def kbhit(self, timeout=None): """ Return whether a keypress has been detected on the keyboard. @@ -1005,14 +1005,6 @@ def kbhit(self, timeout=None, **_kwargs): attached to this terminal. When input is not a terminal, False is always returned. """ - if _kwargs.pop('_intr_continue', None) is not None: - warnings.warn('keyword argument _intr_continue deprecated: ' - 'beginning v1.9.6, behavior is as though such ' - 'value is always True.') - if _kwargs: - raise TypeError('inkey() got unexpected keyword arguments {!r}' - .format(_kwargs)) - stime = time.time() ready_r = [None, ] check_r = [self._keyboard_fd] if self._keyboard_fd is not None else [] diff --git a/docs/history.rst b/docs/history.rst index 8a0a4118..c1ea6a19 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -28,6 +28,9 @@ Version History :attr:`~Terminal.stream` is not a terminal, programs using :meth:`~Terminal.inkey` to block indefinitely if a keyboard is not attached. :ghissue:`69`. + * deprecated: using argument ``_intr_continue`` to method + :meth:`~Terminal.kbhit`, behavior is as though such value is always True + since 1.9. 1.16 * introduced: Windows support?! :ghissue:`110` by :ghuser:`avylove`. diff --git a/tox.ini b/tox.ini index 5759af47..3910f07a 100644 --- a/tox.ini +++ b/tox.ini @@ -12,16 +12,10 @@ skip_missing_interpreters = true [testenv] basepython = python3.8 -looponfailroots = blessed -norecursedirs = .git .tox build deps = pytest==5.3.2 pytest-cov==2.8.1 pytest-xdist==1.31.0 mock==3.0.5 -addopts = --cov-append --cov-report=html --color=yes --ignore=setup.py --ignore=.tox - --log-format='%(levelname)s %(relativeCreated)2.2f %(filename)s:%(lineno)d %(message)s' - --cov=blessed -junit_family = xunit1 commands = {envbindir}/py.test \ --disable-pytest-warnings \ --cov-config={toxinidir}/tox.ini \ @@ -32,6 +26,14 @@ commands = {envbindir}/py.test \ } \ blessed/tests +[pytest] +looponfailroots = blessed +norecursedirs = .git .tox build +addopts = --cov-append --cov-report=html --color=yes --ignore=setup.py --ignore=.tox + --log-format='%(levelname)s %(relativeCreated)2.2f %(filename)s:%(lineno)d %(message)s' + --cov=blessed +junit_family = xunit1 + [coverage:run] branch = True source = blessed From 1db03ae2ebea2bd5b7767b9998e08c71abbfffa7 Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Thu, 16 Jan 2020 20:17:39 -0500 Subject: [PATCH 443/459] Improve usability of colorchart.py --- bin/colorchart.py | 136 +++++++++++++++++++++++++--------------------- 1 file changed, 73 insertions(+), 63 deletions(-) diff --git a/bin/colorchart.py b/bin/colorchart.py index ba618439..ee54a9ef 100644 --- a/bin/colorchart.py +++ b/bin/colorchart.py @@ -1,91 +1,101 @@ +# encoding: utf-8 +""" +Utility to show X11 colors in 24-bit and down-converted to 256, 16, and 8 color +The time to generate the table is displayed to give an indication of how long each +algorithm takes compared to the others. +""" # std imports -import re +import colorsys +import sys +import timeit # local import blessed +from blessed.color import COLOR_DISTANCE_ALGORITHMS from blessed.colorspace import X11_COLORNAMES_TO_RGB -RE_NATURAL = re.compile(r'(dark|light|)(.+?)(\d*)$') +def sort_colors(): + """ + Sort colors by HSV value and remove duplicates + """ + colors = {} + for color_name, rgb_color in X11_COLORNAMES_TO_RGB.items(): + if rgb_color not in colors: + colors[rgb_color] = color_name -def naturalize(string): + return sorted(colors.items(), + key=lambda rgb: colorsys.rgb_to_hsv(*rgb[0]), + reverse=True) - intensity, word, num = RE_NATURAL.match(string).groups() - if intensity == 'light': - intensity = -1 - elif intensity == 'medium': - intensity = 1 - elif intensity == 'dark': - intensity = 2 - else: - intensity = 0 +ALGORITHMS = tuple(sorted(COLOR_DISTANCE_ALGORITHMS)) +SORTED_COLORS = sort_colors() - return word, intensity, int(num) if num else 0 +def draw_chart(term): + """ + Draw a chart of each X11 color represented as in 24-bit and as down-converted + to 256, 16, and 8 color with the currently configured algorithm. + """ + term.move(0, 0) + sys.stdout.write(term.home) + width = term.width + line = '' + line_len = 0 -def color_table(term): - - output = {} - for color, code in X11_COLORNAMES_TO_RGB.items(): - - if code in output: - output[code] = '%s %s' % (output[code], color) - continue + start = timeit.default_timer() + for color in SORTED_COLORS: chart = '' for noc in (1 << 24, 256, 16, 8): term.number_of_colors = noc - chart += getattr(term, color)(u'█') - - output[code] = '%s %s' % (chart, color) + chart += getattr(term, color[1])(u'█') - for color in sorted(X11_COLORNAMES_TO_RGB, key=naturalize): - code = X11_COLORNAMES_TO_RGB[color] - if code in output: - print(output.pop(code)) + if line_len + 5 > width: + line += '\n' + line_len = 0 + line += ' %s' % chart + line_len += 5 -def color_chart(term): - - output = {} - for color, code in X11_COLORNAMES_TO_RGB.items(): - - if code in output: - continue - - chart = '' - for noc in (1 << 24, 256, 16, 8): - term.number_of_colors = noc - chart += getattr(term, color)(u'█') - - output[code] = chart + elapsed = round((timeit.default_timer() - start) * 1000) + print(line) - width = term.width + left_text = '[] to select, q to quit' + center_text = f'{term.color_distance_algorithm}' + right_text = f'{elapsed:d} ms\n' - line = '' - line_len = 0 - for color in sorted(X11_COLORNAMES_TO_RGB, key=naturalize): - code = X11_COLORNAMES_TO_RGB[color] - if code in output: - chart = output.pop(code) - if line_len + 5 > width: - print(line) - line = '' - line_len = 0 - - line += ' %s' % chart - line_len += 5 + sys.stdout.write(term.clear_eos + left_text + + term.center(center_text, term.width - + term.length(left_text) - term.length(right_text)) + + right_text) - print(line) - for color in sorted(X11_COLORNAMES_TO_RGB, key=naturalize): - code = X11_COLORNAMES_TO_RGB[color] - if code in output: - print(output.pop(code)) +def color_chart(term): + """ + Main color chart application + """ + term = blessed.Terminal() + algo_idx = 0 + dirty = True + with term.cbreak(), term.hidden_cursor(), term.fullscreen(): + while True: + if dirty: + draw_chart(term) + inp = term.inkey() + dirty = True + if inp in '[]': + algo_idx += 1 if inp == ']' else -1 + algo_idx = algo_idx % len(ALGORITHMS) + term.color_distance_algorithm = ALGORITHMS[algo_idx] + elif inp == '\x0c': + pass + elif inp in 'qQ': + break + else: + dirty = False if __name__ == '__main__': - - # color_table(blessed.Terminal()) color_chart(blessed.Terminal()) From 09ebe5f7e393bb183c1b77b554ce42028229f1c1 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Thu, 16 Jan 2020 22:26:44 -0800 Subject: [PATCH 444/459] Run terminal tests on Windows 10 using Travis-CI (#123) By manipulating our `@as_subprocess` test framework developed to execute tests in a sub-process to shield from the "cannot change terminal definition at runtime" issue -- for WIndows, no sub-process is used, and the `vtwin10` kind is always used. We achieve 65% code coverage on Windows, and integrate into Travis-CI. --- .travis.yml | 10 ++++++- bin/display-terminalinfo.py | 9 +++++- blessed/terminal.py | 13 +++++---- blessed/tests/accessories.py | 40 +++++++++++++++++++-------- blessed/tests/test_core.py | 35 +++++++++++++++-------- blessed/tests/test_formatters.py | 7 ++++- blessed/tests/test_keyboard.py | 20 +++++++++++--- blessed/tests/test_length_sequence.py | 27 +++++++++++++----- blessed/tests/test_sequences.py | 20 ++++++++++---- tox.ini | 2 ++ 10 files changed, 136 insertions(+), 47 deletions(-) diff --git a/.travis.yml b/.travis.yml index 610f1b35..478a81f5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,11 +18,19 @@ matrix: env: TOXENV=py39,coveralls TEST_QUICK=1 COVERAGE_ID=travis-ci - python: 3.8 env: TOXENV=about,pylint,flake8,flake8_tests,sphinx COVERAGE_ID=travis-ci + - python: 3.8 + os: windows + language: shell + before_install: + - choco install python --version 3.8.0 + - python -m pip install --upgrade pip + - python -m pip install tox + env: PATH=/c/Python38:/c/Python38/Scripts:$PATH TOXENV=py38,coveralls COVERAGE_ID=travis-ci jobs: allow_failures: - env: TOXENV=py39,coveralls TEST_QUICK=1 COVERAGE_ID=travis-ci - - env: TOXENV=about,pylint,flake8,sphinx COVERAGE_ID=travis-ci + - env: TOXENV=about,pylint,flake8,flake8_tests,sphinx COVERAGE_ID=travis-ci install: - pip install tox diff --git a/bin/display-terminalinfo.py b/bin/display-terminalinfo.py index fc8116a3..1f581c09 100755 --- a/bin/display-terminalinfo.py +++ b/bin/display-terminalinfo.py @@ -8,7 +8,7 @@ import os import sys import locale -import termios +import platform BITMAP_IFLAG = { 'IGNBRK': 'ignore BREAK condition', @@ -99,6 +99,7 @@ def display_bitmask(kind, bitmap, value): """Display all matching bitmask values for ``value`` given ``bitmap``.""" + import termios col1_width = max(map(len, list(bitmap.keys()) + [kind])) col2_width = 7 fmt = '{name:>{col1_width}} {value:>{col2_width}} {description}' @@ -126,6 +127,7 @@ def display_bitmask(kind, bitmap, value): def display_ctl_chars(index, ctlc): """Display all control character indicies, names, and values.""" + import termios title = 'Special Character' col1_width = len(title) col2_width = max(map(len, index.values())) @@ -175,6 +177,11 @@ def display_pathconf(names, getter): def main(): """Program entry point.""" + if platform.system() == 'Windows': + print('No terminal on windows systems!') + exit(0) + + import termios fd = sys.stdin.fileno() locale.setlocale(locale.LC_ALL, '') encoding = locale.getpreferredencoding() diff --git a/blessed/terminal.py b/blessed/terminal.py index 7fae7558..0d739746 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -51,9 +51,9 @@ from ordereddict import OrderedDict +HAS_TTY = True if platform.system() == 'Windows': import jinxed as curses # pylint: disable=import-error - HAS_TTY = True else: import curses @@ -61,14 +61,15 @@ import termios import fcntl import tty - HAS_TTY = True except ImportError: _TTY_METHODS = ('setraw', 'cbreak', 'kbhit', 'height', 'width') _MSG_NOSUPPORT = ( "One or more of the modules: 'termios', 'fcntl', and 'tty' " - "are not found on your platform '{0}'. The following methods " - "of Terminal are dummy/no-op unless a deriving class overrides " - "them: {1}".format(sys.platform.lower(), ', '.join(_TTY_METHODS))) + "are not found on your platform '{platform}'. " + "The following methods of Terminal are dummy/no-op " + "unless a deriving class overrides them: {tty_methods}." + .format(platform=platform.system(), + tty_methods=', '.join(_TTY_METHODS))) warnings.warn(_MSG_NOSUPPORT) HAS_TTY = False @@ -201,7 +202,7 @@ def __init__(self, kind=None, stream=None, force_styling=False): if self.does_styling: # Initialize curses (call setupterm), so things like tigetstr() work. try: - curses.setupterm(kind, open(os.devnull).fileno()) + curses.setupterm(self._kind, open(os.devnull).fileno()) except curses.error as err: warnings.warn('Failed to setupterm(kind={0!r}): {1}' .format(self._kind, err)) diff --git a/blessed/tests/accessories.py b/blessed/tests/accessories.py index 0e3a5c93..c4180b51 100644 --- a/blessed/tests/accessories.py +++ b/blessed/tests/accessories.py @@ -5,11 +5,9 @@ # std imports import os -import pty import sys import codecs -import curses -import termios +import platform import functools import traceback import contextlib @@ -22,7 +20,18 @@ # local from blessed import Terminal -TestTerminal = functools.partial(Terminal, kind='xterm-256color') +if platform.system() != "Windows": + import curses + import pty + import termios +else: + import jinxed as curses + + +test_kind = 'xterm-256color' +if platform.system() == 'Windows': + test_kind = 'vtwin10' +TestTerminal = functools.partial(Terminal, kind=test_kind) SEND_SEMAPHORE = SEMAPHORE = b'SEMAPHORE\n' RECV_SEMAPHORE = b'SEMAPHORE\r\n' many_lines_params = [40, 80] @@ -46,6 +55,8 @@ .communicate()[0].splitlines()] except OSError: pass +elif platform.system() == 'Windows': + all_terms_params = ['vtwin10', ] elif os.environ.get('TEST_QUICK'): all_terms_params = 'xterm screen ansi linux'.split() @@ -75,6 +86,10 @@ def __init__(self, func): self.func = func def __call__(self, *args, **kwargs): + if platform.system() == 'Windows': + self.func(*args, **kwargs) + return + pid_testrunner = os.getpid() pid, master_fd = pty.fork() if pid == self._CHILD_PID: @@ -197,14 +212,17 @@ def read_until_eof(fd, encoding='utf8'): @contextlib.contextmanager def echo_off(fd): """Ensure any bytes written to pty fd are not duplicated as output.""" - try: - attrs = termios.tcgetattr(fd) - attrs[3] = attrs[3] & ~termios.ECHO - termios.tcsetattr(fd, termios.TCSANOW, attrs) + if platform.system() != 'Windows': + try: + attrs = termios.tcgetattr(fd) + attrs[3] = attrs[3] & ~termios.ECHO + termios.tcsetattr(fd, termios.TCSANOW, attrs) + yield + finally: + attrs[3] = attrs[3] | termios.ECHO + termios.tcsetattr(fd, termios.TCSANOW, attrs) + else: yield - finally: - attrs[3] = attrs[3] | termios.ECHO - termios.tcsetattr(fd, termios.TCSANOW, attrs) def unicode_cap(cap): diff --git a/blessed/tests/test_core.py b/blessed/tests/test_core.py index fa9b7d20..48ccf181 100644 --- a/blessed/tests/test_core.py +++ b/blessed/tests/test_core.py @@ -14,6 +14,7 @@ # 3rd party import six import mock +import pytest from six.moves import reload_module # local @@ -86,6 +87,7 @@ def child(): child() +@pytest.mark.skipif(platform.system() == 'Windows', reason="requires more than 1 tty") def test_number_of_colors_without_tty(): """``number_of_colors`` should return 0 when there's no tty.""" @as_subprocess @@ -121,6 +123,7 @@ def child_0_forcestyle(): child_256_nostyle() +@pytest.mark.skipif(platform.system() == 'Windows', reason="requires more than 1 tty") def test_number_of_colors_with_tty(): """test ``number_of_colors`` 0, 8, and 256.""" @as_subprocess @@ -174,7 +177,7 @@ def child(kind): child(all_terms) -def test_setupterm_singleton_issue33(): +def test_setupterm_singleton_issue_33(): """A warning is emitted if a new terminal ``kind`` is used per process.""" @as_subprocess def child(): @@ -182,16 +185,18 @@ def child(): # instantiate first terminal, of type xterm-256color term = TestTerminal(force_styling=True) + first_kind = term.kind + next_kind = 'xterm' try: # a second instantiation raises UserWarning - term = TestTerminal(kind="vt220", force_styling=True) + term = TestTerminal(kind=next_kind, force_styling=True) except UserWarning: err = sys.exc_info()[1] assert (err.args[0].startswith( - 'A terminal of kind "vt220" has been requested') + 'A terminal of kind "' + next_kind + '" has been requested') ), err.args[0] - assert ('a terminal of kind "xterm-256color" will ' + assert ('a terminal of kind "' + first_kind + '" will ' 'continue to be returned' in err.args[0]), err.args[0] else: # unless term is not a tty and setupterm() is not called @@ -216,9 +221,12 @@ def child(): term = TestTerminal(kind='unknown', force_styling=True) except UserWarning: err = sys.exc_info()[1] - assert err.args[0] == ( + assert err.args[0] in ( "Failed to setupterm(kind='unknown'): " - "setupterm: could not find terminal") + "setupterm: could not find terminal", + "Failed to setupterm(kind='unknown'): " + "Could not find terminal unknown", + ) else: if platform.system().lower() != 'freebsd': assert not term.is_a_tty and not term.does_styling, ( @@ -316,6 +324,7 @@ def side_effect(): child() +@pytest.mark.skipif(platform.system() == 'Windows', reason="has process-wide side-effects") def test_winsize_IOError_returns_environ(): """When _winsize raises IOError, defaults from os.environ given.""" @as_subprocess @@ -362,6 +371,7 @@ def child(kind): child(all_terms) +@pytest.mark.skipif(platform.system() == 'Windows', reason="windows doesn't work like this") def test_no_preferredencoding_fallback_ascii(): """Ensure empty preferredencoding value defaults to ascii.""" @as_subprocess @@ -374,22 +384,24 @@ def child(): child() +@pytest.mark.skipif(platform.system() == 'Windows', reason="requires fcntl") +#@pytest.mark.filterwarnings("ignore:LookupError") def test_unknown_preferredencoding_warned_and_fallback_ascii(): """Ensure a locale without a codec emits a warning.""" @as_subprocess def child(): with mock.patch('locale.getpreferredencoding') as get_enc: - with warnings.catch_warnings(record=True) as warned: - get_enc.return_value = '---unknown--encoding---' + get_enc.return_value = '---unknown--encoding---' + with pytest.warns(UserWarning, match=( + 'LookupError: unknown encoding: ---unknown--encoding---, ' + 'fallback to ASCII for keyboard.')): t = TestTerminal() assert t._encoding == 'ascii' - assert len(warned) == 1 - assert issubclass(warned[-1].category, UserWarning) - assert "fallback to ASCII" in str(warned[-1].message) child() +@pytest.mark.skipif(platform.system() == 'Windows', reason="requires fcntl") def test_win32_missing_tty_modules(monkeypatch): """Ensure dummy exception is used when io is without UnsupportedOperation.""" @as_subprocess @@ -467,6 +479,7 @@ def test_time_left_infinite_None(): assert _time_left(stime=time.time(), timeout=None) is None +@pytest.mark.skipif(platform.system() == 'Windows', reason="cant multiprocess") def test_termcap_repr(): """Ensure ``hidden_cursor()`` writes hide_cursor and normal_cursor.""" diff --git a/blessed/tests/test_formatters.py b/blessed/tests/test_formatters.py index eccc4776..cfc01475 100644 --- a/blessed/tests/test_formatters.py +++ b/blessed/tests/test_formatters.py @@ -1,12 +1,17 @@ # -*- coding: utf-8 -*- """Tests string formatting functions.""" # std imports -import curses +import platform # 3rd party import mock import pytest +if platform.system() != 'Windows': + import curses +else: + import jinxed as curses + def fn_tparm(*args): return u'~'.join( diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index fd8353fb..afc2e619 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -2,21 +2,27 @@ """Tests for keyboard support.""" # std imports import sys -import tty # NOQA -import curses +import platform import tempfile import functools # 3rd party import mock +import pytest # local from .accessories import TestTerminal, all_terms, as_subprocess +if platform.system() != 'Windows': + import curses + import tty # NOQA +else: + import jinxed as curses + if sys.version_info[0] == 3: unichr = chr - +@pytest.mark.skipif(platform.system() == 'Windows', reason="?") def test_break_input_no_kb(): """cbreak() should not call tty.setcbreak() without keyboard.""" @as_subprocess @@ -30,6 +36,7 @@ def child(): child() +@pytest.mark.skipif(platform.system() == 'Windows', reason="?") def test_raw_input_no_kb(): """raw should not call tty.setraw() without keyboard.""" @as_subprocess @@ -43,6 +50,7 @@ def child(): child() +@pytest.mark.skipif(platform.system() == 'Windows', reason="?") def test_raw_input_with_kb(): """raw should call tty.setraw() when with keyboard.""" @as_subprocess @@ -162,7 +170,10 @@ def child(kind): assert len(sequence) <= maxlen assert sequence maxlen = len(sequence) - child(kind='xterm-256color') + kind = 'xterm-256color' + if platform.system() == 'Windows': + kind = 'vtwin10' + child(kind) def test_get_keyboard_sequence(monkeypatch): @@ -276,6 +287,7 @@ def test_keyboard_prefixes(): assert pfs == set([u'a', u'ab', u'abd', u'j', u'jk']) +@pytest.mark.skipif(platform.system() == 'Windows', reason="no multiprocess") def test_keypad_mixins_and_aliases(): """Test PC-Style function key translations when in ``keypad`` mode.""" # Key plain app modified diff --git a/blessed/tests/test_length_sequence.py b/blessed/tests/test_length_sequence.py index 8272583b..813dc409 100644 --- a/blessed/tests/test_length_sequence.py +++ b/blessed/tests/test_length_sequence.py @@ -2,24 +2,27 @@ # std imports import os import sys -import fcntl import struct -import termios +import platform import itertools # 3rd party import six +import pytest # local from blessed.tests.accessories import ( # isort:skip TestTerminal, as_subprocess, all_terms, many_lines, many_columns ) +if platform.system() != 'Windows': + import fcntl + import termios def test_length_cjk(): @as_subprocess def child(): - term = TestTerminal(kind='xterm-256color') + term = TestTerminal() # given, given = term.bold_red(u'コンニチハ, セカイ!') @@ -33,9 +36,9 @@ def child(): def test_length_ansiart(): @as_subprocess - def child(): + def child(kind): import codecs - term = TestTerminal(kind='xterm-256color') + term = TestTerminal(kind=kind) # this 'ansi' art contributed by xzip!impure for another project, # unlike most CP-437 DOS ansi art, this is actually utf-8 encoded. fname = os.path.join(os.path.dirname(__file__), 'wall.ans') @@ -47,9 +50,14 @@ def child(): assert term.length(lines[4]) == 78 assert term.length(lines[5]) == 78 assert term.length(lines[6]) == 77 - child() + kind = 'xterm-256color' + if platform.system() == 'Windows': + kind = 'vtwin10' + child(kind) +@pytest.mark.skipif(platform.system() == 'Windows', + reason='https://github.com/jquast/blessed/issues/122') def test_sequence_length(all_terms): """Ensure T.length(string containing sequence) is correcterm.""" @as_subprocess @@ -164,12 +172,13 @@ def child(kind): # XXX why are some terminals width of 9 here ?? assert (term.length(u'\t') in (8, 9)) assert (term.strip(u'\t') == u'') + assert (term.length(u'_' + term.move_left) == 0) + assert (term.length(term.move_right) == 1) if term.cub: assert (term.length((u'_' * 10) + term.cub(10)) == 0) - assert (term.length(term.move_right) == 1) if term.cuf: assert (term.length(term.cuf(10)) == 10) @@ -215,6 +224,7 @@ def child(): child() +@pytest.mark.skipif(platform.system() == 'Windows', reason="requires fcntl") def test_winsize(many_lines, many_columns): """Test height and width is appropriately queried in a pty.""" @as_subprocess @@ -254,6 +264,7 @@ def child(kind): assert (term.length(radjusted) == len(pony_msg.rjust(88))) +@pytest.mark.skipif(platform.system() == 'Windows', reason="requires fcntl") def test_Sequence_alignment(all_terms): """Tests methods related to Sequence class, namely ljust, rjust, center.""" @as_subprocess @@ -392,6 +403,8 @@ def child(kind): child(all_terms) +@pytest.mark.skipif(platform.system() == 'Windows', + reason='https://github.com/jquast/blessed/issues/122') def test_termcap_will_move_true(all_terms): """Test parser about sequences that move the cursor.""" @as_subprocess diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index cd3d1903..50082789 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -6,6 +6,7 @@ # 3rd party import six +import pytest # local from .accessories import (TestTerminal, @@ -15,12 +16,12 @@ as_subprocess) +@pytest.mark.skipif(platform.system() == 'Windows', reason="requires real tty") def test_capability(): """Check that capability lookup works.""" @as_subprocess def child(): - # Also test that Terminal grabs a reasonable default stream. This test - # assumes it will be run from a tty. + # Also test that Terminal grabs a reasonable default stream. t = TestTerminal() sc = unicode_cap('sc') assert t.save == sc @@ -50,6 +51,8 @@ def child(): child() +@pytest.mark.skipif(platform.system() == 'Windows', + reason='https://github.com/jquast/blessed/issues/122') def test_parametrization(): """Test parameterizing a capability.""" @as_subprocess @@ -153,6 +156,7 @@ def child(kind): child(all_terms) +@pytest.mark.skipif(platform.system() == 'Windows', reason="requires multiprocess") def test_inject_move_x(): """Test injection of hpa attribute for screen/ansi (issue #55).""" @as_subprocess @@ -173,6 +177,7 @@ def child(kind): child('ansi') +@pytest.mark.skipif(platform.system() == 'Windows', reason="requires multiprocess") def test_inject_move_y(): """Test injection of vpa attribute for screen/ansi (issue #55).""" @as_subprocess @@ -193,6 +198,7 @@ def child(kind): child('ansi') +@pytest.mark.skipif(platform.system() == 'Windows', reason="requires multiprocess") def test_inject_civis_and_cnorm_for_ansi(): """Test injection of cvis attribute for ansi.""" @as_subprocess @@ -206,6 +212,7 @@ def child(kind): child('ansi') +@pytest.mark.skipif(platform.system() == 'Windows', reason="requires multiprocess") def test_inject_sc_and_rc_for_ansi(): """Test injection of sc and rc (save and restore cursor) for ansi.""" @as_subprocess @@ -475,16 +482,19 @@ def child(kind): def test_padd(): """Test Terminal.padd(seq).""" @as_subprocess - def child(): + def child(kind): from blessed.sequences import Sequence from blessed import Terminal - term = Terminal('xterm-256color') + term = Terminal(kind) assert Sequence('xyz\b', term).padd() == u'xy' assert Sequence('xyz\b-', term).padd() == u'xy-' assert Sequence('xxxx\x1b[3Dzz', term).padd() == u'xzz' assert Sequence('\x1b[3D', term).padd() == u'' # "Trim left" - child() + kind = 'xterm-256color' + if platform.system() == 'Windows': + kind = 'vtwin10' + child(kind) def test_split_seqs(all_terms): diff --git a/tox.ini b/tox.ini index 3910f07a..3e833300 100644 --- a/tox.ini +++ b/tox.ini @@ -32,6 +32,8 @@ norecursedirs = .git .tox build addopts = --cov-append --cov-report=html --color=yes --ignore=setup.py --ignore=.tox --log-format='%(levelname)s %(relativeCreated)2.2f %(filename)s:%(lineno)d %(message)s' --cov=blessed +filterwarnings = + error junit_family = xunit1 [coverage:run] From 8ee991aa1608a7b78ce31b832cedc399d01f495c Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Thu, 16 Jan 2020 23:37:29 -0800 Subject: [PATCH 445/459] Bring back some oldies, use new coverage service (#124) Bring back full keyboard tests (TEST_KEYBOARD=yes), change coverage service to codecov.io which *combines* windows and linux coverage. --- .travis.yml | 18 +- bin/colorchart.py | 24 +- blessed/keyboard.py | 2 +- blessed/tests/test_core.py | 1 - blessed/tests/test_full_keyboard.py | 660 ++++++++++++++++++++++++++ blessed/tests/test_keyboard.py | 1 + blessed/tests/test_length_sequence.py | 3 +- blessed/tests/test_sequences.py | 6 +- docs/intro.rst | 4 + tox.ini | 30 +- 10 files changed, 706 insertions(+), 43 deletions(-) create mode 100644 blessed/tests/test_full_keyboard.py diff --git a/.travis.yml b/.travis.yml index 478a81f5..5f6e9c19 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,19 +3,19 @@ matrix: fast_finish: true include: - python: 2.7 - env: TOXENV=py27,coveralls TEST_QUICK=1 COVERAGE_ID=travis-ci + env: TOXENV=py27,codecov TEST_QUICK=1 COVERAGE_ID=travis-ci - python: 3.4 - env: TOXENV=py34,coveralls TEST_QUICK=1 COVERAGE_ID=travis-ci + env: TOXENV=py34,codecov TEST_QUICK=1 COVERAGE_ID=travis-ci - python: 3.5 - env: TOXENV=py35,coveralls TEST_QUICK=1 COVERAGE_ID=travis-ci + env: TOXENV=py35,codecov TEST_QUICK=1 COVERAGE_ID=travis-ci - python: 3.6 - env: TOXENV=py36,coveralls TEST_QUICK=1 COVERAGE_ID=travis-ci + env: TOXENV=py36,codecov TEST_QUICK=1 COVERAGE_ID=travis-ci - python: 3.7 - env: TOXENV=py37,coveralls TEST_QUICK=1 COVERAGE_ID=travis-ci + env: TOXENV=py37,codecov TEST_QUICK=1 COVERAGE_ID=travis-ci - python: 3.8 - env: TOXENV=py38,coveralls COVERAGE_ID=travis-ci + env: TOXENV=py38,codecov COVERAGE_ID=travis-ci - python: 3.9-dev - env: TOXENV=py39,coveralls TEST_QUICK=1 COVERAGE_ID=travis-ci + env: TOXENV=py39,codecov TEST_QUICK=1 COVERAGE_ID=travis-ci - python: 3.8 env: TOXENV=about,pylint,flake8,flake8_tests,sphinx COVERAGE_ID=travis-ci - python: 3.8 @@ -25,11 +25,11 @@ matrix: - choco install python --version 3.8.0 - python -m pip install --upgrade pip - python -m pip install tox - env: PATH=/c/Python38:/c/Python38/Scripts:$PATH TOXENV=py38,coveralls COVERAGE_ID=travis-ci + env: PATH=/c/Python38:/c/Python38/Scripts:$PATH TOXENV=py38,codecov COVERAGE_ID=travis-ci TEST_KEYBOARD=no jobs: allow_failures: - - env: TOXENV=py39,coveralls TEST_QUICK=1 COVERAGE_ID=travis-ci + - env: TOXENV=py39,codecov TEST_QUICK=1 COVERAGE_ID=travis-ci - env: TOXENV=about,pylint,flake8,flake8_tests,sphinx COVERAGE_ID=travis-ci install: diff --git a/bin/colorchart.py b/bin/colorchart.py index ee54a9ef..be7aa2ab 100644 --- a/bin/colorchart.py +++ b/bin/colorchart.py @@ -1,13 +1,11 @@ # encoding: utf-8 -""" -Utility to show X11 colors in 24-bit and down-converted to 256, 16, and 8 color -The time to generate the table is displayed to give an indication of how long each -algorithm takes compared to the others. -""" +"""Utility to show X11 colors in 24-bit and down-converted to 256, 16, and 8 color The time to +generate the table is displayed to give an indication of how long each algorithm takes compared to +the others.""" # std imports -import colorsys import sys import timeit +import colorsys # local import blessed @@ -16,9 +14,7 @@ def sort_colors(): - """ - Sort colors by HSV value and remove duplicates - """ + """Sort colors by HSV value and remove duplicates.""" colors = {} for color_name, rgb_color in X11_COLORNAMES_TO_RGB.items(): if rgb_color not in colors: @@ -34,10 +30,8 @@ def sort_colors(): def draw_chart(term): - """ - Draw a chart of each X11 color represented as in 24-bit and as down-converted - to 256, 16, and 8 color with the currently configured algorithm. - """ + """Draw a chart of each X11 color represented as in 24-bit and as down-converted to 256, 16, and + 8 color with the currently configured algorithm.""" term.move(0, 0) sys.stdout.write(term.home) width = term.width @@ -73,9 +67,7 @@ def draw_chart(term): def color_chart(term): - """ - Main color chart application - """ + """Main color chart application.""" term = blessed.Terminal() algo_idx = 0 dirty = True diff --git a/blessed/keyboard.py b/blessed/keyboard.py index 777c041e..d1ff45a4 100644 --- a/blessed/keyboard.py +++ b/blessed/keyboard.py @@ -412,7 +412,7 @@ def _read_until(term, pattern, timeout): (u"\x1bOv", KEY_KP_6), # noqa 6 (u"\x1bOw", KEY_KP_7), # noqa 7 (u"\x1bOx", KEY_KP_8), # noqa 8 - (u"\x1bOy", KEY_KP_9), # noqa 9 + (u"\x1bOy", KEY_KP_9), # noqa 9 # keypad, numlock off (u"\x1b[1~", curses.KEY_FIND), # find diff --git a/blessed/tests/test_core.py b/blessed/tests/test_core.py index 48ccf181..9c5a75f5 100644 --- a/blessed/tests/test_core.py +++ b/blessed/tests/test_core.py @@ -385,7 +385,6 @@ def child(): @pytest.mark.skipif(platform.system() == 'Windows', reason="requires fcntl") -#@pytest.mark.filterwarnings("ignore:LookupError") def test_unknown_preferredencoding_warned_and_fallback_ascii(): """Ensure a locale without a codec emits a warning.""" @as_subprocess diff --git a/blessed/tests/test_full_keyboard.py b/blessed/tests/test_full_keyboard.py new file mode 100644 index 00000000..b47fc75c --- /dev/null +++ b/blessed/tests/test_full_keyboard.py @@ -0,0 +1,660 @@ +# -*- coding: utf-8 -*- +# std imports +import os +import pty +import sys +import math +import time +import signal +import platform + +# 3rd party +import six +import pytest + +# local +from .accessories import (SEMAPHORE, + RECV_SEMAPHORE, + SEND_SEMAPHORE, + TestTerminal, + echo_off, + as_subprocess, + read_until_eof, + read_until_semaphore, + init_subproc_coverage) + +pytestmark = pytest.mark.skipif( + os.environ.get('TEST_KEYBOARD', None) != 'yes' or platform.system() == 'Windows', + reason="Timing-sensitive tests please do not run on build farms.") + +@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, + reason="TEST_QUICK specified") +def test_kbhit_interrupted(): + """kbhit() should not be interrupted with a signal handler.""" + pid, master_fd = pty.fork() + if pid == 0: + cov = init_subproc_coverage('test_kbhit_interrupted') + + # child pauses, writes semaphore and begins awaiting input + global got_sigwinch + got_sigwinch = False + + def on_resize(sig, action): + global got_sigwinch + got_sigwinch = True + + term = TestTerminal() + signal.signal(signal.SIGWINCH, on_resize) + read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.raw(): + assert term.inkey(timeout=1.05) == u'' + os.write(sys.__stdout__.fileno(), b'complete') + assert got_sigwinch + if cov is not None: + cov.stop() + cov.save() + os._exit(0) + + with echo_off(master_fd): + os.write(master_fd, SEND_SEMAPHORE) + read_until_semaphore(master_fd) + stime = time.time() + os.kill(pid, signal.SIGWINCH) + output = read_until_eof(master_fd) + + pid, status = os.waitpid(pid, 0) + assert output == u'complete' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 1.0 + + +@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, + reason="TEST_QUICK specified") +def test_kbhit_interrupted_nonetype(): + """kbhit() should also allow interruption with timeout of None.""" + pid, master_fd = pty.fork() + if pid == 0: + cov = init_subproc_coverage('test_kbhit_interrupted_nonetype') + + # child pauses, writes semaphore and begins awaiting input + global got_sigwinch + got_sigwinch = False + + def on_resize(sig, action): + global got_sigwinch + got_sigwinch = True + + term = TestTerminal() + signal.signal(signal.SIGWINCH, on_resize) + read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.raw(): + term.inkey(timeout=1) + os.write(sys.__stdout__.fileno(), b'complete') + assert got_sigwinch + if cov is not None: + cov.stop() + cov.save() + os._exit(0) + + with echo_off(master_fd): + os.write(master_fd, SEND_SEMAPHORE) + read_until_semaphore(master_fd) + stime = time.time() + time.sleep(0.05) + os.kill(pid, signal.SIGWINCH) + output = read_until_eof(master_fd) + + pid, status = os.waitpid(pid, 0) + assert output == u'complete' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 1.0 + + +def test_kbhit_no_kb(): + """kbhit() always immediately returns False without a keyboard.""" + @as_subprocess + def child(): + term = TestTerminal(stream=six.StringIO()) + stime = time.time() + assert term._keyboard_fd is None + assert not term.kbhit(timeout=1.1) + assert math.floor(time.time() - stime) == 1.0 + child() + + +def test_keystroke_0s_cbreak_noinput(): + """0-second keystroke without input; '' should be returned.""" + @as_subprocess + def child(): + term = TestTerminal() + with term.cbreak(): + stime = time.time() + inp = term.inkey(timeout=0) + assert (inp == u'') + assert (math.floor(time.time() - stime) == 0.0) + child() + + +def test_keystroke_0s_cbreak_noinput_nokb(): + """0-second keystroke without data in input stream and no keyboard/tty.""" + @as_subprocess + def child(): + term = TestTerminal(stream=six.StringIO()) + with term.cbreak(): + stime = time.time() + inp = term.inkey(timeout=0) + assert (inp == u'') + assert (math.floor(time.time() - stime) == 0.0) + child() + + +@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, + reason="TEST_QUICK specified") +def test_keystroke_1s_cbreak_noinput(): + """1-second keystroke without input; '' should be returned after ~1 second.""" + @as_subprocess + def child(): + term = TestTerminal() + with term.cbreak(): + stime = time.time() + inp = term.inkey(timeout=1) + assert (inp == u'') + assert (math.floor(time.time() - stime) == 1.0) + child() + + +@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, + reason="TEST_QUICK specified") +def test_keystroke_1s_cbreak_noinput_nokb(): + """1-second keystroke without input or keyboard.""" + @as_subprocess + def child(): + term = TestTerminal(stream=six.StringIO()) + with term.cbreak(): + stime = time.time() + inp = term.inkey(timeout=1) + assert (inp == u'') + assert (math.floor(time.time() - stime) == 1.0) + child() + + +def test_keystroke_0s_cbreak_with_input(): + """0-second keystroke with input; Keypress should be immediately returned.""" + pid, master_fd = pty.fork() + if pid == 0: + cov = init_subproc_coverage('test_keystroke_0s_cbreak_with_input') + # child pauses, writes semaphore and begins awaiting input + term = TestTerminal() + read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.cbreak(): + inp = term.inkey(timeout=0) + os.write(sys.__stdout__.fileno(), inp.encode('utf-8')) + if cov is not None: + cov.stop() + cov.save() + os._exit(0) + + with echo_off(master_fd): + os.write(master_fd, SEND_SEMAPHORE) + os.write(master_fd, u'x'.encode('ascii')) + read_until_semaphore(master_fd) + stime = time.time() + output = read_until_eof(master_fd) + + pid, status = os.waitpid(pid, 0) + assert output == u'x' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 0.0 + + +def test_keystroke_cbreak_with_input_slowly(): + """0-second keystroke with input; Keypress should be immediately returned.""" + pid, master_fd = pty.fork() + if pid == 0: + cov = init_subproc_coverage('test_keystroke_cbreak_with_input_slowly') + # child pauses, writes semaphore and begins awaiting input + term = TestTerminal() + read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.cbreak(): + while True: + inp = term.inkey(timeout=0.5) + os.write(sys.__stdout__.fileno(), inp.encode('utf-8')) + if inp == 'X': + break + if cov is not None: + cov.stop() + cov.save() + os._exit(0) + + with echo_off(master_fd): + os.write(master_fd, SEND_SEMAPHORE) + os.write(master_fd, u'a'.encode('ascii')) + time.sleep(0.1) + os.write(master_fd, u'b'.encode('ascii')) + time.sleep(0.1) + os.write(master_fd, u'cdefgh'.encode('ascii')) + time.sleep(0.1) + os.write(master_fd, u'X'.encode('ascii')) + read_until_semaphore(master_fd) + stime = time.time() + output = read_until_eof(master_fd) + + pid, status = os.waitpid(pid, 0) + assert output == u'abcdefghX' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 0.0 + + +def test_keystroke_0s_cbreak_multibyte_utf8(): + """0-second keystroke with multibyte utf-8 input; should decode immediately.""" + # utf-8 bytes represent "latin capital letter upsilon". + pid, master_fd = pty.fork() + if pid == 0: # child + cov = init_subproc_coverage('test_keystroke_0s_cbreak_multibyte_utf8') + term = TestTerminal() + read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.cbreak(): + inp = term.inkey(timeout=0) + os.write(sys.__stdout__.fileno(), inp.encode('utf-8')) + if cov is not None: + cov.stop() + cov.save() + os._exit(0) + + with echo_off(master_fd): + os.write(master_fd, SEND_SEMAPHORE) + os.write(master_fd, u'\u01b1'.encode('utf-8')) + read_until_semaphore(master_fd) + stime = time.time() + output = read_until_eof(master_fd) + pid, status = os.waitpid(pid, 0) + assert output == u'Ʊ' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 0.0 + + +@pytest.mark.skipif(os.environ.get('TRAVIS', None) is not None, + reason="travis-ci does not handle ^C very well.") +def test_keystroke_0s_raw_input_ctrl_c(): + """0-second keystroke with raw allows receiving ^C.""" + pid, master_fd = pty.fork() + if pid == 0: # child + cov = init_subproc_coverage('test_keystroke_0s_raw_input_ctrl_c') + term = TestTerminal() + read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) + with term.raw(): + os.write(sys.__stdout__.fileno(), RECV_SEMAPHORE) + inp = term.inkey(timeout=0) + os.write(sys.__stdout__.fileno(), inp.encode('latin1')) + if cov is not None: + cov.stop() + cov.save() + os._exit(0) + + with echo_off(master_fd): + os.write(master_fd, SEND_SEMAPHORE) + # ensure child is in raw mode before sending ^C, + read_until_semaphore(master_fd) + os.write(master_fd, u'\x03'.encode('latin1')) + stime = time.time() + output = read_until_eof(master_fd) + pid, status = os.waitpid(pid, 0) + assert (output == u'\x03' or + output == u'' and not os.isatty(0)) + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 0.0 + + +def test_keystroke_0s_cbreak_sequence(): + """0-second keystroke with multibyte sequence; should decode immediately.""" + pid, master_fd = pty.fork() + if pid == 0: # child + cov = init_subproc_coverage('test_keystroke_0s_cbreak_sequence') + term = TestTerminal() + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.cbreak(): + inp = term.inkey(timeout=0) + os.write(sys.__stdout__.fileno(), inp.name.encode('ascii')) + sys.stdout.flush() + if cov is not None: + cov.stop() + cov.save() + os._exit(0) + + with echo_off(master_fd): + os.write(master_fd, u'\x1b[D'.encode('ascii')) + read_until_semaphore(master_fd) + stime = time.time() + output = read_until_eof(master_fd) + pid, status = os.waitpid(pid, 0) + assert output == u'KEY_LEFT' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 0.0 + + +@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, + reason="TEST_QUICK specified") +def test_keystroke_1s_cbreak_with_input(): + """1-second keystroke w/multibyte sequence; should return after ~1 second.""" + pid, master_fd = pty.fork() + if pid == 0: # child + cov = init_subproc_coverage('test_keystroke_1s_cbreak_with_input') + term = TestTerminal() + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.cbreak(): + inp = term.inkey(timeout=3) + os.write(sys.__stdout__.fileno(), inp.name.encode('utf-8')) + sys.stdout.flush() + if cov is not None: + cov.stop() + cov.save() + os._exit(0) + + with echo_off(master_fd): + read_until_semaphore(master_fd) + stime = time.time() + time.sleep(1) + os.write(master_fd, u'\x1b[C'.encode('ascii')) + output = read_until_eof(master_fd) + + pid, status = os.waitpid(pid, 0) + assert output == u'KEY_RIGHT' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 1.0 + + +@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, + reason="TEST_QUICK specified") +def test_esc_delay_cbreak_035(): + """esc_delay will cause a single ESC (\\x1b) to delay for 0.35.""" + pid, master_fd = pty.fork() + if pid == 0: # child + cov = init_subproc_coverage('test_esc_delay_cbreak_035') + term = TestTerminal() + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.cbreak(): + stime = time.time() + inp = term.inkey(timeout=5) + measured_time = (time.time() - stime) * 100 + os.write(sys.__stdout__.fileno(), ( + '%s %i' % (inp.name, measured_time,)).encode('ascii')) + sys.stdout.flush() + if cov is not None: + cov.stop() + cov.save() + os._exit(0) + + with echo_off(master_fd): + read_until_semaphore(master_fd) + stime = time.time() + os.write(master_fd, u'\x1b'.encode('ascii')) + key_name, duration_ms = read_until_eof(master_fd).split() + + pid, status = os.waitpid(pid, 0) + assert key_name == u'KEY_ESCAPE' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 0.0 + assert 34 <= int(duration_ms) <= 45, duration_ms + + +@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, + reason="TEST_QUICK specified") +def test_esc_delay_cbreak_135(): + """esc_delay=1.35 will cause a single ESC (\\x1b) to delay for 1.35.""" + pid, master_fd = pty.fork() + if pid == 0: # child + cov = init_subproc_coverage('test_esc_delay_cbreak_135') + term = TestTerminal() + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.cbreak(): + stime = time.time() + inp = term.inkey(timeout=5, esc_delay=1.35) + measured_time = (time.time() - stime) * 100 + os.write(sys.__stdout__.fileno(), ( + '%s %i' % (inp.name, measured_time,)).encode('ascii')) + sys.stdout.flush() + if cov is not None: + cov.stop() + cov.save() + os._exit(0) + + with echo_off(master_fd): + read_until_semaphore(master_fd) + stime = time.time() + os.write(master_fd, u'\x1b'.encode('ascii')) + key_name, duration_ms = read_until_eof(master_fd).split() + + pid, status = os.waitpid(pid, 0) + assert key_name == u'KEY_ESCAPE' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 1.0 + assert 134 <= int(duration_ms) <= 145, int(duration_ms) + + +def test_esc_delay_cbreak_timout_0(): + """esc_delay still in effect with timeout of 0 ("nonblocking").""" + pid, master_fd = pty.fork() + if pid == 0: # child + cov = init_subproc_coverage('test_esc_delay_cbreak_timout_0') + term = TestTerminal() + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.cbreak(): + stime = time.time() + inp = term.inkey(timeout=0) + measured_time = (time.time() - stime) * 100 + os.write(sys.__stdout__.fileno(), ( + '%s %i' % (inp.name, measured_time,)).encode('ascii')) + sys.stdout.flush() + if cov is not None: + cov.stop() + cov.save() + os._exit(0) + + with echo_off(master_fd): + os.write(master_fd, u'\x1b'.encode('ascii')) + read_until_semaphore(master_fd) + stime = time.time() + key_name, duration_ms = read_until_eof(master_fd).split() + + pid, status = os.waitpid(pid, 0) + assert key_name == u'KEY_ESCAPE' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 0.0 + assert 34 <= int(duration_ms) <= 45, int(duration_ms) + + +def test_esc_delay_cbreak_nonprefix_sequence(): + """ESC a (\\x1ba) will return an ESC immediately.""" + pid, master_fd = pty.fork() + if pid == 0: # child + cov = init_subproc_coverage('test_esc_delay_cbreak_nonprefix_sequence') + term = TestTerminal() + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.cbreak(): + stime = time.time() + esc = term.inkey(timeout=5) + inp = term.inkey(timeout=5) + measured_time = (time.time() - stime) * 100 + os.write(sys.__stdout__.fileno(), ( + '%s %s %i' % (esc.name, inp, measured_time,)).encode('ascii')) + sys.stdout.flush() + if cov is not None: + cov.stop() + cov.save() + os._exit(0) + + with echo_off(master_fd): + read_until_semaphore(master_fd) + stime = time.time() + os.write(master_fd, u'\x1ba'.encode('ascii')) + key1_name, key2, duration_ms = read_until_eof(master_fd).split() + + pid, status = os.waitpid(pid, 0) + assert key1_name == u'KEY_ESCAPE' + assert key2 == u'a' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 0.0 + assert -1 <= int(duration_ms) <= 15, duration_ms + + +def test_esc_delay_cbreak_prefix_sequence(): + """An unfinished multibyte sequence (\\x1b[) will delay an ESC by .35.""" + pid, master_fd = pty.fork() + if pid == 0: # child + cov = init_subproc_coverage('test_esc_delay_cbreak_prefix_sequence') + term = TestTerminal() + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.cbreak(): + stime = time.time() + esc = term.inkey(timeout=5) + inp = term.inkey(timeout=5) + measured_time = (time.time() - stime) * 100 + os.write(sys.__stdout__.fileno(), ( + '%s %s %i' % (esc.name, inp, measured_time,)).encode('ascii')) + sys.stdout.flush() + if cov is not None: + cov.stop() + cov.save() + os._exit(0) + + with echo_off(master_fd): + read_until_semaphore(master_fd) + stime = time.time() + os.write(master_fd, u'\x1b['.encode('ascii')) + key1_name, key2, duration_ms = read_until_eof(master_fd).split() + + pid, status = os.waitpid(pid, 0) + assert key1_name == u'KEY_ESCAPE' + assert key2 == u'[' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 0.0 + assert 34 <= int(duration_ms) <= 45, duration_ms + + +def test_get_location_0s(): + """0-second get_location call without response.""" + @as_subprocess + def child(): + term = TestTerminal(stream=six.StringIO()) + stime = time.time() + y, x = term.get_location(timeout=0) + assert (math.floor(time.time() - stime) == 0.0) + assert (y, x) == (-1, -1) + child() + + +def test_get_location_0s_under_raw(): + """0-second get_location call without response under raw mode.""" + @as_subprocess + def child(): + term = TestTerminal(stream=six.StringIO()) + with term.raw(): + stime = time.time() + y, x = term.get_location(timeout=0) + assert (math.floor(time.time() - stime) == 0.0) + assert (y, x) == (-1, -1) + child() + + +def test_get_location_0s_reply_via_ungetch(): + """0-second get_location call with response.""" + @as_subprocess + def child(): + term = TestTerminal(stream=six.StringIO()) + stime = time.time() + # monkey patch in an invalid response ! + term.ungetch(u'\x1b[10;10R') + + y, x = term.get_location(timeout=0.01) + assert (math.floor(time.time() - stime) == 0.0) + assert (y, x) == (10, 10) + child() + + +def test_get_location_0s_reply_via_ungetch_under_raw(): + """0-second get_location call with response under raw mode.""" + @as_subprocess + def child(): + term = TestTerminal(stream=six.StringIO()) + with term.raw(): + stime = time.time() + # monkey patch in an invalid response ! + term.ungetch(u'\x1b[10;10R') + + y, x = term.get_location(timeout=0.01) + assert (math.floor(time.time() - stime) == 0.0) + assert (y, x) == (10, 10) + child() + + +def test_kbhit_no_kb(): + """kbhit() always immediately returns False without a keyboard.""" + @as_subprocess + def child(): + term = TestTerminal(stream=six.StringIO()) + stime = time.time() + assert term._keyboard_fd is None + assert not term.kbhit(timeout=1.1) + assert math.floor(time.time() - stime) == 1.0 + child() + + +def test_keystroke_0s_cbreak_noinput(): + """0-second keystroke without input; '' should be returned.""" + @as_subprocess + def child(): + term = TestTerminal() + with term.cbreak(): + stime = time.time() + inp = term.inkey(timeout=0) + assert (inp == u'') + assert (math.floor(time.time() - stime) == 0.0) + child() + + +def test_keystroke_0s_cbreak_noinput_nokb(): + """0-second keystroke without data in input stream and no keyboard/tty.""" + @as_subprocess + def child(): + term = TestTerminal(stream=six.StringIO()) + with term.cbreak(): + stime = time.time() + inp = term.inkey(timeout=0) + assert (inp == u'') + assert (math.floor(time.time() - stime) == 0.0) + child() + + +@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, + reason="TEST_QUICK specified") +def test_keystroke_1s_cbreak_noinput(): + """1-second keystroke without input; '' should be returned after ~1 second.""" + @as_subprocess + def child(): + term = TestTerminal() + with term.cbreak(): + stime = time.time() + inp = term.inkey(timeout=1) + assert (inp == u'') + assert (math.floor(time.time() - stime) == 1.0) + child() + + +@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None, + reason="TEST_QUICK specified") +def test_keystroke_1s_cbreak_noinput_nokb(): + """1-second keystroke without input or keyboard.""" + @as_subprocess + def child(): + term = TestTerminal(stream=six.StringIO()) + with term.cbreak(): + stime = time.time() + inp = term.inkey(timeout=1) + assert (inp == u'') + assert (math.floor(time.time() - stime) == 1.0) + child() diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index afc2e619..b604293a 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -22,6 +22,7 @@ if sys.version_info[0] == 3: unichr = chr + @pytest.mark.skipif(platform.system() == 'Windows', reason="?") def test_break_input_no_kb(): """cbreak() should not call tty.setcbreak() without keyboard.""" diff --git a/blessed/tests/test_length_sequence.py b/blessed/tests/test_length_sequence.py index 813dc409..daa46f95 100644 --- a/blessed/tests/test_length_sequence.py +++ b/blessed/tests/test_length_sequence.py @@ -10,7 +10,6 @@ import six import pytest -# local from blessed.tests.accessories import ( # isort:skip TestTerminal, as_subprocess, all_terms, many_lines, many_columns ) @@ -19,6 +18,7 @@ import fcntl import termios + def test_length_cjk(): @as_subprocess def child(): @@ -179,7 +179,6 @@ def child(kind): if term.cub: assert (term.length((u'_' * 10) + term.cub(10)) == 0) - if term.cuf: assert (term.length(term.cuf(10)) == 10) diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index 50082789..5811315b 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -9,11 +9,7 @@ import pytest # local -from .accessories import (TestTerminal, - all_terms, - unicode_cap, - unicode_parm, - as_subprocess) +from .accessories import TestTerminal, all_terms, unicode_cap, unicode_parm, as_subprocess @pytest.mark.skipif(platform.system() == 'Windows', reason="requires real tty") diff --git a/docs/intro.rst b/docs/intro.rst index 803382c3..b6a85c72 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -14,6 +14,10 @@ :alt: Coveralls Code Coverage :target: https://coveralls.io/github/jquast/blessed?branch=master +.. |codecov| image:: https://codecov.io/gh/jquast/blessed/branch/master/graph/badge.svg + :alt: codecov.io Code Coverage + :target: https://codecov.io/gh/jquast/blessed + .. |pypi| image:: https://img.shields.io/pypi/v/blessed.svg?logo=pypi :alt: Latest Version :target: https://pypi.python.org/pypi/blessed diff --git a/tox.ini b/tox.ini index 3e833300..6b88f3be 100644 --- a/tox.ini +++ b/tox.ini @@ -16,10 +16,7 @@ deps = pytest==5.3.2 pytest-cov==2.8.1 pytest-xdist==1.31.0 mock==3.0.5 -commands = {envbindir}/py.test \ - --disable-pytest-warnings \ - --cov-config={toxinidir}/tox.ini \ - {posargs:\ +commands = {envbindir}/py.test --cov-config={toxinidir}/tox.ini {posargs:\ --strict --verbose \ --junit-xml=.tox/results.{envname}.xml \ --durations=3 \ @@ -29,7 +26,8 @@ commands = {envbindir}/py.test \ [pytest] looponfailroots = blessed norecursedirs = .git .tox build -addopts = --cov-append --cov-report=html --color=yes --ignore=setup.py --ignore=.tox +addopts = --disable-pytest-warnings + --cov-append --cov-report=html --color=yes --ignore=setup.py --ignore=.tox --log-format='%(levelname)s %(relativeCreated)2.2f %(filename)s:%(lineno)d %(message)s' --cov=blessed filterwarnings = @@ -99,6 +97,20 @@ basepython = python3.6 setenv = TEST_QUICK=1 basepython = python3.7 +[testenv:py38] +basepython = python3.8 +deps = {[testenv]deps} + pytest-rerunfailures==8.0 +setenv = TEST_KEYBOARD = {env:TEST_KEYBOARD:yes} +commands = {envbindir}/py.test --cov-config={toxinidir}/tox.ini \ + {posargs:\ + --reruns 5 \ + --strict --verbose \ + --junit-xml=.tox/results.{envname}.xml \ + --durations=3 \ + } \ + blessed/tests + [testenv:develop] commands = {posargs} @@ -170,7 +182,7 @@ deps = Sphinx==2.3.1 commands = {envbindir}/sphinx-build \ {posargs:-v -W -d {toxinidir}/docs/_build/doctrees -b html docs {toxinidir}/docs/_build/html} -[testenv:coveralls] -passenv = TRAVIS TRAVIS_* -deps = coveralls -commands = coveralls +[testenv:codecov] +passenv = TOXENV CI TRAVIS TRAVIS_* CODECOV_* +deps = codecov>=1.4.0 +commands = codecov -e TOXENV From f056067fdb916456cfc536520e8c404e9e9b585e Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Thu, 16 Jan 2020 23:38:56 -0800 Subject: [PATCH 446/459] missed this small coveralls leftover --- docs/intro.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/intro.rst b/docs/intro.rst index b6a85c72..bbd164e4 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -10,10 +10,6 @@ :alt: Travis Continuous Integration :target: https://travis-ci.org/jquast/blessed/ -.. |coveralls| image:: https://coveralls.io/repos/github/jquast/blessed/badge.svg?branch=master - :alt: Coveralls Code Coverage - :target: https://coveralls.io/github/jquast/blessed?branch=master - .. |codecov| image:: https://codecov.io/gh/jquast/blessed/branch/master/graph/badge.svg :alt: codecov.io Code Coverage :target: https://codecov.io/gh/jquast/blessed From df6304ddcc627d046be14b0ca9aa9dc674e52a95 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Thu, 16 Jan 2020 23:47:22 -0800 Subject: [PATCH 447/459] oh --- docs/intro.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/intro.rst b/docs/intro.rst index bbd164e4..2e3cb6f1 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -1,4 +1,4 @@ -| |docs| |travis| |coveralls| +| |docs| |travis| |codecov| | |pypi| |downloads| |gitter| | |linux| |windows| |mac| |bsd| From 690f9ca257eb9d13841f97dc5feb1c390c2ca9af Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 17 Jan 2020 00:16:16 -0800 Subject: [PATCH 448/459] Remove _intr_continue in inkey(), also --- blessed/terminal.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/blessed/terminal.py b/blessed/terminal.py index 0d739746..41034f45 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -1150,7 +1150,7 @@ def keypad(self): self.stream.write(self.rmkx) self.stream.flush() - def inkey(self, timeout=None, esc_delay=0.35, **_kwargs): + def inkey(self, timeout=None, esc_delay=0.35): """ Read and return the next keyboard event within given timeout. @@ -1173,14 +1173,6 @@ def inkey(self, timeout=None, esc_delay=0.35, **_kwargs): :meth:`raw`, :obj:`sys.__stdin__` remains line-buffered, and this function will block until the return key is pressed! """ - if _kwargs.pop('_intr_continue', None) is not None: - warnings.warn('keyword argument _intr_continue deprecated: ' - 'beginning v1.9.6, behavior is as though such ' - 'value is always True.') - if _kwargs: - raise TypeError('inkey() got unexpected keyword arguments {!r}' - .format(_kwargs)) - resolve = functools.partial(resolve_sequence, mapper=self._keymap, codes=self._keycodes) From 36db940f9b0844487a572edbab0f0603f9219ada Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 17 Jan 2020 00:24:41 -0800 Subject: [PATCH 449/459] coerce windows away from importing pty --- blessed/tests/test_full_keyboard.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/blessed/tests/test_full_keyboard.py b/blessed/tests/test_full_keyboard.py index b47fc75c..c805fe66 100644 --- a/blessed/tests/test_full_keyboard.py +++ b/blessed/tests/test_full_keyboard.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # std imports import os -import pty import sys import math import time @@ -31,6 +30,7 @@ reason="TEST_QUICK specified") def test_kbhit_interrupted(): """kbhit() should not be interrupted with a signal handler.""" + import pty pid, master_fd = pty.fork() if pid == 0: cov = init_subproc_coverage('test_kbhit_interrupted') @@ -73,6 +73,7 @@ def on_resize(sig, action): reason="TEST_QUICK specified") def test_kbhit_interrupted_nonetype(): """kbhit() should also allow interruption with timeout of None.""" + import pty pid, master_fd = pty.fork() if pid == 0: cov = init_subproc_coverage('test_kbhit_interrupted_nonetype') @@ -182,6 +183,7 @@ def child(): def test_keystroke_0s_cbreak_with_input(): """0-second keystroke with input; Keypress should be immediately returned.""" + import pty pid, master_fd = pty.fork() if pid == 0: cov = init_subproc_coverage('test_keystroke_0s_cbreak_with_input') @@ -212,6 +214,7 @@ def test_keystroke_0s_cbreak_with_input(): def test_keystroke_cbreak_with_input_slowly(): """0-second keystroke with input; Keypress should be immediately returned.""" + import pty pid, master_fd = pty.fork() if pid == 0: cov = init_subproc_coverage('test_keystroke_cbreak_with_input_slowly') @@ -252,6 +255,7 @@ def test_keystroke_cbreak_with_input_slowly(): def test_keystroke_0s_cbreak_multibyte_utf8(): """0-second keystroke with multibyte utf-8 input; should decode immediately.""" # utf-8 bytes represent "latin capital letter upsilon". + import pty pid, master_fd = pty.fork() if pid == 0: # child cov = init_subproc_coverage('test_keystroke_0s_cbreak_multibyte_utf8') @@ -282,6 +286,7 @@ def test_keystroke_0s_cbreak_multibyte_utf8(): reason="travis-ci does not handle ^C very well.") def test_keystroke_0s_raw_input_ctrl_c(): """0-second keystroke with raw allows receiving ^C.""" + import pty pid, master_fd = pty.fork() if pid == 0: # child cov = init_subproc_coverage('test_keystroke_0s_raw_input_ctrl_c') @@ -312,6 +317,7 @@ def test_keystroke_0s_raw_input_ctrl_c(): def test_keystroke_0s_cbreak_sequence(): """0-second keystroke with multibyte sequence; should decode immediately.""" + import pty pid, master_fd = pty.fork() if pid == 0: # child cov = init_subproc_coverage('test_keystroke_0s_cbreak_sequence') @@ -341,6 +347,7 @@ def test_keystroke_0s_cbreak_sequence(): reason="TEST_QUICK specified") def test_keystroke_1s_cbreak_with_input(): """1-second keystroke w/multibyte sequence; should return after ~1 second.""" + import pty pid, master_fd = pty.fork() if pid == 0: # child cov = init_subproc_coverage('test_keystroke_1s_cbreak_with_input') @@ -372,6 +379,7 @@ def test_keystroke_1s_cbreak_with_input(): reason="TEST_QUICK specified") def test_esc_delay_cbreak_035(): """esc_delay will cause a single ESC (\\x1b) to delay for 0.35.""" + import pty pid, master_fd = pty.fork() if pid == 0: # child cov = init_subproc_coverage('test_esc_delay_cbreak_035') @@ -406,6 +414,7 @@ def test_esc_delay_cbreak_035(): reason="TEST_QUICK specified") def test_esc_delay_cbreak_135(): """esc_delay=1.35 will cause a single ESC (\\x1b) to delay for 1.35.""" + import pty pid, master_fd = pty.fork() if pid == 0: # child cov = init_subproc_coverage('test_esc_delay_cbreak_135') @@ -438,6 +447,7 @@ def test_esc_delay_cbreak_135(): def test_esc_delay_cbreak_timout_0(): """esc_delay still in effect with timeout of 0 ("nonblocking").""" + import pty pid, master_fd = pty.fork() if pid == 0: # child cov = init_subproc_coverage('test_esc_delay_cbreak_timout_0') @@ -470,6 +480,7 @@ def test_esc_delay_cbreak_timout_0(): def test_esc_delay_cbreak_nonprefix_sequence(): """ESC a (\\x1ba) will return an ESC immediately.""" + import pty pid, master_fd = pty.fork() if pid == 0: # child cov = init_subproc_coverage('test_esc_delay_cbreak_nonprefix_sequence') @@ -504,6 +515,7 @@ def test_esc_delay_cbreak_nonprefix_sequence(): def test_esc_delay_cbreak_prefix_sequence(): """An unfinished multibyte sequence (\\x1b[) will delay an ESC by .35.""" + import pty pid, master_fd = pty.fork() if pid == 0: # child cov = init_subproc_coverage('test_esc_delay_cbreak_prefix_sequence') From b54acaa82c61836ed5579c693d7da40c023fe26b Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 17 Jan 2020 00:27:59 -0800 Subject: [PATCH 450/459] help readthedocs.org process our tox.ini req's --- docs/requirements.txt | 4 ++++ tox.ini | 5 +---- 2 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 docs/requirements.txt diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 00000000..1eaa2ed3 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,4 @@ +Sphinx==2.3.1 +sphinx_rtd_theme +sphinx-paramlinks +jinxed>=0.5.4 diff --git a/tox.ini b/tox.ini index 6b88f3be..fb726058 100644 --- a/tox.ini +++ b/tox.ini @@ -175,10 +175,7 @@ commands = {envbindir}/pydocstyle --source --explain \ {envbindir}/doc8 --ignore-path docs/_build --ignore D000 docs [testenv:sphinx] -deps = Sphinx==2.3.1 - sphinx_rtd_theme - sphinx-paramlinks - jinxed>=0.5.4 +deps = -r docs/requirements.txt commands = {envbindir}/sphinx-build \ {posargs:-v -W -d {toxinidir}/docs/_build/doctrees -b html docs {toxinidir}/docs/_build/html} From b43e744316dac6afcf60adc5e717b61991473618 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 17 Jan 2020 00:52:49 -0800 Subject: [PATCH 451/459] Move tests blessed/tests -> tests (#125) packaging tests within blessed comes from a problem with coverage that is no longer an issue, and we can relax linters on our tests, like pydocstyle --- MANIFEST.in | 2 +- setup.py | 2 +- {blessed/tests => tests}/__init__.py | 0 {blessed/tests => tests}/accessories.py | 3 +-- {blessed/tests => tests}/test_core.py | 0 {blessed/tests => tests}/test_formatters.py | 0 {blessed/tests => tests}/test_full_keyboard.py | 0 {blessed/tests => tests}/test_keyboard.py | 0 {blessed/tests => tests}/test_length_sequence.py | 5 ++--- {blessed/tests => tests}/test_sequences.py | 0 {blessed/tests => tests}/test_wrap.py | 0 {blessed/tests => tests}/wall.ans | 0 tox.ini | 11 ++++++----- 13 files changed, 11 insertions(+), 12 deletions(-) rename {blessed/tests => tests}/__init__.py (100%) rename {blessed/tests => tests}/accessories.py (99%) rename {blessed/tests => tests}/test_core.py (100%) rename {blessed/tests => tests}/test_formatters.py (100%) rename {blessed/tests => tests}/test_full_keyboard.py (100%) rename {blessed/tests => tests}/test_keyboard.py (100%) rename {blessed/tests => tests}/test_length_sequence.py (99%) rename {blessed/tests => tests}/test_sequences.py (100%) rename {blessed/tests => tests}/test_wrap.py (100%) rename {blessed/tests => tests}/wall.ans (100%) diff --git a/MANIFEST.in b/MANIFEST.in index 5a2ca9ef..1ce192c3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,4 +3,4 @@ include LICENSE include version.json include *.txt include tox.ini -include blessed/tests/wall.ans +recursive-include tests *.py *.ans diff --git a/setup.py b/setup.py index 86bcd83d..afa0b687 100755 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ def _get_long_description(fname): author='Jeff Quast, Erik Rose', author_email='contact@jeffquast.com', license='MIT', - packages=['blessed', 'blessed.tests'], + packages=['blessed', ], url='https://github.com/jquast/blessed', include_package_data=True, zip_safe=True, diff --git a/blessed/tests/__init__.py b/tests/__init__.py similarity index 100% rename from blessed/tests/__init__.py rename to tests/__init__.py diff --git a/blessed/tests/accessories.py b/tests/accessories.py similarity index 99% rename from blessed/tests/accessories.py rename to tests/accessories.py index c4180b51..9ea6a009 100644 --- a/blessed/tests/accessories.py +++ b/tests/accessories.py @@ -68,8 +68,7 @@ def init_subproc_coverage(run_note): return None _coveragerc = os.path.join( os.path.dirname(__file__), - os.pardir, os.pardir, - 'tox.ini') + os.pardir, 'tox.ini') cov = coverage.Coverage(config_file=_coveragerc) cov.set_option("run:note", run_note) cov.start() diff --git a/blessed/tests/test_core.py b/tests/test_core.py similarity index 100% rename from blessed/tests/test_core.py rename to tests/test_core.py diff --git a/blessed/tests/test_formatters.py b/tests/test_formatters.py similarity index 100% rename from blessed/tests/test_formatters.py rename to tests/test_formatters.py diff --git a/blessed/tests/test_full_keyboard.py b/tests/test_full_keyboard.py similarity index 100% rename from blessed/tests/test_full_keyboard.py rename to tests/test_full_keyboard.py diff --git a/blessed/tests/test_keyboard.py b/tests/test_keyboard.py similarity index 100% rename from blessed/tests/test_keyboard.py rename to tests/test_keyboard.py diff --git a/blessed/tests/test_length_sequence.py b/tests/test_length_sequence.py similarity index 99% rename from blessed/tests/test_length_sequence.py rename to tests/test_length_sequence.py index daa46f95..cfe89983 100644 --- a/blessed/tests/test_length_sequence.py +++ b/tests/test_length_sequence.py @@ -10,9 +10,8 @@ import six import pytest -from blessed.tests.accessories import ( # isort:skip - TestTerminal, as_subprocess, all_terms, many_lines, many_columns -) +from .accessories import ( # isort:skip + TestTerminal, as_subprocess, all_terms, many_lines, many_columns) if platform.system() != 'Windows': import fcntl diff --git a/blessed/tests/test_sequences.py b/tests/test_sequences.py similarity index 100% rename from blessed/tests/test_sequences.py rename to tests/test_sequences.py diff --git a/blessed/tests/test_wrap.py b/tests/test_wrap.py similarity index 100% rename from blessed/tests/test_wrap.py rename to tests/test_wrap.py diff --git a/blessed/tests/wall.ans b/tests/wall.ans similarity index 100% rename from blessed/tests/wall.ans rename to tests/wall.ans diff --git a/tox.ini b/tox.ini index fb726058..157500d2 100644 --- a/tox.ini +++ b/tox.ini @@ -6,6 +6,7 @@ envlist = about pylint flake8 flake8_tests + pydocstyle sphinx py{26,27,34,35,36,37,38} skip_missing_interpreters = true @@ -21,7 +22,7 @@ commands = {envbindir}/py.test --cov-config={toxinidir}/tox.ini {posargs:\ --junit-xml=.tox/results.{envname}.xml \ --durations=3 \ } \ - blessed/tests + tests [pytest] looponfailroots = blessed @@ -40,7 +41,7 @@ source = blessed parallel = True [coverage:report] -omit = blessed/tests/* +omit = tests/* exclude_lines = pragma: no cover precision = 1 @@ -109,7 +110,7 @@ commands = {envbindir}/py.test --cov-config={toxinidir}/tox.ini \ --junit-xml=.tox/results.{envname}.xml \ --durations=3 \ } \ - blessed/tests + tests [testenv:develop] commands = {posargs} @@ -158,11 +159,11 @@ commands = {envbindir}/pylint --rcfile={toxinidir}/.pylintrc \ [testenv:flake8] deps = {[flake8]deps} -commands = {envbindir}/flake8 --exclude=blessed/tests,docs/sphinxext/github.py setup.py docs/ blessed/ bin/ +commands = {envbindir}/flake8 --exclude=tests,docs/sphinxext/github.py setup.py docs/ blessed/ bin/ [testenv:flake8_tests] deps = {[flake8]deps} -commands = {envbindir}/flake8 --ignore=W504,F811,F401 blessed/tests/ +commands = {envbindir}/flake8 --ignore=W504,F811,F401 tests/ [testenv:pydocstyle] deps = pydocstyle==3.0.0 From 08f4a9fbb75cb6d88273626c2046d7f704039e11 Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Fri, 17 Jan 2020 11:23:22 -0500 Subject: [PATCH 452/459] Convert ReST refs to http refs for Readme --- docs/intro.rst | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/docs/intro.rst b/docs/intro.rst index 2e3cb6f1..e1275137 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -183,17 +183,17 @@ Forked Changes since 1.7 have all been proposed but unaccepted upstream. Enhancements only in *Blessed*: - * 24-bit color support with :meth:`~Terminal.color_rgb` and :meth:`~Terminal.on_color_rgb` methods + * 24-bit color support with `Terminal.color_rgb()`_ and `Terminal.on_color_rgb()`_ methods * X11 color name attributes * Windows support - * :meth:`~.Terminal.length` to determine printable length of text containing sequences - * :meth:`~.Terminal.strip`, :meth:`~.Terminal.rstrip`, :meth:`~.Terminal.rstrip`, - and :meth:`~.Terminal.strip_seqs` for removing sequences from text - * :meth:`Terminal.wrap` for wrapping text containing sequences at a specified width - * :meth:`~.Terminal.center`, :meth:`~.Terminal.rjust`, and :meth:`~.Terminal.ljust` + * `Terminal.length()`_ to determine printable length of text containing sequences + * `Terminal.strip()`_, `Terminal.rstrip()`_, `Terminal.lstrip()`_, + and `Terminal.strip_seqs()`_ for removing sequences from text + * `Terminal.wrap()`_ for wrapping text containing sequences at a specified width + * `Terminal.center()`_, `Terminal.rjust()`_, and `Terminal.ljust()`_ for alignment of text containing sequences - * :meth:`~.cbreak` and :meth:`~.raw` context managers for keyboard input - * :meth:`~.inkey` for keyboard event detection + * `Terminal.cbreak()`_ and `Terminal.raw()`_ context managers for keyboard input + * `Terminal.inkey()`_ for keyboard event detection Furthermore, a project in the node.js language of the `same name `_ is **not** related, or a fork @@ -208,3 +208,17 @@ of each other in any way. .. _PDCurses: http://www.lfd.uci.edu/~gohlke/pythonlibs/#curses .. _`terminfo(5)`: http://invisible-island.net/ncurses/man/terminfo.5.html .. _`stackoverflow`: http://stackoverflow.com/ +.. _`Terminal.color_rgb()`: https://blessed.readthedocs.io/en/stable/api.html#blessed.terminal.Terminal.color_rgb +.. _`Terminal.on_color_rgb()`: https://blessed.readthedocs.io/en/stable/api.html#blessed.terminal.Terminal.on_color_rgb +.. _`Terminal.length()`: https://blessed.readthedocs.io/en/stable/api.html#blessed.terminal.Terminal.length +.. _`Terminal.strip()`: https://blessed.readthedocs.io/en/stable/api.html#blessed.terminal.Terminal.strip +.. _`Terminal.rstrip()`: https://blessed.readthedocs.io/en/stable/api.html#blessed.terminal.Terminal.rstrip +.. _`Terminal.lstrip()`: https://blessed.readthedocs.io/en/stable/api.html#blessed.terminal.Terminal.lstrip +.. _`Terminal.strip_seqs()`: https://blessed.readthedocs.io/en/stable/api.html#blessed.terminal.Terminal.strip_seqs +.. _`Terminal.wrap()`: https://blessed.readthedocs.io/en/stable/api.html#blessed.terminal.Terminal.wrap +.. _`Terminal.center()`: https://blessed.readthedocs.io/en/stable/api.html#blessed.terminal.Terminal.center +.. _`Terminal.rjust()`: https://blessed.readthedocs.io/en/stable/api.html#blessed.terminal.Terminal.rjust +.. _`Terminal.ljust()`: https://blessed.readthedocs.io/en/stable/api.html#blessed.terminal.Terminal.ljust +.. _`Terminal.cbreak()`: https://blessed.readthedocs.io/en/stable/api.html#blessed.terminal.Terminal.cbreak +.. _`Terminal.raw()`: https://blessed.readthedocs.io/en/stable/api.html#blessed.terminal.Terminal.raw +.. _`Terminal.inkey()`: https://blessed.readthedocs.io/en/stable/api.html#blessed.terminal.Terminal.inkey From 19c213e75081f34816f61584d0674140e638ab05 Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Fri, 17 Jan 2020 11:24:18 -0500 Subject: [PATCH 453/459] Set doc8 max-line-length --- tox.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tox.ini b/tox.ini index 157500d2..fb2b6282 100644 --- a/tox.ini +++ b/tox.ini @@ -67,6 +67,9 @@ max-line-length = 100 exclude = .tox,build deps = flake8==3.7.9 +[doc8] +max-line-length = 100 + [testenv:py26] setenv = TEST_QUICK=1 basepython = python2.6 From 96923a6c9be8847be62c43fb0e3db417dd5b1ccd Mon Sep 17 00:00:00 2001 From: Avram Lubkin Date: Fri, 17 Jan 2020 11:28:51 -0500 Subject: [PATCH 454/459] Add documentation link to PyPI --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index afa0b687..16600b5e 100755 --- a/setup.py +++ b/setup.py @@ -41,6 +41,7 @@ def _get_long_description(fname): license='MIT', packages=['blessed', ], url='https://github.com/jquast/blessed', + project_urls={'Documentation': 'https://blessed.readthedocs.io'}, include_package_data=True, zip_safe=True, classifiers=[ From 80990c71b14df54cf99eb4c7698502ab3e583bc9 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 17 Jan 2020 13:43:54 -0800 Subject: [PATCH 455/459] blessed/tests -> tests/ (#127) - move tests, they were previously packaged to workaround early adoption of coverage + tox vs. path matching bugs, those problems are no longer an issue, we don't have to "release" our tests as a package any longer to track its coverage. - Also, `force_styling=True` allows more test coverage for windows. From 56e1edef8011fc807b25f5499d816f0850ddcc27 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 17 Jan 2020 14:55:06 -0800 Subject: [PATCH 456/459] More windows test support, Closes issue #122 (#128) --- tests/test_length_sequence.py | 8 ++------ tests/test_sequences.py | 5 ++--- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/tests/test_length_sequence.py b/tests/test_length_sequence.py index cfe89983..964796ee 100644 --- a/tests/test_length_sequence.py +++ b/tests/test_length_sequence.py @@ -55,13 +55,11 @@ def child(kind): child(kind) -@pytest.mark.skipif(platform.system() == 'Windows', - reason='https://github.com/jquast/blessed/issues/122') def test_sequence_length(all_terms): """Ensure T.length(string containing sequence) is correcterm.""" @as_subprocess def child(kind): - term = TestTerminal(kind=kind) + term = TestTerminal(kind=kind, force_styling=True) # Create a list of ascii characters, to be separated # by word, to be zipped up with a cycling list of # terminal sequences. Then, compare the length of @@ -401,14 +399,12 @@ def child(kind): child(all_terms) -@pytest.mark.skipif(platform.system() == 'Windows', - reason='https://github.com/jquast/blessed/issues/122') def test_termcap_will_move_true(all_terms): """Test parser about sequences that move the cursor.""" @as_subprocess def child(kind): from blessed.sequences import iter_parse - term = TestTerminal(kind=kind) + term = TestTerminal(kind=kind, force_styling=True) assert next(iter_parse(term, term.move(98, 76)))[1].will_move assert next(iter_parse(term, term.move(54)))[1].will_move assert next(iter_parse(term, term.cud1))[1].will_move diff --git a/tests/test_sequences.py b/tests/test_sequences.py index 5811315b..b4af8bc5 100644 --- a/tests/test_sequences.py +++ b/tests/test_sequences.py @@ -47,13 +47,12 @@ def child(): child() -@pytest.mark.skipif(platform.system() == 'Windows', - reason='https://github.com/jquast/blessed/issues/122') def test_parametrization(): """Test parameterizing a capability.""" @as_subprocess def child(): - assert TestTerminal().cup(3, 4) == unicode_parm('cup', 3, 4) + term = TestTerminal(force_styling=True) + assert term.cup(3, 4) == unicode_parm('cup', 3, 4) child() From 8e070545ee047b845a1d6967ec97c39f61248671 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 17 Jan 2020 15:06:17 -0800 Subject: [PATCH 457/459] bugfix: ValueError: underlying buffer has been detached (#129) Prevent error condition, ValueError: underlying buffer has been detached from raising, Closes #126 --- blessed/terminal.py | 4 +++- docs/history.rst | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/blessed/terminal.py b/blessed/terminal.py index 41034f45..27ba3e88 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -175,7 +175,9 @@ def __init__(self, kind=None, stream=None, force_styling=False): try: stream_fd = (stream.fileno() if hasattr(stream, 'fileno') and callable(stream.fileno) else None) - except io.UnsupportedOperation: + except (io.UnsupportedOperation, ValueError): + # The stream is not a file, such as the case of StringIO, or, when it has been + # "detached", such as might be the case of stdout in some test scenarios. stream_fd = None self._stream = stream diff --git a/docs/history.rst b/docs/history.rst index c1ea6a19..1162348e 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -4,6 +4,8 @@ Version History * introduced: 24-bit color support, detected by ``term.number_of_colors == 1 << 24``, and 24-bit color foreground method :meth:`~Terminal.color_rgb` and background method :meth:`~Terminal.on_color_rgb`. + * bugfix: prevent error condition, ``ValueError: underlying buffer has been detached`` in rare + conditions where sys.__stdout__ has been detached in test frameworks. :ghissue:`126`. * bugfix: off-by-one error in :meth:`~.Terminal.get_location`, now accounts for ``%i`` in cursor_report, :ghissue:`94`. * bugfix :meth:`~Terminal.split_seqs` and related functions failed to match when the From a5600eea2cbdd1641f59563434845ba2e5352597 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 17 Jan 2020 15:49:33 -0800 Subject: [PATCH 458/459] setting inverse structures --- bin/colorchart.py | 1 - bin/editor.py | 2 +- bin/keymatrix.py | 10 +++--- bin/plasma.py | 4 +-- bin/progress_bar.py | 10 +++--- bin/worms.py | 18 +++++----- bin/x11_colorpicker.py | 2 +- blessed/terminal.py | 24 +++++++++++++ docs/history.rst | 56 ++++++++++++++--------------- docs/overview.rst | 67 +++++++++++++---------------------- tests/test_core.py | 12 +++---- tests/test_length_sequence.py | 6 ++++ 12 files changed, 110 insertions(+), 102 deletions(-) diff --git a/bin/colorchart.py b/bin/colorchart.py index be7aa2ab..57d247b6 100644 --- a/bin/colorchart.py +++ b/bin/colorchart.py @@ -32,7 +32,6 @@ def sort_colors(): def draw_chart(term): """Draw a chart of each X11 color represented as in 24-bit and as down-converted to 256, 16, and 8 color with the currently configured algorithm.""" - term.move(0, 0) sys.stdout.write(term.home) width = term.width line = '' diff --git a/bin/editor.py b/bin/editor.py index 1fe873d6..2963fce5 100755 --- a/bin/editor.py +++ b/bin/editor.py @@ -63,7 +63,7 @@ def input_filter(keystroke): def echo_yx(cursor, text): """Move to ``cursor`` and display ``text``.""" - echo(cursor.term.move(cursor.y, cursor.x) + text) + echo(cursor.term.move_yx(cursor.y, cursor.x) + text) Cursor = collections.namedtuple('Cursor', ('y', 'x', 'term')) diff --git a/bin/keymatrix.py b/bin/keymatrix.py index 4b3d2629..18753f3e 100755 --- a/bin/keymatrix.py +++ b/bin/keymatrix.py @@ -39,13 +39,13 @@ def refresh(term, board, level, score, inps): bottom = 0 for keycode, attr in board.items(): echo(u''.join(( - term.move(attr['row'], attr['column']), + term.move_yx(attr['row'], attr['column']), term.color(level_color), (term.reverse if attr['hit'] else term.bold), keycode, term.normal))) bottom = max(bottom, attr['row']) - echo(term.move(term.height, 0) + 'level: %s score: %s' % (level, score,)) + echo(term.move_yx(term.height, 0) + 'level: %s score: %s' % (level, score,)) if bottom >= (term.height - 5): sys.stderr.write( ('\n' * (term.height // 2)) + @@ -54,10 +54,10 @@ def refresh(term, board, level, score, inps): term.center("(use a larger screen)") + ('\n' * (term.height // 2))) sys.exit(1) - echo(term.move(bottom + 1, 0)) + echo(term.move_yx(bottom + 1, 0)) echo('Press ^C to exit.') for row, inp in enumerate(inps[(term.height - (bottom + 3)) * -1:], 1): - echo(term.move(bottom + row + 1, 0)) + echo(term.move_yx(bottom + row + 1, 0)) echo('{0!r}, {1}, {2}'.format( inp.__str__() if inp.is_sequence else inp, inp.code, @@ -128,7 +128,7 @@ def main(): inps.append(inp) with term.cbreak(): - echo(term.move(term.height)) + echo(term.move_yx(term.height)) echo( u'{term.clear_eol}Your final score was {score} ' u'at level {level}{term.clear_eol}\n' diff --git a/bin/plasma.py b/bin/plasma.py index acf1b668..5adf0b51 100755 --- a/bin/plasma.py +++ b/bin/plasma.py @@ -51,14 +51,14 @@ def elapser(): def show_please_wait(term): txt_wait = 'please wait ...' - outp = term.move(term.height - 1, 0) + term.clear_eol + term.center(txt_wait) + outp = term.move_yx(term.height - 1, 0) + term.clear_eol + term.center(txt_wait) print(outp, end='') sys.stdout.flush() def show_paused(term): txt_paused = 'paused' - outp = term.move(term.height - 1, int(term.width / 2 - len(txt_paused) / 2)) + outp = term.move_yx(term.height - 1, int(term.width / 2 - len(txt_paused) / 2)) outp += txt_paused print(outp, end='') sys.stdout.flush() diff --git a/bin/progress_bar.py b/bin/progress_bar.py index 1139ac10..e8b0a8d8 100755 --- a/bin/progress_bar.py +++ b/bin/progress_bar.py @@ -2,10 +2,10 @@ """ Example application for the 'blessed' Terminal library for python. -This isn't a real progress bar, just a sample "animated prompt" of sorts that demonstrates the -separate move_x() and move_y() functions, made mainly to test the `hpa' compatibility for 'screen' -terminal type which fails to provide one, but blessed recognizes that it actually does, and provides -a proxy. +This isn't a real progress bar, just a sample "animated prompt" of sorts that +demonstrates the separate move_x() and move_yx() capabilities, made mainly to +test the `hpa' compatibility for 'screen' terminal type which fails to provide +one, but blessed recognizes that it actually does, and provides a proxy. """ from __future__ import print_function @@ -26,7 +26,7 @@ def main(): with term.cbreak(): inp = None print("press 'X' to stop.") - sys.stderr.write(term.move(term.height, 0) + u'[') + sys.stderr.write(term.move_yx(term.height, 0) + u'[') sys.stderr.write(term.move_x(term.width - 1) + u']' + term.move_x(1)) while inp != 'X': if col >= (term.width - 2): diff --git a/bin/worms.py b/bin/worms.py index 853059e0..233a29ac 100755 --- a/bin/worms.py +++ b/bin/worms.py @@ -191,7 +191,7 @@ def main(): color_worm = term.yellow_reverse color_head = term.red_reverse color_bg = term.on_blue - echo(term.move(1, 1)) + echo(term.move_yx(1, 1)) echo(color_bg(term.clear)) # speed is actually a measure of time; the shorter, the faster. @@ -199,13 +199,13 @@ def main(): modifier = 0.93 inp = None - echo(term.move(term.height, 0)) + echo(term.move_yx(term.height, 0)) with term.hidden_cursor(), term.cbreak(), term.location(): while inp not in (u'q', u'Q'): # delete the tail of the worm at worm_length if len(worm) > worm_length: - echo(term.move(*worm.pop(0))) + echo(term.move_yx(*worm.pop(0))) echo(color_bg(u' ')) # compute head location @@ -229,22 +229,22 @@ def main(): # with a worm color for those portions that overlay. for (yloc, xloc) in nibble_locations(*nibble): echo(u''.join(( - term.move(yloc, xloc), + term.move_yx(yloc, xloc), (color_worm if (yloc, xloc) == head else color_bg)(u' '), term.normal))) # and draw the new, - echo(term.move(*n_nibble.location) + ( + echo(term.move_yx(*n_nibble.location) + ( color_nibble('{}'.format(n_nibble.value)))) # display new worm head - echo(term.move(*head) + color_head(head_glyph(direction))) + echo(term.move_yx(*head) + color_head(head_glyph(direction))) # and its old head (now, a body piece) if worm: - echo(term.move(*(worm[-1]))) + echo(term.move_yx(*(worm[-1]))) echo(color_worm(u' ')) - echo(term.move(*head)) + echo(term.move_yx(*head)) # wait for keyboard input, which may indicate # a new direction (up/down/left/right) @@ -271,7 +271,7 @@ def main(): echo(term.normal) score = (worm_length - 1) * 100 - echo(u''.join((term.move(term.height - 1, 1), term.normal))) + echo(u''.join((term.move_yx(term.height - 1, 1), term.normal))) echo(u''.join((u'\r\n', u'score: {}'.format(score), u'\r\n'))) diff --git a/bin/x11_colorpicker.py b/bin/x11_colorpicker.py index acb45c9a..ccb24bf5 100644 --- a/bin/x11_colorpicker.py +++ b/bin/x11_colorpicker.py @@ -35,7 +35,7 @@ def render(term, idx): f'{term.number_of_colors} colors - ' f'{term.color_distance_algorithm}') - result += term.move(idx // term.width, idx % term.width) + result += term.move_yx(idx // term.width, idx % term.width) result += term.on_color_rgb(*rgb_color)(' \b') return result diff --git a/blessed/terminal.py b/blessed/terminal.py index 27ba3e88..7e47e8aa 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -667,6 +667,30 @@ def color(self): return ParameterizingString(self._foreground_color, self.normal, 'color') + def move_xy(self, x, y): + """ + A callable string that moves the cursor to the given ``(x, y)`` screen coordinates. + + :arg int x: horizontal position. + :arg int y: vertical position. + :rtype: ParameterizingString + """ + # this is just a convenience alias to the built-in, but hidden 'move' + # attribute -- we encourage folks to use only (x, y) positional + # arguments, or, if they must use (y, x), then use the 'move_yx' + # alias. + return self.move(y, x) + + def move_yx(self, y, x): + """ + A callable string that moves the cursor to the given ``(y, x)`` screen coordinates. + + :arg int x: horizontal position. + :arg int y: vertical position. + :rtype: ParameterizingString + """ + return self.move(y, x) + def color_rgb(self, red, green, blue): if self.number_of_colors == 1 << 24: # "truecolor" 24-bit diff --git a/docs/history.rst b/docs/history.rst index 1162348e..9a5aa750 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -1,38 +1,34 @@ Version History =============== 1.17 - * introduced: 24-bit color support, detected by ``term.number_of_colors == 1 << 24``, - and 24-bit color foreground method :meth:`~Terminal.color_rgb` and background method - :meth:`~Terminal.on_color_rgb`. + * introduced: 24-bit color support, detected by ``term.number_of_colors == 1 << 24``, and 24-bit + color foreground method :meth:`~Terminal.color_rgb` and background method + :meth:`~Terminal.on_color_rgb`, as well as 676 common X11 color attribute names are now + possible, such as ``term.aquamarine_on_wheat``. * bugfix: prevent error condition, ``ValueError: underlying buffer has been detached`` in rare conditions where sys.__stdout__ has been detached in test frameworks. :ghissue:`126`. - * bugfix: off-by-one error in :meth:`~.Terminal.get_location`, now accounts for - ``%i`` in cursor_report, :ghissue:`94`. - * bugfix :meth:`~Terminal.split_seqs` and related functions failed to match when the - color index was greater than 15, :ghissue:`101`. - * bugfix: Context Managers, :meth:`~.Terminal.fullscreen`, - :meth:`~.Terminal.hidden_cursor`, and :meth:`~Terminal.keypad` - now flush the stream after writing their sequences. - * bugfix: ``chr(127)``, ``\x7f`` has changed from keycode ``term.DELETE`` to the more - common match, ``term.BACKSPACE``, :ghissue:115` by :ghuser:`jwezel`. - * deprecated: the direct curses ``move()`` capability is no longer recommended, - suggest to use :meth:`~.Terminal.move_xy()`, which matches the return value of - :meth:`~.Terminal.get_location`. - * deprecated: ``superscript``, ``subscript``, ``shadow``, and ``dim`` are no - longer "compoundable" with colors, such as in phrase ``Terminal.blue_subscript('a')``. - These attributes are not typically supported, anyway. Use Unicode text or 256 or - 24-bit color codes instead. - * deprecated: additional key names, such as ``KEY_TAB``, are no longer "injected" into - the curses module namespace. - * deprecated: :func:`curses.setupterm` is now called with :attr:`os.devnull` - as the file descriptor, let us know if this causes any issues. :ghissue:`59`. - * deprecated: :meth:`~Terminal.inkey` no longer raises RuntimeError when - :attr:`~Terminal.stream` is not a terminal, programs using - :meth:`~Terminal.inkey` to block indefinitely if a keyboard is not - attached. :ghissue:`69`. - * deprecated: using argument ``_intr_continue`` to method - :meth:`~Terminal.kbhit`, behavior is as though such value is always True - since 1.9. + * bugfix: off-by-one error in :meth:`~.Terminal.get_location`, now accounts for ``%i`` in + cursor_report, :ghissue:`94`. + * bugfix :meth:`~Terminal.split_seqs` and related functions failed to match when the color index + was greater than 15, :ghissue:`101`. + * bugfix: Context Managers, :meth:`~.Terminal.fullscreen`, :meth:`~.Terminal.hidden_cursor`, and + :meth:`~Terminal.keypad` now flush the stream after writing their sequences. + * bugfix: ``chr(127)``, ``\x7f`` has changed from keycode ``term.DELETE`` to the more common + match, ``term.BACKSPACE``, :ghissue:115` by :ghuser:`jwezel`. + * deprecated: the direct curses ``move()`` capability is no longer recommended, suggest to use + :meth:`~.Terminal.move_xy()`, which matches the return value of :meth:`~.Terminal.get_location`. + * deprecated: ``superscript``, ``subscript``, ``shadow``, and ``dim`` are no longer "compoundable" + with colors, such as in phrase ``Terminal.blue_subscript('a')``. These attributes are not + typically supported, anyway. Use Unicode text or 256 or 24-bit color codes instead. + * deprecated: additional key names, such as ``KEY_TAB``, are no longer "injected" into the curses + module namespace. + * deprecated: :func:`curses.setupterm` is now called with :attr:`os.devnull` as the file + descriptor, let us know if this causes any issues. :ghissue:`59`. + * deprecated: :meth:`~Terminal.inkey` no longer raises RuntimeError when :attr:`~Terminal.stream` + is not a terminal, programs using :meth:`~Terminal.inkey` to block indefinitely if a keyboard is + not attached. :ghissue:`69`. + * deprecated: using argument ``_intr_continue`` to method :meth:`~Terminal.kbhit`, behavior is as + though such value is always True since 1.9. 1.16 * introduced: Windows support?! :ghissue:`110` by :ghuser:`avylove`. diff --git a/docs/overview.rst b/docs/overview.rst index d4224b95..4e85fb90 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -184,39 +184,22 @@ demonstration script. Moving The Cursor ----------------- -When you want to move the cursor, you have a few choices: - -- ``location(x=None, y=None)`` context manager. -- ``move(row, col)`` capability. -- ``move_y(row)`` capability. -- ``move_x(col)`` capability. - -.. warning:: The :meth:`~.Terminal.location` method receives arguments in - positional order *(x, y)*, whereas the ``move()`` capability receives - arguments in order *(y, x)*. Please use keyword arguments as a later - release may correct the argument order of :meth:`~.Terminal.location`. - -Finding The Cursor ------------------- - -We can determine the cursor's current position at anytime using -:meth:`~.get_location`, returning the current (y, x) location. This uses a -kind of "answer back" sequence that your terminal emulator responds to. If -the terminal may not respond, the :paramref:`~.get_location.timeout` keyword -argument can be specified to return coordinates (-1, -1) after a blocking -timeout:: +If you just want to move and aren't worried about returning, do something like +this:: from blessed import Terminal term = Terminal() + print(term.move_xy(10, 1) + 'Hi, mom!') - row, col = term.get_location(timeout=5) +There are three basic movement capabilities: - if row < term.height: - print(term.move_y(term.height) + 'Get down there!') - -Moving Temporarily -~~~~~~~~~~~~~~~~~~ +``move_xy(x, y)`` + Position cursor at given **x**, **y**. +``move_x(x)`` + Position cursor at column **x**. +``move_y(y)`` + Position cursor at row **y**. A context manager, :meth:`~.Terminal.location` is provided to move the cursor to an *(x, y)* screen position and restore the previous position upon exit:: @@ -236,32 +219,32 @@ keyword arguments:: with term.location(y=10): print('We changed just the row.') -When omitted, it saves the cursor position and restore it upon exit:: +When omitted, it saves the current cursor position, and restore it upon exit:: with term.location(): - print(term.move(1, 1) + 'Hi') - print(term.move(9, 9) + 'Mom') + print(term.move_xy(1, 1) + 'Hi') + print(term.move_xy(9, 9) + 'Mom') .. note:: calls to :meth:`~.Terminal.location` may not be nested. +Finding The Cursor +------------------ -Moving Permanently -~~~~~~~~~~~~~~~~~~ - -If you just want to move and aren't worried about returning, do something like -this:: +We can determine the cursor's current position at anytime using +:meth:`~.get_location`, returning the current (y, x) location. This uses a +kind of "answer back" sequence that your terminal emulator responds to. If +the terminal may not respond, the :paramref:`~.get_location.timeout` keyword +argument can be specified to return coordinates (-1, -1) after a blocking +timeout:: from blessed import Terminal term = Terminal() - print(term.move(10, 1) + 'Hi, mom!') -``move(y, x)`` - Position cursor at given **y**, **x**. -``move_x(x)`` - Position cursor at column **x**. -``move_y(y)`` - Position cursor at row **y**. + row, col = term.get_location(timeout=5) + + if row < term.height: + print(term.move_y(term.height) + 'Get down there!') One-Notch Movement ~~~~~~~~~~~~~~~~~~ diff --git a/tests/test_core.py b/tests/test_core.py index 9c5a75f5..a939aea7 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -41,17 +41,17 @@ def child(kind): child(all_terms) -def test_flipped_location_move(all_terms): - """``location()`` and ``move()`` receive counter-example arguments.""" +def test_location_to_move_xy(all_terms): + """``location()`` and ``move_xy()`` receive complimentary arguments.""" @as_subprocess def child(kind): buf = six.StringIO() t = TestTerminal(stream=buf, force_styling=True) - y, x = 10, 20 + x, y = 12, 34 with t.location(y, x): - xy_val = t.move(x, y) - yx_val = buf.getvalue()[len(t.sc):] - assert xy_val == yx_val + xy_val_from_move_xy = t.move_xy(y, x) + xy_val_from_location = buf.getvalue()[len(t.sc):] + assert xy_val_from_move_xy == xy_val_from_location child(all_terms) diff --git a/tests/test_length_sequence.py b/tests/test_length_sequence.py index 964796ee..33bea28c 100644 --- a/tests/test_length_sequence.py +++ b/tests/test_length_sequence.py @@ -377,6 +377,10 @@ def child(kind): measure_length(term.move(98, 76), term)) assert (len(term.move(54)) == measure_length(term.move(54), term)) + assert (len(term.move_xy(1, 2)) == + measure_length(term.move(1, 2), term)) + assert (len(term.move_yx(3, 4)) == + measure_length(term.move(3, 4), term)) assert not term.cud1 or (len(term.cud1) == measure_length(term.cud1, term)) assert not term.cub1 or (len(term.cub1) == @@ -406,6 +410,8 @@ def child(kind): from blessed.sequences import iter_parse term = TestTerminal(kind=kind, force_styling=True) assert next(iter_parse(term, term.move(98, 76)))[1].will_move + assert next(iter_parse(term, term.move_yx(8, 76)))[1].will_move + assert next(iter_parse(term, term.move_xy(98, 7)))[1].will_move assert next(iter_parse(term, term.move(54)))[1].will_move assert next(iter_parse(term, term.cud1))[1].will_move assert next(iter_parse(term, term.cub1))[1].will_move From 19b4b0b513c7c7d2009fa1d268f68b38721c74cd Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Fri, 17 Jan 2020 15:51:05 -0800 Subject: [PATCH 459/459] flattening module stackframes --- tests/accessories.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/accessories.py b/tests/accessories.py index 9ea6a009..73a6eedd 100644 --- a/tests/accessories.py +++ b/tests/accessories.py @@ -70,7 +70,6 @@ def init_subproc_coverage(run_note): os.path.dirname(__file__), os.pardir, 'tox.ini') cov = coverage.Coverage(config_file=_coveragerc) - cov.set_option("run:note", run_note) cov.start() return cov