diff options
author | Jeff Quast <contact@jeffquast.com> | 2015-04-14 19:52:19 -0700 |
---|---|---|
committer | Jeff Quast <contact@jeffquast.com> | 2015-04-14 19:52:19 -0700 |
commit | 9aebb6b3521fadf1f618c879945da726327e2252 (patch) | |
tree | 21120bab65e3aebe6d704f93ceb0062c613f069e | |
parent | 22d74eb1bc0004c5f079eb43850c45bec3b58c7c (diff) | |
parent | f9543adf65eca78bf8fd402a8ac2532c084894e8 (diff) | |
download | blessings-9aebb6b3521fadf1f618c879945da726327e2252.tar.gz |
Merge pull request #100 from erikrose/blessed-integration-pep257-docstrings
Blessed integration pep257 docstrings
-rw-r--r-- | .prospector.yaml | 37 | ||||
-rw-r--r-- | .travis.yml | 2 | ||||
-rw-r--r-- | blessings/__init__.py | 10 | ||||
-rw-r--r-- | blessings/_binterms.py | 21 | ||||
-rw-r--r-- | blessings/formatters.py | 261 | ||||
-rw-r--r-- | blessings/keyboard.py | 259 | ||||
-rw-r--r-- | blessings/sequences.py | 448 | ||||
-rw-r--r-- | blessings/terminal.py | 796 | ||||
-rw-r--r-- | blessings/tests/accessories.py | 13 | ||||
-rw-r--r-- | blessings/tests/test_core.py | 46 | ||||
-rw-r--r-- | blessings/tests/test_keyboard.py | 12 | ||||
-rw-r--r-- | blessings/tests/test_length_sequence.py | 12 | ||||
-rw-r--r-- | blessings/tests/test_sequences.py | 49 | ||||
-rw-r--r-- | docs/api.rst | 32 | ||||
-rw-r--r-- | docs/conf.py | 48 | ||||
-rw-r--r-- | docs/further.rst | 21 | ||||
-rw-r--r-- | docs/history.rst | 28 | ||||
-rw-r--r-- | docs/index.rst | 2 | ||||
-rw-r--r-- | docs/intro.rst | 24 | ||||
-rw-r--r-- | docs/overview.rst | 379 | ||||
-rw-r--r-- | fabfile.py | 39 | ||||
-rw-r--r-- | setup.cfg | 2 | ||||
-rwxr-xr-x | setup.py | 131 | ||||
-rwxr-xr-x | tools/teamcity-runtests.sh | 2 | ||||
-rw-r--r-- | tox.ini | 35 |
25 files changed, 1654 insertions, 1055 deletions
diff --git a/.prospector.yaml b/.prospector.yaml index 5d1f7d2..c32f978 100644 --- a/.prospector.yaml +++ b/.prospector.yaml @@ -1,12 +1,16 @@ inherits: - strictness_veryhigh -ignore: +ignore-patterns: - (^|/)\..+ - ^docs/ - # ignore tests and bin/ for the moment, their quality does not so much matter. + - ^build/ + # ignore these, their quality does not so much matter. - ^bin/ - ^blessings/tests/ + - ^tools/ + # not maintained + - ^fabfile.py test-warnings: true @@ -24,6 +28,9 @@ frosted: mccabe: # complexity checking. run: true + disable: + # Terminal.__init__ is too complex (14) + - MC0001 pep257: # docstring checking @@ -46,22 +53,26 @@ pylint: # 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 + # Access to a protected member _sugar of a client class + - protected-access + # blessings.Terminal: Too many instance attributes (12/7) + - too-many-instance-attributes + # blessings.Terminal: Too many public methods (25/20) + - too-many-public-methods + # blessings.Terminal: Too many branches (13/12) + - too-many-branches + # blessings.sequences.get_wontmove_sequence_patterns: + # Used builtin function 'map' + - bad-builtin + pyroma: # checks setup.py run: true vulture: - # this tool does a good job of finding unused code. - run: true + # this tool does a good job of finding unused code, which isn't terribly + # useful from an API perspective, disable. + run: false # vim: noai:ts=4:sw=4 diff --git a/.travis.yml b/.travis.yml index e3c76c2..99e0fcc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,6 +26,8 @@ install: fi script: + - if [[ "${TOXENV}" == "py27" ]]; then tox -e sa; fi + - if [[ "${TOXENV}" == "py27" ]]; then tox -e docs; fi - tox -e $TOXENV after_success: diff --git a/blessings/__init__.py b/blessings/__init__.py index f5a65eb..813e2df 100644 --- a/blessings/__init__.py +++ b/blessings/__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/blessings """ +# std imports import platform as _platform + +# local +from blessings.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('Blessings 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/blessings/_binterms.py b/blessings/_binterms.py index 9621be5..1f6cc80 100644 --- a/blessings/_binterms.py +++ b/blessings/_binterms.py @@ -1,12 +1,13 @@ -""" Exports a list of binary terminals blessings is not able to cope with. """ -#: This list of terminals is manually managed, it describes all of the terminals -#: that blessings 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 blessings 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 @@ zen50 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, blessings " + u"is likely to fail to measure the length of its sequences.") + +__all__ = ['BINARY_TERMINALS', 'BINTERM_UNSUPPORTED_MSG'] diff --git a/blessings/formatters.py b/blessings/formatters.py index 283fb73..8a0f4e4 100644 --- a/blessings/formatters.py +++ b/blessings/formatters.py @@ -1,43 +1,64 @@ -"This sub-module provides formatting functions." +"""This sub-module provides sequence-formatting functions.""" # standard imports import curses -import sys # 3rd-party import six -_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()) +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((_derivitive, _color)) + for _derivitive in derivatives + for _color in colors]) | colors -#: 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 -#: All valid compoundable names. -COMPOUNDABLES = (COLORS | _compoundables) +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 + + +#: 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): - """A Unicode string which can be called as a parameterizing termcap. + + 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 = six.text_type.__new__(cls, args[0]) @@ -46,11 +67,14 @@ class ParameterizingString(six.text_type): 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 @@ -80,32 +104,41 @@ class ParameterizingString(six.text_type): class ParameterizingProxyString(six.text_type): - """A Unicode string which can be called to proxy missing termcap entries. - - For example:: - >>> from blessings 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' + r""" + A Unicode string which can be called to proxy missing termcap entries. + + 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 blessings 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 = six.text_type.__new__(cls, args[0][0]) new._fmt_args = args[0][1] @@ -114,25 +147,31 @@ class ParameterizingProxyString(six.text_type): return new def __call__(self, *args): - """P(*args) -> FormattingString() + """ + Returning :class:`FormattingString` instance for given parameters. - 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. + 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. + + :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',) @@ -160,18 +199,29 @@ def get_proxy_string(term, attr): class FormattingString(six.text_type): - """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') - 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 = six.text_type.__new__(cls, args[0]) @@ -179,35 +229,37 @@ class FormattingString(six.text_type): 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(six.text_type): - """A dummy callable Unicode to stand in for ``FormattingString`` and - ``ParameterizingString`` for terminals that cannot perform styling. + """ + 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): + """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): @@ -235,27 +287,37 @@ class NullCallableString(six.text_type): 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'. @@ -266,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) @@ -285,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) @@ -294,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/blessings/keyboard.py b/blessings/keyboard.py index bf6a4a5..8037163 100644 --- a/blessings/keyboard.py +++ b/blessings/keyboard.py @@ -1,67 +1,44 @@ -"This sub-module provides 'keyboard awareness'." +"""This sub-module provides 'keyboard awareness'.""" -__all__ = ['Keystroke', 'get_keyboard_codes', 'get_keyboard_sequences'] - -# standard imports +# std imports import curses.has_key -import collections import curses -# 3rd-party +# 3rd party import six -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_')) -) +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 -# 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(six.text_type): + """ + A unicode-derived class for describing a single keystroke. -class Keystroke(six.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 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): + """Class constructor.""" new = six.text_type.__new__(cls, ucs) new._name = name new._code = code @@ -69,31 +46,54 @@ class Keystroke(six.text_type): @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 six.text_type.__repr__(self) or self._name + """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`` @@ -102,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) @@ -112,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 blessings.Terminal term: :class:`~.Terminal` instance. + :rtype: dict + + This function supports :func:`get_terminal_sequences` to discover + the preferred input sequence for the left and right application keys. - 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). 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' ': @@ -130,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 blessings.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 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. + (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 @@ -170,35 +192,75 @@ 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. @@ -268,3 +330,22 @@ DEFAULT_SEQUENCE_MIXIN = ( (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/blessings/sequences.py b/blessings/sequences.py index 8667693..00241de 100644 --- a/blessings/sequences.py +++ b/blessings/sequences.py @@ -1,9 +1,7 @@ # encoding: utf-8 -" This sub-module provides 'sequence awareness' for blessings." +"""This module provides 'sequence awareness'.""" -__all__ = ['init_sequence_patterns', 'Sequence', 'SequenceTextWrapper'] - -# built-ins +# std imports import functools import textwrap import warnings @@ -11,29 +9,55 @@ import math import re # local -from ._binterms import binary_terminals as _BINTERM_UNSUPPORTED +from blessings._binterms import BINARY_TERMINALS, BINTERM_UNSUPPORTED_MSG -# 3rd-party +# 3rd party import wcwidth import six -_BINTERM_UNSUPPORTED_MSG = ( - u"Terminal kind {0!r} contains binary-packed capabilities, blessings " - u"is likely to fail to measure the length of its sequences.") +__all__ = ['init_sequence_patterns', 'Sequence', 'SequenceTextWrapper'] + + +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. + return sorted(list(filter(None, regex_seqlist)), 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+). + r""" + 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 + ``<base_num>-1``, through ``<base_num>+1`` inclusive is replaced + by regular expression pattern ``\d``. Any other digits found are + *not* replaced. + + :param blessings.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 '' @@ -51,13 +75,23 @@ 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""" + Return regular expression for capabilities containing any numerics. + + :param blessings.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)) @@ -65,8 +99,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 blessings.Terminal term: :class:`~.Terminal` instance. + :rtype: list """ bnc = functools.partial(_build_numeric_capability, term) @@ -106,8 +143,11 @@ 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 blessings.Terminal term: :class:`~.Terminal` instance. + :rtype: list """ bnc = functools.partial(_build_numeric_capability, term) bna = functools.partial(_build_any_numeric_capability, term) @@ -226,62 +266,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 blessings.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_will_move`` - any sequence matching this pattern will cause the terminal - cursor to move (such as *term.home*). + - ``_re_wont_move`` - ``_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), - or None if temrinal is without cuf sequence. + - ``_re_cuf`` - ``_cuf1`` - *term.cuf1* sequence (cursor forward 1 character) as a static value. + Regular expression that matches term.cuf(N) (move N characters + forward), or None if temrinal is without cuf sequence. - ``_re_cub`` - regular expression that matches term.cub(N) (move N characters backward), - or None if terminal is without cub sequence. + - ``_cuf1`` - ``_cub1`` - *term.cuf1* sequence (cursor backward 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), 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.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'), ] @@ -315,16 +370,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): @@ -362,12 +428,13 @@ class SequenceTextWrapper(textwrap.TextWrapper): 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 @@ -417,153 +484,187 @@ SequenceTextWrapper.__doc__ = textwrap.TextWrapper.__doc__ 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 blessings.Terminal term: :class:`~.Terminal` instance. """ 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 blessings import Terminal - >>> from blessings.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 blessings import Terminal + >>> from blessings.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): + """ + Return string of sequences, leading, 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().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 + r""" + Return string of all sequences removed. - 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. - - All other sequences are simply removed. An example, >>> from blessings import Terminal >>> from blessings.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 blessings import Terminal + >>> from blessings.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'' @@ -583,21 +684,31 @@ class Sequence(six.text_type): def measure_length(ucs, term): - """measure_length(S, term) -> int + 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 blessings.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. - 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. + 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' @@ -613,7 +724,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! @@ -621,20 +732,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 blessings.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[<n>C``, ``\033[<n>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): @@ -650,16 +775,23 @@ 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 ``<n>`` in SGR sequence of form ``<ESC>[<n>C`` - (T.move_right(n)), or ``-(n)`` in sequence of form ``<ESC>[<n>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. ``<ESC>[<n>C``): returns value ``(n)``. + - move left (fe. ``<ESC>[<n>D``): returns value ``-(n)``. + - backspace (``\b``) returns value -1. + - tab (``\t``) returns value 8. + :param blessings.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 diff --git a/blessings/terminal.py b/blessings/terminal.py index 29f4835..94ecef9 100644 --- a/blessings/terminal.py +++ b/blessings/terminal.py @@ -1,5 +1,6 @@ -"This primary module provides the Terminal class." -# standard imports +# encoding: utf-8 +"""This module contains :class:`Terminal`, the primary API interface.""" +# std imports import collections import contextlib import functools @@ -13,31 +14,25 @@ import struct import time import sys import os +import io try: import termios import fcntl import tty 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 _ except NameError: @@ -66,16 +61,8 @@ from .keyboard import ( 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. - """ + """Wrapper for curses and related terminfo(5) terminal capabilities.""" #: Sugary names for commonly-used capabilities _sugar = dict( @@ -114,46 +101,48 @@ 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``. - """ + Class initializer. + + :param str kind: A terminal string as taken by + :func:`curses.setupterm`. Defaults to the value of the ``TERM`` + Environment variable. + + .. note:: A terminal of only one ``kind`` may be initialized for + each process. 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 + # Using the global statement (col 8) + 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 + # Default stream is stdout, keyboard only valid as stdin 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() 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) @@ -162,16 +151,15 @@ class Terminal(object): # _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) + 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: @@ -185,7 +173,8 @@ class Terminal(object): 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: @@ -207,14 +196,14 @@ class Terminal(object): 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() @@ -225,29 +214,33 @@ class Terminal(object): self._keyboard_decoder = codecs.getincrementaldecoder( self._encoding)() except LookupError as err: - warnings.warn('%s, fallback to ASCII for keyboard.' % (err,)) + warnings.warn('LookupError: %s, fallback to ASCII for ' + 'keyboard.' % (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``. - - This capability is also callable, so you can use ``term.bold("hi")`` - which results in the joining of (term.bold, "hi", 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!")``. - 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. + >>> term.bold_blink_red_on_green("merry x-mas!"). + u'\x1b[1m\x1b[5m\x1b[31m\x1b[42mmerry x-mas!\x1b[m' + + For a parametrized capability such as ``move`` (cup), pass the + parameters as positional arguments ``term.move(line, column)``. See + manual page of terminfo(5) for a complete list of capabilities and + their arguments. """ if not self.does_styling: return NullCallableString() @@ -258,60 +251,82 @@ class Terminal(object): @property def kind(self): - """Name of this terminal type as string.""" + """Name of this terminal type.""" return self._kind @property def does_styling(self): - """Whether this instance will emit terminal sequences (bool).""" + """Whether this instance will emit terminal sequences.""" return self._does_styling @property def is_a_tty(self): - """Whether the ``stream`` associated with this instance is a terminal - (bool).""" + """Whether :attr:`~.stream` is a terminal.""" return self._is_a_tty @property def height(self): - """T.height -> int - - The height of the terminal in characters. - """ + """The height of the terminal (by number of character cells).""" return self._height_and_width().ws_row @property def width(self): - """T.width -> int - - The width of the terminal in characters. - """ + """The width of the terminal (by number of character cells).""" return self._height_and_width().ws_col @staticmethod - def _winsize(fd): - """T._winsize -> WINSZ(ws_row, ws_col, ws_xpixel, ws_ypixel) + def _winsize(fdesc): + """ + Return named tuple describing size of the terminal by ``fdesc``. + + If the given platform does not have modules :mod:`termios`, + :mod:`fcntl`, or :mod:`tty`, window size of 80 columns by 24 + 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 fdesc: file descriptor queries for its window size. + :raises IOError: the file descriptor ``fdesc`` 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) + data = fcntl.ioctl(fdesc, 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__): + 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 24 rows is returned. + + :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 fdesc 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) + if fdesc is not None: + return self._winsize(fdesc) except IOError: pass @@ -322,24 +337,32 @@ class Terminal(object): @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. + + :param int x: Move to a specific column (optional). + :param int y: Move to a specific row (optional). + :rtype: None 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('Hello, world!') + print('previous 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 - restoration of cursor position will happen. This can be useful if you - simply want to restore your place after doing some manual cursor - movement. + This context manager yields no value, its side-effect is to write + the "save cursor position (sc)" sequence upon entering to + :attr:`stream` and "restore cursor position (rc)" upon entering. + .. note:: Store and restore cursor provides no stack: This means that + :meth:`location` calls cannot be chained: 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 +379,23 @@ class Terminal(object): @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 alternate screen. + + :rtype: None - 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 context manager yields no value, its side-effect is to save + the primary screen buffer on entering, and to restore it again + upon exit. The secondary screen buffer entered while using the + context manager also remains, and is faithfully restored again + on the next entrance:: - This call may not be tested; only one screen state may be saved at a - time. + with term.fullscreen(), term.hidden_cursor(): + main() + + .. note:: There is only one primary and secondary screen: This means + that :meth:`fullscreen` calls cannot be chained: only one should + be entered at a time. """ self.stream.write(self.enter_fullscreen) try: @@ -375,8 +405,21 @@ class Terminal(object): @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. + + :rtype: None + + This context manager yields no value, its side-effect is to emit + the ``hide_cursor`` sequence to :attr:`stream` on entering, and + to emit ``normal_cursor`` sequence upon exit:: + + with term.fullscreen(), term.hidden_cursor(): + main() + + .. note:: :meth:`hidden_cursor` calls cannot be chained: only one + should be entered at a time. + """ self.stream.write(self.hide_cursor) try: yield @@ -385,15 +428,16 @@ class Terminal(object): @property def color(self): - """Returns capability that sets the foreground color. + """ + Callable string that sets the foreground color. + + :arg int num: The foreground color index. This should be within the + bounds of :attr:`~.number_of_colors`. 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 +446,12 @@ class Terminal(object): @property def on_color(self): - "Returns capability that sets the background color." + """ + 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 +459,213 @@ class Terminal(object): @property def normal(self): - "Returns sequence that resets video attribute." + """ + 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 set to default. + """ if self._normal: return self._normal self._normal = resolve_capability(self, 'normal') return self._normal @property + def stream(self): + """ + The output stream connected to the terminal. + + This is a convenience attribute. It is used 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. + """ + 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:: + this may be used to test whether the terminal supports colors:: if term.number_of_colors: - ...""" + ... + """ # 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 - - 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.""" + """ + Return string ``text`` containing sequences, left-adjusted. + + :param str text: String of text to be right-adjusted, may contain + terminal sequences. + :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 + """ 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.""" + """ + Return string ``text`` containing sequences, right-adjusted. + + :param str text: String of text to be right-adjusted, may contain + terminal sequences. + :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 + """ 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.""" + """ + Return string ``text`` containing sequences, centered. + + :param str text: String of text to be centered, may contain terminal + sequences. + :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 + """ 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 string ``text`` containing sequences. + + :param str text: String of text to determine printable length, may + contain terminal sequences. + :rtype: int + :returns: printable length of string as terminal character cells. + + Strings containing text that consumes 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 - - Return string ``text`` with terminal sequences removed, and leading - and trailing whitespace removed. + r""" + Return ``text`` without sequences and leading or trailing whitespace. + + :param str text: String of text that may contain terminal + sequences. + :returns: Text stripped of sequences and leading or trailing + whitespace. + :rtype: str + + >>> term = blessings.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`` stripped of terminal sequences and trailing whitespace. - Return string ``text`` with terminal sequences and trailing whitespace - removed. + :param str text: String of text that may contain terminal sequences. + :returns: Text stripped of sequences and trailing whitespace. + :rtype: str + + >>> term = blessings.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`` stripped of terminal sequences and leading whitespace. + + :param str text: String of text that may contain terminal sequences. + :returns: Text stripped of sequences and leading whitespace. + :rtype: str - Return string ``text`` with terminal sequences and leading whitespace - removed. + >>> term = blessings.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 only of its terminal sequences. - Return string ``text`` stripped only of its sequences. + :param str text: String of text that may contain terminal sequences. + :returns: Text stripped of sequences. + :rtype: str + + >>> term = blessings.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``. + """ + Wrap a string of ``text``, returning an array of wrapped lines. + + :param 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`. + :param int width: Unlike :func:`textwrap.wrap`, ``width`` will + default to the width of the attached terminal. + :rtype: list + :returns: list of strings that may contain escape sequences. + + See :class:`textwrap.TextWrapper` class for available keyword arguments + to customize wrapping behaviour. """ width = self.width if width is None else width lines = [] @@ -535,29 +678,49 @@ class Terminal(object): return lines def _next_char(self): - """T._next_char() -> unicode + """ + Read and decode next byte from keyboard stream. - 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._char_is_ready() returns True. + :rtype: unicode + :returns: a single unicode character, or ``u''`` if a multi-byte + sequence has not yet been fully received. - Implementors of input streams other than os.read() on the stdin fd - should derive and override this method. + This method supports :meth:`keystroke`, reading only one byte from + the keyboard string at a time. This method should always return + without blocking if called when :meth:`_char_is_ready` returns + True. + + 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) def _char_is_ready(self, timeout=None, interruptable=True): - """T._char_is_ready([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. + """ + Whether a keypress has been detected on the keyboard. + + This method is used by method :meth:`keystroke` to determine if + a byte may be read using method :meth:`_next_char` without blocking. + + :param 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). + :param bool interruptable: 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 False for ``interruptable``. + + This is an open issue for review to **remove** this parameter, + https://github.com/erikrose/blessings/issues/96 + :rtype: bool + :returns: True if a keypress is awaiting to be read on the keyboard + attached to this terminal. 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) -- @@ -566,13 +729,12 @@ class Terminal(object): # 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, ] + 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 interruptable: return u'' @@ -591,28 +753,36 @@ class Terminal(object): @contextlib.contextmanager def keystroke_input(self, raw=False): - """Return a context manager that sets up the terminal to do - key-at-a-time input. - - On entering the context manager, "cbreak" mode is activated, disabling - line buffering of keyboard input and turning off automatic echoing of - input. (You must explicitly print any input if you'd like it shown.) - Also referred to as 'rare' mode, this is the opposite of 'cooked' mode, - the default for most shells. - - If ``raw`` is True, enter "raw" mode instead. Raw mode differs in that - the interrupt, quit, suspend, and flow control characters are all - passed through as their raw character values instead of generating a - signal. - - 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 + """ + Context manager that enables key-at-a-time input. + + Normally, characters received from the keyboard cannot be read by + python until the return key is pressed: this is referred to as + "cooked" or "canonical input" mode, allowing the tty driver to perform + line editing before being read by your program and is usually the + default mode set by your unix shell before executing any programs. + + Also referred to as 'rare' mode, entering this context is the opposite + of 'cooked' mode: On entering, :func:`tty.setcbreak` mode is activated, + disabling line buffering of keyboard input and turning off automatic + echoing of input. This allows each keystroke to be received + immediately after it is pressed. + + :param bool raw: When True, enter :func:`tty.setraw` mode instead. + Raw mode differs in that the interrupt, quit, suspend, and flow + control characters are all passed through as their raw character + values instead of generating a signal. + + This context manager yields no value, its side-effect is to + set the :mod:`termios` attributes of the terminal attached to + :obj:`sys.__stdin__`. + + .. note:: you must explicitly print any input received if you'd like + it displayed. And, if providing any kind of editing, you must + also handle backspace and other line editing control characters. + + .. 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: @@ -631,17 +801,19 @@ class Terminal(object): @contextlib.contextmanager def keypad(self): - """ - Context manager that enables keypad input (*keyboard_transmit* mode). + r""" + 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. + This context manager yields no value, its side-effect is to emit + capability keypad_xmit (smkx) upon entering, and 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``. + 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, + However, upon entering :meth:`keypad`, ``\\x1b[OF`` is transmitted, translating to ``KEY_LL`` (lower-left key), allowing diagonal direction keys to be determined. """ @@ -652,39 +824,37 @@ class Terminal(object): self.stream.write(self.rmkx) def keystroke(self, timeout=None, esc_delay=0.35, interruptable=True): - """T.keystroke(timeout=None, [esc_delay, [interruptable]]) -> Keystroke - - 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 ``False`` for - ``interruptable``. - """ - # 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') ? - + """ + Receive and return next keystroke from keyboard within given timeout. + + :param float timeout: Number of seconds to allow to elapse without + keystroke before returning. When None (default), this + method blocks indefinitely. + :param float esc_delay: To distinguish between ``KEY_ESCAPE`` and + sequences beginning with escape, the parameter ``esc_delay`` + specifies the amount of time after receiving the escape character + (``chr(27)``) to seek for the completion of an application key + before returning a :class:`~.Keystroke` for ``KEY_ESCAPE``. + :param bool interruptable: 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 False for ``interruptable``. + + This is an open issue for review to **remove** this parameter, + https://github.com/erikrose/blessings/issues/96 + :rtype: :class:`~.Keystroke`. + :raises NoKeyboard: The :attr:`stream` is not a terminal with + timeout parameter as the default value of None, which would + cause the program to hang indefinitely. + :returns: :class:`~.Keystroke`, which may be empty (``u''``) if + ``timeout`` is specified and keystroke is not received. + + .. note:: When used without the context manager + :meth:`keystroke_input`, :obj:`sys.__stdin__` remains + line-buffered, and this function will block until the return key + is pressed. + """ if timeout is None and self._keyboard_fd is None: raise NoKeyboard( 'Waiting for a keystroke on a terminal with no keyboard ' @@ -692,10 +862,18 @@ class Terminal(object): 'timeout and revise your program logic.') def time_left(stime, timeout): - """time_left(stime, timeout) -> float + """ + Return time remaining since ``stime`` before given ``timeout``. - Returns time-relative time remaining before ``timeout`` - after time elapsed since ``stime``. + This function assists determining the value of ``timeout`` for + class method :meth:`_char_is_ready`. + + :param float stime: starting time for measurement + :param 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: @@ -718,66 +896,82 @@ class Terminal(object): ucs += self._next_char() # decode keystroke, if any - ks = resolve(text=ucs) + keystroke = 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._char_is_ready(time_left(stime, timeout), - interruptable): + while (not keystroke and + self._char_is_ready(time_left(stime, timeout), interruptable)): ucs += self._next_char() - ks = resolve(text=ucs) + keystroke = 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 == self.KEY_ESCAPE: + if keystroke.code == self.KEY_ESCAPE: esctime = time.time() - while (ks.code == self.KEY_ESCAPE and + while (keystroke.code == self.KEY_ESCAPE and self._char_is_ready(time_left(esctime, esc_delay))): ucs += self._next_char() - ks = resolve(text=ucs) + keystroke = resolve(text=ucs) # buffer any remaining text received - self._keyboard_buf.extendleft(ucs[len(ks):]) - return ks + self._keyboard_buf.extendleft(ucs[len(keystroke):]) + return keystroke class NoKeyboard(Exception): - """Exception raised when a Terminal that has no means of input connected is - asked to retrieve a keystroke without an infinite timeout.""" - - -# 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 + """Illegal operation requiring a keyboard without one attached.""" + -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) +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 + + 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/blessings/tests/accessories.py b/blessings/tests/accessories.py index 1d3dd45..cb4c05c 100644 --- a/blessings/tests/accessories.py +++ b/blessings/tests/accessories.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """Accessories for automated py.test runner.""" # standard imports -from __future__ import with_statement +from __future__ import with_statement, print_function import contextlib import subprocess import functools @@ -27,7 +27,7 @@ RECV_SEMAPHORE = b'SEMAPHORE\r\n' all_xterms_params = ['xterm', 'xterm-256color'] many_lines_params = [30, 100] many_columns_params = [1, 10] -from blessings._binterms import binary_terminals +from blessings._binterms import BINARY_TERMINALS default_all_terms = ['screen', 'vt220', 'rxvt', 'cons25', 'linux', 'ansi'] if os.environ.get('TEST_ALLTERMS'): try: @@ -42,7 +42,7 @@ if os.environ.get('TEST_ALLTERMS'): 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 @@ -58,6 +58,7 @@ class as_subprocess(object): self.func = func def __call__(self, *args, **kwargs): + pid_testrunner = os.getpid() pid, master_fd = pty.fork() if pid == self._CHILD_PID: # child process executes function, raises exception @@ -93,6 +94,10 @@ class as_subprocess(object): cov.save() os._exit(0) + 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: @@ -206,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/blessings/tests/test_core.py b/blessings/tests/test_core.py index bc7f7f1..e69884e 100644 --- a/blessings/tests/test_core.py +++ b/blessings/tests/test_core.py @@ -2,10 +2,6 @@ "Core blessings 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 locale import sys import imp import os +import io # local from .accessories import ( @@ -25,6 +22,7 @@ from .accessories import ( # 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 test_number_of_colors_without_tty(): # '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)) @@ -300,20 +298,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 blessings.terminal - import io - if hasattr(io, 'UnsupportedOperation'): - monkeypatch.delattr('io.UnsupportedOperation') - - imp.reload(blessings.terminal) - assert blessings.terminal.IOUnsupportedOperation.__doc__.startswith( - "A dummy exception to take the place of") - monkeypatch.undo() - imp.reload(blessings.terminal) - - def test_without_dunder(): "Ensure dunder does not remain in module (py2x InterruptedError test." import blessings.terminal @@ -327,7 +311,7 @@ def test_IOUnsupportedOperation(): import blessings.terminal def side_effect(): - raise blessings.terminal.IOUnsupportedOperation + raise io.UnsupportedOperation mock_stream = mock.Mock() mock_stream.fileno = side_effect @@ -361,7 +345,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 +360,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,7 +431,7 @@ def test_win32_missing_tty_modules(monkeypatch): imp.reload(blessings.terminal) except UserWarning: err = sys.exc_info()[1] - assert err.args[0] == blessings.terminal.msg_nosupport + assert err.args[0] == blessings.terminal._MSG_NOSUPPORT warnings.filterwarnings("ignore", category=UserWarning) import blessings.terminal diff --git a/blessings/tests/test_keyboard.py b/blessings/tests/test_keyboard.py index 46e53c2..ff4d518 100644 --- a/blessings/tests/test_keyboard.py +++ b/blessings/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 @@ from .accessories import ( # 3rd-party import pytest import mock +import six if sys.version_info[0] == 3: unichr = chr @@ -261,7 +257,7 @@ def test_char_is_ready_no_kb(): "_char_is_ready() 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 not term._char_is_ready(timeout=1.1) @@ -286,7 +282,7 @@ def test_keystroke_0s_keystroke_input_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.keystroke_input(): stime = time.time() inp = term.keystroke(timeout=0) @@ -312,7 +308,7 @@ def test_keystroke_1s_keystroke_input_noinput_nokb(): "1-second keystroke without input or keyboard." @as_subprocess def child(): - term = TestTerminal(stream=StringIO()) + term = TestTerminal(stream=six.StringIO()) with term.keystroke_input(): stime = time.time() inp = term.keystroke(timeout=1) diff --git a/blessings/tests/test_length_sequence.py b/blessings/tests/test_length_sequence.py index 0eac165..4f19b3f 100644 --- a/blessings/tests/test_length_sequence.py +++ b/blessings/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 struct import fcntl import sys import os -try: - from StringIO import StringIO -except ImportError: - from io import StringIO -from .accessories import ( +# local +from blessings.tests.accessories import ( all_terms, as_subprocess, TestTerminal, @@ -19,7 +17,9 @@ from .accessories import ( many_lines, ) +# 3rd party import pytest +import six def test_length_cjk(): @@ -204,7 +204,7 @@ def test_env_winsize(): # 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/blessings/tests/test_sequences.py b/blessings/tests/test_sequences.py index 8d1597e..af3619f 100644 --- a/blessings/tests/test_sequences.py +++ b/blessings/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 @@ from .accessories import ( 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,7 +91,7 @@ def test_stream_attr(): 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 blessings.sequences import _BINTERM_UNSUPPORTED_MSG + from blessings._binterms import BINTERM_UNSUPPORTED_MSG @as_subprocess def child(kind): @@ -106,7 +103,7 @@ def test_emit_warnings_about_binpacked(): 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 @@ -128,8 +125,8 @@ def test_emit_warnings_about_binpacked(): def test_unit_binpacked_unittest(): """Unit Test known binary-packed terminals emit a warning (travis-safe).""" import warnings - from blessings.sequences import (_BINTERM_UNSUPPORTED_MSG, - init_sequence_patterns) + from blessings._binterms import BINTERM_UNSUPPORTED_MSG + from blessings.sequences import init_sequence_patterns warnings.filterwarnings("error", category=UserWarning) term = mock.Mock() term.kind = 'tek4207-s' @@ -138,25 +135,25 @@ def test_unit_binpacked_unittest(): 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 blessings.sequences import _merge_sequences + from blessings.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( @@ -173,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') @@ -187,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( @@ -206,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( @@ -224,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 @@ -244,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 @@ -264,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( @@ -280,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( @@ -354,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') @@ -432,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: @@ -484,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 index 0317767..9bca2fa 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,30 +1,46 @@ API Documentation ================= -terminal --------- +terminal.py +----------- .. automodule:: blessings.terminal :members: :undoc-members: + :special-members: __getattr__ +.. autodata:: _CUR_TERM -formatters ----------- +formatters.py +------------- .. automodule:: blessings.formatters :members: :undoc-members: + :private-members: + :special-members: __call__ +.. autodata:: COLORS +.. autodata:: COMPOUNDABLES -keyboard --------- +keyboard.py +----------- .. automodule:: blessings.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 ---------- +sequences.py +------------ .. automodule:: blessings.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 98742f6..9e5ae92 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -2,9 +2,6 @@ import sys import os -# local imports -from blessings import Terminal - # 3rd-party import sphinx_rtd_theme import sphinx.environment @@ -32,12 +29,33 @@ github_project_url = "https://github.com/erikrose/blessings" # # https://github.com/SuperCowPowers/workbench/issues/172 # https://groups.google.com/forum/#!topic/sphinx-users/GNx7PVXoZIU -# http://stackoverflow.com/questions/12772927/specifying-an-online-image-in-sphinx-restructuredtext-format +# 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 'blessings' 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 blessings.terminal import * + # -- General configuration ---------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. @@ -45,8 +63,12 @@ sphinx.environment.BuildEnvironment.warn_node = _warn_node # 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.intersphinx', - 'sphinx.ext.viewcode', 'github', ] +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'] @@ -93,7 +115,7 @@ exclude_patterns = ['_build'] # 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::). @@ -167,16 +189,16 @@ html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # 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 <link> tag referring to it. The value of this option must be the @@ -245,3 +267,7 @@ autodoc_member_order = 'bysource' # when linking to standard python library, use and prefer python 3 # documentation. intersphinx_mapping = {'http://docs.python.org/3/': None} + +# Both the class’ and the __init__ method’s docstring are concatenated and +# inserted. +autoclass_content = "both" diff --git a/docs/further.rst b/docs/further.rst index 4847814..ed13d81 100644 --- a/docs/further.rst +++ b/docs/further.rst @@ -6,7 +6,7 @@ that dive deeper into Terminal I/O programming than :class:`~.Terminal` offers. Here are some recommended readings to help you along: - `terminfo(5) - <http://www.openbsd.org/cgi-bin/man.cgi?query=terminfo&apropos=0&sektion=5>`_ + <http://invisible-island.net/ncurses/man/terminfo.5.html>`_ 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**. @@ -16,8 +16,8 @@ Here are some recommended readings to help you along: of your preferred posix-like operating system. - `The TTY demystified - <http://www.linusakesson.net/programming/tty/index.php>`_ by - Linus Åkesson. + <http://www.linusakesson.net/programming/tty/index.php>`_ + by Linus Åkesson. - `A Brief Introduction to Termios <https://blog.nelhage.com/2009/12/a-brief-introduction-to-termios/>`_ by @@ -53,18 +53,19 @@ Here are some recommended readings to help you along: terminal types) emitted by most terminal capabilities to an action in a series of ``case`` switch statements. - - Many modern libraries are now based `https://github.com/GNOME/vte - <libvte>`_ (or just 'vte'): Gnome Terminal, sakura, Terminator, Lilyterm, - ROXTerm, evilvte, Termit, Termite, Tilda, tinyterm, lxterminal. - - `Thomas E. Dickey <http://invisible-island.net/>` has been maintaining + - Many modern libraries are now based on `libvte + <https://github.com/GNOME/vte>`_ (or just 'vte'): Gnome Terminal, + sakura, Terminator, Lilyterm, ROXTerm, evilvte, Termit, Termite, Tilda, + tinyterm, lxterminal. + - `Thomas E. Dickey <http://invisible-island.net/>`_ has been maintaining `xterm <http://invisible-island.net/xterm/xterm.html>`_, as well as a - primary maintainer of many related packages such as `ncurses + primary maintainer of many related packages such as `ncurses <http://invisible-island.net/ncurses/ncurses.html>`_ for quite a long while. There is often speculation and misinformation among developers of terminal emulators and programs that interact with them. Thomas Dickey's analysis is always thorough and complete. - xterm, urxvt, SyncTerm, and EtherTerm. - - There are far too many to name, Chose one you like! Thomas Dickey + - 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 @@ -76,7 +77,7 @@ Here are some recommended readings to help you along: - `FreeBSD <https://github.com/freebsd/freebsd/blob/master/sys/kern/tty.c>`_ - `OpenBSD <http://cvsweb.openbsd.org/cgi-bin/cvsweb/~checkout~/src/sys/kern/tty.c?content-type=text/plain>`_ - - `Illumos (Solaris) <https://github.com/illumos/illumos-gate/blob/master/usr/src/uts/common/io/tty_common.c>` + - `Illumos (Solaris) <https://github.com/illumos/illumos-gate/blob/master/usr/src/uts/common/io/tty_common.c>`_ - `Minix <https://github.com/minix3/minix/blob/master/minix/drivers/tty/tty/tty.c>`_ - `Linux <https://github.com/torvalds/linux/blob/master/drivers/tty/n_tty.c>`_ diff --git a/docs/history.rst b/docs/history.rst index c7f5417..ce5a321 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -1,8 +1,9 @@ Version History =============== -2.0 - * This release is primarily a set of contributions from +1.9.5 + * This release is primarily a set of contributions from the + `blessed <https://github.com/jquast/blessed>` fork by :ghuser:`jquast` unless otherwise indicated. **new features**: @@ -34,7 +35,7 @@ Version History 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. - - New public attribute: :attr:`~.kind`: the very same as given + - New public attribute: :attr:`~.kind`: the very same as given by the keyword argument of the same (or, determined by and equivalent to the ``TERM`` Environment variable). - Some attributes are now properties and raise exceptions when assigned, @@ -52,9 +53,10 @@ Version History - The '2to3' tool is no longer used for python 3 support - Converted nose tests to pytest via tox. Added a TeamCity build farm to include OSX and FreeBSD testing. ``tox`` is now the primary entry point - with which to execute tests, run static analysis, and build documentation. - - py.test fixtures and ``@as_subprocess`` decorator for testing of many more - terminal types than just 'xterm-256-color' as previously tested. + with which to execute tests, run static analysis, and build + documentation. + - py.test fixtures and ``@as_subprocess`` decorator for testing of many + more terminal types than just 'xterm-256-color' as previously tested. - ``setup.py develop`` ensures a virtualenv and installs tox. - 100% (combined) coverage. @@ -118,10 +120,10 @@ Version History 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. + * 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 @@ -133,9 +135,9 @@ Version History 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 :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`. diff --git a/docs/index.rst b/docs/index.rst index 986f483..37794b3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,9 +10,9 @@ Contents: intro overview - api further pains + api history ======= diff --git a/docs/intro.rst b/docs/intro.rst index c41ed0f..fefb747 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -20,10 +20,13 @@ .. image:: https://img.shields.io/pypi/dm/blessings.svg :alt: Downloads + :target: https://pypi.python.org/pypi/blessings Introduction ============ +Blessings is a thin, practical wrapper around terminal capabilities in Python. + Coding with *Blessings* looks like this... :: from blessings import Terminal @@ -36,8 +39,9 @@ Coding with *Blessings* looks like this... :: with t.location(0, t.height - 1): print(t.center(t.blink('press any key to continue.'))) - with t.cbreak(): - t.inkey() + with t.keystroke_input(): + inp = t.keystroke() + print('You pressed ' + repr(inp)) The Pitch @@ -104,18 +108,19 @@ 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: -* Uses the `terminfo(5)`_ 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 + 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.) + 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. + calls to curses or whatever other terminal libraries you like. +* Safely decodes internationalization keyboard input to their unicod e + 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, @@ -126,7 +131,8 @@ Blessings 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! + resolve this. `Patches welcome + <https://github.com/erikrose/blessings/issues/21>`_! Further Documentation --------------------- @@ -152,4 +158,4 @@ Blessings is under the MIT License. See the LICENSE file. .. _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://www.openbsd.org/cgi-bin/man.cgi?query=terminfo&apropos=0&sektion=5 +.. _`terminfo(5)`: http://invisible-island.net/ncurses/man/terminfo.5.html diff --git a/docs/overview.rst b/docs/overview.rst index 4b78be6..f718aa8 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -1,44 +1,63 @@ Overview ======== -Blessings 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, such as -utf-8 characters. +Blessings 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() -Simple Formatting ------------------ +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: -Lots of handy formatting codes are available as attributes on a *Terminal* class -instance. For example:: + >>> 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 blessings 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*. +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:: +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: @@ -68,7 +87,7 @@ The less commonly supported capabilities: ``no_shadow`` Exit shadow text mode. ``standout`` - Enable standout mode (often, an alias for ``reverse``.). + Enable standout mode (often, an alias for ``reverse``). ``no_standout`` Exit standout mode. ``subscript`` @@ -88,13 +107,13 @@ 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. +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. @@ -110,8 +129,8 @@ Color terminals are capable of at least 8 basic colors. 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 +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 blessings import Terminal @@ -119,50 +138,53 @@ terminals also provide an additional 8 high-intensity versions using 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.:: +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 blessings import Terminal term = Terminal() - for n in range(16): - print(term.color(n)('Color {}'.format(n))) + 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. -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 +~~~~~~~~~~~~~~~~~~~ -Colorless terminals, such as the amber or green monochrome *vt220*, do not +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, such as a selection bar, to simply select -a foreground color, followed by reverse video to achieve the desired -background color effect:: +desirable in some applications to simply select a foreground color, followed +by reverse video to achieve the desired background color effect:: from blessings 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. + print(term.green_reverse('some terminals standout more than others')) -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. +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, unlink *black*, *bright_black* is not -invisible -- it is actually a very dark shade of gray! +.. 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:: @@ -174,28 +196,30 @@ all together:: 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:: +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 blessings 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='+') - from blessings 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:: +Saved as **tprint.py**, this could be used like:: $ ./tprint.py bright_blue_reverse Blue Skies @@ -203,29 +227,34 @@ Saved as **tprint.py**, this could be called simply:: 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. +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. -**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. +.. note:: The :meth:`~.Terminal.location` method receives arguments in form + of *(x, y)*, whereas the ``move()`` capability receives arguments in form + of *(y, x)*. This will be changed to match in the 2.0 release, :ghissue:`58`. 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:: +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 blessings 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*:: +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.') @@ -236,8 +265,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**: calls to ``location()`` may not be nested, as only one location -may be saved. +.. note:: calls to :meth:`~.Terminal.location` may not be nested. Moving Permanently @@ -269,14 +297,17 @@ cursor one character in various directions: * ``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. +.. 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 *height* and *width* properties of the *Terminal* class instance:: +Use the :attr:`~.Terminal.height` and :attr:`~.Terminal.width` properties to +determine the size of the window:: from blessings import Terminal @@ -285,7 +316,8 @@ 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 values are always current. To detect when the size of the window +changes, you may author a callback for SIGWINCH_ signals:: import signal from blessings import Terminal @@ -297,7 +329,8 @@ These are always current, so they may be used with a callback from SIGWINCH_ sig signal.signal(signal.SIGWINCH, on_resize) - term.inkey() + # wait for keypress + term.keystroke() Clearing The Screen @@ -336,7 +369,7 @@ There's also a context manager you can use as a shortcut:: with term.fullscreen(): print(term.move_y(term.height // 2) + term.center('press any key').rstrip()) - term.inkey() + term.keystroke() Pipe Savvy @@ -344,13 +377,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 -any formatting codes gumming up the works. +: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 ``force_styling=True`` to the *Terminal* constructor. +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 *does_styling* attribute on *Terminal* that lets +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:: @@ -361,34 +394,38 @@ bars and other frippery and just stick to content:: if term.does_styling: with term.location(x=0, y=term.height - 1): print('Progress: [=======> ]') - print(term.bold('Important stuff')) + print(term.bold("60%")) Sequence Awareness ------------------ Blessings 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:: +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 blessings import Terminal term = Terminal() - with term.location(y=term.height / 2): - print (term.center(term.bold('X')) + 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. -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:: +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 blessings import Terminal term = Terminal() - poem = (term.bold_blue('Plan difficult tasks'), - term.blue('through the simplest tasks'), + 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')) @@ -399,64 +436,77 @@ from Tao Te Ching, word-wrapped to 25 columns:: Keyboard Input -------------- -The built-in python function ``raw_input`` function does not return a value until +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 that emit multibyte sequences. +keypress, much less arrow or function keys. -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. +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``: blessings handles all of these special cases for you! -Blessings handles all of these special cases!! +keystroke_input +~~~~~~~~~~~~~~~ -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:: +The context manager :meth:`~.Terminal.keystroke_input` can be used to enter +*key-at-a-time* mode: Any keypress by the user is immediately consumed by read +calls:: from blessings import Terminal import sys - t = Terminal() + term = Terminal() - with t.cbreak(): - # blocks until any key is pressed. + with term.keystroke_input(): + # block until any single key is pressed. sys.stdin.read(1) -raw -~~~ +The mode entered using :meth:`~.Terminal.keystroke_input` is called +`cbreak(3)`_ in curses: -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 + 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. -inkey -~~~~~ +:meth:`~.Terminal.keystroke_input` also accepts optional parameter +:paramref:`~.Terminal.keystroke_input.raw` which may be set as *True*. When +used, the given behavior is described in `raw(3)`_ as follows: -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):: + The raw and noraw routines place the terminal into or out of 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. + +keystroke +~~~~~~~~~ + +The method :meth:`~.Terminal.keystroke` combined with `keystroke_input`_ +completes the circle of providing key-at-a-time keyboard input with multibyte +encoding and awareness of application keys. + +:meth:`~.Terminal.keystroke` 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 blessings import Terminal - t = Terminal() + term = Terminal() print("press 'q' to quit.") - with t.cbreak(): - val = None + with term.keystroke_input(): + val = u'' while val not in (u'q', u'Q',): - val = t.inkey(timeout=5) + val = term.keystroke(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))) + print("got sequence: {0}.".format((str(val), val.name, val.code))) elif val: - print("got {}.".format(val)) + print("got {0}.".format(val)) print('bye!') Its output might appear as:: @@ -473,45 +523,51 @@ 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 non-blocking. +A :paramref:`~.Terminal.keystroke.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.keystroke.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: +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 <https://docs.python.org/3/library/curses.html#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: + +=================== ============= ==================== +blessings 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`` @@ -521,14 +577,13 @@ context manager is used: * ``KEY_KP_DIVIDE`` * ``KEY_KP_0`` through ``KEY_KP_9`` -.. _`erikrose/blessings`: https://github.com/erikrose/blessings -.. _curses: https://docs.python.org/library/curses.html .. _couleur: https://pypi.python.org/pypi/couleur .. _wcwidth: https://pypi.python.org/pypi/wcwidth .. _`cbreak(3)`: http://www.openbsd.org/cgi-bin/man.cgi?query=cbreak&apropos=0&sektion=3 +.. _`raw(3)`: http://www.openbsd.org/cgi-bin/man.cgi?query=raw&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 +.. _`terminfo(5)`: http://invisible-island.net/ncurses/man/terminfo.5.html .. _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 diff --git a/fabfile.py b/fabfile.py deleted file mode 100644 index 27b418e..0000000 --- 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/setup.cfg b/setup.cfg new file mode 100644 index 0000000..2a9acf1 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 @@ -1,4 +1,5 @@ #!/usr/bin/env python +"""setuptools entry point for package management.""" import subprocess import sys import os @@ -7,76 +8,90 @@ import setuptools import setuptools.command.develop import setuptools.command.test -here = os.path.dirname(__file__) +HERE = os.path.dirname(__file__) class SetupDevelop(setuptools.command.develop.develop): + + """Docstring is overwritten.""" + def run(self): - # ensure a virtualenv is loaded, + """ + Prepare environment for development. + + - Ensures a virtualenv environmnt is used. + - Ensures tox, ipython, wheel is installed for convenience and testing. + - Call super()'s run method. + """ assert os.getenv('VIRTUAL_ENV'), 'You should be in a virtualenv' - # ensure tox and ipython is installed - subprocess.check_call(('pip', 'install', 'tox', 'ipython')) + subprocess.check_call(('pip', 'install', 'tox', 'ipython', 'wheel')) + # Call super() (except develop is an old-style class, so we must call # directly). The effect is that the development egg-link is installed. setuptools.command.develop.develop.run(self) +SetupDevelop.__doc__ = setuptools.command.develop.develop.__doc__ + class SetupTest(setuptools.command.test.test): + + """Docstring is overwritten.""" + def run(self): + """Spawn tox.""" self.spawn(('tox',)) +SetupTest.__doc__ = setuptools.command.test.test.__doc__ + +EXTRA = { + 'install_requires': [ + 'wcwidth>=0.1.4', + 'six>=1.9.0', + ] +} + +if sys.version_info < (2, 7): + # we make use of collections.ordereddict: for python 2.6 we require the + # assistance of the 'orderddict' module which backports the same. + EXTRA['install_requires'].extend(['ordereddict>=1.1']) -def main(): - extra = { - 'install_requires': [ - 'wcwidth>=0.1.4', - 'six>=1.9.0', - ] - } - if sys.version_info < (2, 7): - # we make use of collections.ordereddict: for python 2.6 we require the - # assistance of the 'orderddict' module which backports the same. - extra['install_requires'].extend(['ordereddict>=1.1']) - - setuptools.setup( - name='blessings', - version='1.9.5', - description=('A thin, practical wrapper around terminal coloring, ' - 'styling, positioning, and keyboard input.'), - long_description=open(os.path.join(here, 'docs/intro.rst')).read(), - author='Erik Rose, Jeff Quast', - author_email='erikrose@grinchcentral.com', - license='MIT', - packages=['blessings', 'blessings.tests'], - url='https://github.com/erikrose/blessings', - include_package_data=True, - test_suite='blessings.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}, - **extra - ) - -if __name__ == '__main__': - main() +setuptools.setup( + name='blessings', + version='1.9.5', + description=('A thin, practical wrapper around terminal coloring, ' + 'styling, positioning, and keyboard input.'), + long_description=open(os.path.join(HERE, 'docs/intro.rst')).read(), + author='Erik Rose, Jeff Quast', + author_email='erikrose@grinchcentral.com', + license='MIT', + packages=['blessings', 'blessings.tests'], + url='https://github.com/erikrose/blessings', + include_package_data=True, + test_suite='blessings.tests', + 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'], + cmdclass={'develop': SetupDevelop, + 'test': SetupTest}, + **EXTRA +) diff --git a/tools/teamcity-runtests.sh b/tools/teamcity-runtests.sh index 3b3faec..c43766a 100755 --- a/tools/teamcity-runtests.sh +++ b/tools/teamcity-runtests.sh @@ -14,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,pypy,docs,sa' fi ret=0 @@ -10,7 +10,7 @@ envlist = sa, skip_missing_interpreters = true [testenv] -whitelist_externals = /bin/bash /bin/mv +whitelist_externals = /bin/mv setenv = PYTHONIOENCODING=UTF8 deps = pytest-xdist pytest-cov @@ -20,23 +20,46 @@ commands = {envbindir}/py.test \ --strict --junit-xml=results.{envname}.xml \ --verbose --verbose \ --cov blessings --cov-report=term-missing \ + blessings/tests \ {posargs} /bin/mv {toxinidir}/.coverage {toxinidir}/.coverage.{envname} [testenv:sa] -# static analysis -deps = prospector[with_everything] -commands = -prospector \ +# 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 = prospector \ --die-on-tool-error \ --doc-warnings \ {toxinidir} + rst-lint README.rst + + doc8 --ignore-path docs/_build --ignore D000 docs + [testenv:docs] +whitelist_externals=echo basepython=python deps=sphinx sphinx_rtd_theme -commands= - sphinx-build -v -W -b html -d {toxinidir}/docs/_build/doctrees docs {toxinidir}/docs/_build/html + sphinx-paramlinks + +commands = 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 |