From 92ba728e00a5d5650026f0e3d53fd010ee0a5872 Mon Sep 17 00:00:00 2001 From: Erik Rose Date: Tue, 3 Feb 2015 00:19:39 -0500 Subject: Change textual occurrences of "blessed" to "blessings". Also divide a few import stanzas into sections conformant with PEP 8. --- bin/editor.py | 4 ++- bin/keymatrix.py | 3 +- bin/on_resize.py | 6 ++-- bin/progress_bar.py | 7 ++-- bin/tprint.py | 4 ++- bin/worms.py | 7 ++-- blessed/__init__.py | 4 +-- blessed/_binterms.py | 4 +-- blessed/formatters.py | 2 +- blessed/keyboard.py | 2 +- blessed/sequences.py | 12 +++---- blessed/tests/accessories.py | 4 +-- blessed/tests/test_core.py | 58 +++++++++++++++--------------- blessed/tests/test_formatters.py | 66 ++++++++++++++++++++--------------- blessed/tests/test_keyboard.py | 20 +++++------ blessed/tests/test_length_sequence.py | 6 ++-- blessed/tests/test_sequences.py | 20 +++++------ docs/Makefile | 8 ++--- docs/conf.py | 14 ++++---- docs/index.rst | 20 +++++------ docs/make.bat | 4 +-- setup.py | 8 ++--- tox.ini | 2 +- 23 files changed, 151 insertions(+), 134 deletions(-) diff --git a/bin/editor.py b/bin/editor.py index 4dff2b7..dc8cde2 100755 --- a/bin/editor.py +++ b/bin/editor.py @@ -9,7 +9,9 @@ from __future__ import division, print_function import collections import functools -from blessed import Terminal + +from blessings import Terminal + echo = lambda text: ( functools.partial(print, end='', flush=True)(text)) diff --git a/bin/keymatrix.py b/bin/keymatrix.py index daaad99..f4e99f6 100755 --- a/bin/keymatrix.py +++ b/bin/keymatrix.py @@ -1,8 +1,9 @@ #!/usr/bin/env python from __future__ import division -from blessed import Terminal import sys +from blessings import Terminal + def main(): """ diff --git a/bin/on_resize.py b/bin/on_resize.py index 2481d05..4140748 100755 --- a/bin/on_resize.py +++ b/bin/on_resize.py @@ -1,6 +1,6 @@ #!/usr/bin/env python """ -This is an example application for the 'blessed' Terminal library for python. +This is an example application for the 'blessings' Terminal library for python. Window size changes are caught by the 'on_resize' function using a traditional signal handler. Meanwhile, blocking keyboard input is displayed to stdout. @@ -8,7 +8,9 @@ If a resize event is discovered, an empty string is returned by term.keystroke() when interruptable is False, as it is here. """ import signal -from blessed import Terminal + +from blessings import Terminal + term = Terminal() diff --git a/bin/progress_bar.py b/bin/progress_bar.py index 4fd4ca0..5257e95 100755 --- a/bin/progress_bar.py +++ b/bin/progress_bar.py @@ -1,17 +1,18 @@ #!/usr/bin/env python """ -This is an example application for the 'blessed' Terminal library for python. +This is an example application for the 'blessings' Terminal library for python. This isn't a real progress bar, just a sample "animated prompt" of sorts that demonstrates the separate move_x() and move_y() functions, made mainly to test the `hpa' compatibility for 'screen' terminal type which -fails to provide one, but blessed recognizes that it actually does, and +fails to provide one, but blessings recognizes that it actually does, and provides a proxy. """ from __future__ import print_function -from blessed import Terminal import sys +from blessings import Terminal + def main(): term = Terminal() diff --git a/bin/tprint.py b/bin/tprint.py index e85f580..c00c486 100755 --- a/bin/tprint.py +++ b/bin/tprint.py @@ -1,7 +1,9 @@ #!/usr/bin/env python import argparse -from blessed import Terminal + +from blessings import Terminal + parser = argparse.ArgumentParser( description='displays argument as specified style') diff --git a/bin/worms.py b/bin/worms.py index 4941679..be46d3d 100755 --- a/bin/worms.py +++ b/bin/worms.py @@ -1,15 +1,16 @@ #!/usr/bin/env python """ -This is an example application for the 'blessed' Terminal library for python. +This is an example application for the 'blessings' Terminal library for python. It is also an experiment in functional programming. """ from __future__ import division, print_function from collections import namedtuple -from random import randrange from functools import partial -from blessed import Terminal +from random import randrange + +from blessings import Terminal # python 2/3 compatibility, provide 'echo' function as an diff --git a/blessed/__init__.py b/blessed/__init__.py index 481468f..3622d0e 100644 --- a/blessed/__init__.py +++ b/blessed/__init__.py @@ -1,13 +1,13 @@ """ A thin, practical wrapper around terminal capabilities in Python -http://pypi.python.org/pypi/blessed +http://pypi.python.org/pypi/blessings """ import platform as _platform if ('3', '0', '0') <= _platform.python_version_tuple() < ('3', '2', '2+'): # Good till 3.2.10 # Python 3.x < 3.2.3 has a bug in which tparm() erroneously takes a string. - raise ImportError('Blessed needs Python 3.2.3 or greater for Python 3 ' + raise ImportError('Blessings needs Python 3.2.3 or greater for Python 3 ' 'support due to http://bugs.python.org/issue10570.') diff --git a/blessed/_binterms.py b/blessed/_binterms.py index 84f58fe..6c93ee0 100644 --- a/blessed/_binterms.py +++ b/blessed/_binterms.py @@ -1,6 +1,6 @@ -""" Exports a list of binary terminals blessed is not able to cope with. """ +""" 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 blessed cannot measure the sequence length for; they contain +#: 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. #: diff --git a/blessed/formatters.py b/blessed/formatters.py index e77adcd..54f090c 100644 --- a/blessed/formatters.py +++ b/blessed/formatters.py @@ -86,7 +86,7 @@ class ParameterizingProxyString(text_type): For example:: - >>> from blessed import Terminal + >>> from blessings import Terminal >>> term = Terminal('screen') >>> hpa = ParameterizingString(term.hpa, term.normal, 'hpa') >>> hpa(9) diff --git a/blessed/keyboard.py b/blessed/keyboard.py index d701a16..280606d 100644 --- a/blessed/keyboard.py +++ b/blessed/keyboard.py @@ -139,7 +139,7 @@ def get_keyboard_sequences(term): """get_keyboard_sequences(T) -> (OrderedDict) Initialize and return a keyboard map and sequence lookup table, - (sequence, constant) from blessed Terminal instance ``term``, + (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. diff --git a/blessed/sequences.py b/blessed/sequences.py index 9353da8..80649c3 100644 --- a/blessed/sequences.py +++ b/blessed/sequences.py @@ -1,5 +1,5 @@ # encoding: utf-8 -" This sub-module provides 'sequence awareness' for blessed." +" This sub-module provides 'sequence awareness' for blessings." __author__ = 'Jeff Quast ' __license__ = 'MIT' @@ -21,7 +21,7 @@ from ._binterms import binary_terminals as _BINTERM_UNSUPPORTED import wcwidth # https://github.com/jquast/wcwidth _BINTERM_UNSUPPORTED_MSG = ( - u"Terminal kind {0!r} contains binary-packed capabilities, blessed " + u"Terminal kind {0!r} contains binary-packed capabilities, blessings " u"is likely to fail to measure the length of its sequences.") if sys.version_info[0] == 3: @@ -491,8 +491,8 @@ class Sequence(text_type): as half or full-width characters. For example: - >>> from blessed import Terminal - >>> from blessed.sequences import Sequence + >>> from blessings import Terminal + >>> from blessings.sequences import Sequence >>> term = Terminal() >>> Sequence(term.clear + term.red(u'コンニチハ')).length() 5 @@ -543,8 +543,8 @@ class Sequence(text_type): those last-most characters are destroyed. All other sequences are simply removed. An example, - >>> from blessed import Terminal - >>> from blessed.sequences import Sequence + >>> from blessings import Terminal + >>> from blessings.sequences import Sequence >>> term = Terminal() >>> Sequence(term.clear + term.red(u'test')).strip_seqs() u'test' diff --git a/blessed/tests/accessories.py b/blessed/tests/accessories.py index f7e2a42..8cb3f5b 100644 --- a/blessed/tests/accessories.py +++ b/blessed/tests/accessories.py @@ -15,7 +15,7 @@ import pty import os # local -from blessed import Terminal +from blessings import Terminal # 3rd import pytest @@ -31,7 +31,7 @@ RECV_SEMAPHORE = b'SEMAPHORE\r\n' all_xterms_params = ['xterm', 'xterm-256color'] many_lines_params = [30, 100] many_columns_params = [1, 10] -from blessed._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: diff --git a/blessed/tests/test_core.py b/blessed/tests/test_core.py index befcea3..ce1e838 100644 --- a/blessed/tests/test_core.py +++ b/blessed/tests/test_core.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"Core blessed Terminal() tests." +"Core blessings Terminal() tests." # std try: @@ -29,8 +29,8 @@ import pytest def test_export_only_Terminal(): "Ensure only Terminal instance is exported for import * statements." - import blessed - assert blessed.__all__ == ('Terminal',) + import blessings + assert blessings.__all__ == ('Terminal',) def test_null_location(all_terms): @@ -246,23 +246,23 @@ def test_setupterm_invalid_has_no_styling(): reason='PyPy freezes') def test_missing_ordereddict_uses_module(monkeypatch): "ordereddict module is imported when without collections.OrderedDict." - import blessed.keyboard + import blessings.keyboard if hasattr(collections, 'OrderedDict'): monkeypatch.delattr('collections.OrderedDict') try: - imp.reload(blessed.keyboard) + imp.reload(blessings.keyboard) except ImportError as err: assert err.args[0] in ("No module named ordereddict", # py2 "No module named 'ordereddict'") # py3 sys.modules['ordereddict'] = mock.Mock() sys.modules['ordereddict'].OrderedDict = -1 - imp.reload(blessed.keyboard) - assert blessed.keyboard.OrderedDict == -1 + imp.reload(blessings.keyboard) + assert blessings.keyboard.OrderedDict == -1 del sys.modules['ordereddict'] monkeypatch.undo() - imp.reload(blessed.keyboard) + imp.reload(blessings.keyboard) else: assert platform.python_version_tuple() < ('2', '7') # reached by py2.6 @@ -271,51 +271,51 @@ def test_missing_ordereddict_uses_module(monkeypatch): reason='PyPy freezes') def test_python3_2_raises_exception(monkeypatch): "Test python version 3.0 through 3.2 raises an exception." - import blessed + import blessings monkeypatch.setattr('platform.python_version_tuple', lambda: ('3', '2', '2')) try: - imp.reload(blessed) + imp.reload(blessings) except ImportError as err: assert err.args[0] == ( - 'Blessed needs Python 3.2.3 or greater for Python 3 ' + 'Blessings needs Python 3.2.3 or greater for Python 3 ' 'support due to http://bugs.python.org/issue10570.') monkeypatch.undo() - imp.reload(blessed) + imp.reload(blessings) else: assert False, 'Exception should have been raised' def test_IOUnsupportedOperation_dummy(monkeypatch): "Ensure dummy exception is used when io is without UnsupportedOperation." - import blessed.terminal + import blessings.terminal import io if hasattr(io, 'UnsupportedOperation'): monkeypatch.delattr('io.UnsupportedOperation') - imp.reload(blessed.terminal) - assert blessed.terminal.IOUnsupportedOperation.__doc__.startswith( + imp.reload(blessings.terminal) + assert blessings.terminal.IOUnsupportedOperation.__doc__.startswith( "A dummy exception to take the place of") monkeypatch.undo() - imp.reload(blessed.terminal) + imp.reload(blessings.terminal) def test_without_dunder(): "Ensure dunder does not remain in module (py2x InterruptedError test." - import blessed.terminal - assert '_' not in dir(blessed.terminal) + import blessings.terminal + assert '_' not in dir(blessings.terminal) def test_IOUnsupportedOperation(): "Ensure stream that throws IOUnsupportedOperation results in non-tty." @as_subprocess def child(): - import blessed.terminal + import blessings.terminal def side_effect(): - raise blessed.terminal.IOUnsupportedOperation + raise blessings.terminal.IOUnsupportedOperation mock_stream = mock.Mock() mock_stream.fileno = side_effect @@ -431,17 +431,17 @@ def test_win32_missing_tty_modules(monkeypatch): else: __builtins__['__import__'] = __import__ try: - import blessed.terminal - imp.reload(blessed.terminal) + import blessings.terminal + imp.reload(blessings.terminal) except UserWarning: err = sys.exc_info()[1] - assert err.args[0] == blessed.terminal.msg_nosupport + assert err.args[0] == blessings.terminal.msg_nosupport warnings.filterwarnings("ignore", category=UserWarning) - import blessed.terminal - imp.reload(blessed.terminal) - assert blessed.terminal.HAS_TTY is False - term = blessed.terminal.Terminal('ansi') + import blessings.terminal + imp.reload(blessings.terminal) + assert blessings.terminal.HAS_TTY is False + term = blessings.terminal.Terminal('ansi') assert term.height == 24 assert term.width == 80 @@ -451,7 +451,7 @@ def test_win32_missing_tty_modules(monkeypatch): else: __builtins__['__import__'] = original_import warnings.resetwarnings() - import blessed.terminal - imp.reload(blessed.terminal) + import blessings.terminal + imp.reload(blessings.terminal) child() diff --git a/blessed/tests/test_formatters.py b/blessed/tests/test_formatters.py index b2e5f0c..3184140 100644 --- a/blessed/tests/test_formatters.py +++ b/blessed/tests/test_formatters.py @@ -6,7 +6,7 @@ import mock def test_parameterizing_string_args_unspecified(monkeypatch): """Test default args of formatters.ParameterizingString.""" - from blessed.formatters import ParameterizingString, FormattingString + from blessings.formatters import ParameterizingString, FormattingString # first argument to tparm() is the sequence name, returned as-is; # subsequent arguments are usually Integers. tparm = lambda *args: u'~'.join( @@ -38,7 +38,7 @@ def test_parameterizing_string_args_unspecified(monkeypatch): def test_parameterizing_string_args(monkeypatch): """Test basic formatters.ParameterizingString.""" - from blessed.formatters import ParameterizingString, FormattingString + from blessings.formatters import ParameterizingString, FormattingString # first argument to tparm() is the sequence name, returned as-is; # subsequent arguments are usually Integers. @@ -71,7 +71,7 @@ def test_parameterizing_string_args(monkeypatch): def test_parameterizing_string_type_error(monkeypatch): """Test formatters.ParameterizingString raising TypeError""" - from blessed.formatters import ParameterizingString + from blessings.formatters import ParameterizingString def tparm_raises_TypeError(*args): raise TypeError('custom_err') @@ -107,7 +107,7 @@ def test_parameterizing_string_type_error(monkeypatch): def test_formattingstring(monkeypatch): """Test formatters.FormattingString""" - from blessed.formatters import (FormattingString) + from blessings.formatters import FormattingString # given, with arg pstr = FormattingString(u'attr', u'norm') @@ -124,7 +124,7 @@ def test_formattingstring(monkeypatch): def test_nullcallablestring(monkeypatch): """Test formatters.NullCallableString""" - from blessed.formatters import (NullCallableString) + from blessings.formatters import (NullCallableString) # given, with arg pstr = NullCallableString() @@ -141,7 +141,7 @@ def test_nullcallablestring(monkeypatch): def test_split_compound(): """Test formatters.split_compound.""" - from blessed.formatters import split_compound + from blessings.formatters import split_compound assert split_compound(u'') == [u''] assert split_compound(u'a_b_c') == [u'a', u'b', u'c'] @@ -152,7 +152,7 @@ def test_split_compound(): def test_resolve_capability(monkeypatch): """Test formatters.resolve_capability and term sugaring """ - from blessed.formatters import resolve_capability + from blessings.formatters import resolve_capability # given, always returns a b'seq' tigetstr = lambda attr: ('seq-%s' % (attr,)).encode('latin1') @@ -183,9 +183,9 @@ def test_resolve_capability(monkeypatch): def test_resolve_color(monkeypatch): """Test formatters.resolve_color.""" - from blessed.formatters import (resolve_color, - FormattingString, - NullCallableString) + from blessings.formatters import (resolve_color, + FormattingString, + NullCallableString) color_cap = lambda digit: 'seq-%s' % (digit,) monkeypatch.setattr(curses, 'COLOR_RED', 1984) @@ -227,28 +227,30 @@ def test_resolve_color(monkeypatch): def test_resolve_attribute_as_color(monkeypatch): """ Test simple resolve_attribte() given color name. """ - import blessed - from blessed.formatters import resolve_attribute + import blessings + from blessings.formatters import resolve_attribute resolve_color = lambda term, digit: 'seq-%s' % (digit,) COLORS = set(['COLORX', 'COLORY']) COMPOUNDABLES = set(['JOINT', 'COMPOUND']) - monkeypatch.setattr(blessed.formatters, 'resolve_color', resolve_color) - monkeypatch.setattr(blessed.formatters, 'COLORS', COLORS) - monkeypatch.setattr(blessed.formatters, 'COMPOUNDABLES', COMPOUNDABLES) + monkeypatch.setattr(blessings.formatters, 'resolve_color', resolve_color) + monkeypatch.setattr(blessings.formatters, 'COLORS', COLORS) + monkeypatch.setattr(blessings.formatters, 'COMPOUNDABLES', COMPOUNDABLES) term = mock.Mock() assert resolve_attribute(term, 'COLORX') == u'seq-COLORX' def test_resolve_attribute_as_compoundable(monkeypatch): """ Test simple resolve_attribte() given a compoundable. """ - import blessed - from blessed.formatters import resolve_attribute, FormattingString + import blessings + from blessings.formatters import resolve_attribute, FormattingString resolve_cap = lambda term, digit: 'seq-%s' % (digit,) COMPOUNDABLES = set(['JOINT', 'COMPOUND']) - monkeypatch.setattr(blessed.formatters, 'resolve_capability', resolve_cap) - monkeypatch.setattr(blessed.formatters, 'COMPOUNDABLES', COMPOUNDABLES) + monkeypatch.setattr(blessings.formatters, + 'resolve_capability', + resolve_cap) + monkeypatch.setattr(blessings.formatters, 'COMPOUNDABLES', COMPOUNDABLES) term = mock.Mock() term.normal = 'seq-normal' @@ -260,12 +262,16 @@ def test_resolve_attribute_as_compoundable(monkeypatch): def test_resolve_attribute_non_compoundables(monkeypatch): """ Test recursive compounding of resolve_attribute(). """ - import blessed - from blessed.formatters import resolve_attribute, ParameterizingString + import blessings + from blessings.formatters import resolve_attribute, ParameterizingString uncompoundables = lambda attr: ['split', 'compound'] resolve_cap = lambda term, digit: 'seq-%s' % (digit,) - monkeypatch.setattr(blessed.formatters, 'split_compound', uncompoundables) - monkeypatch.setattr(blessed.formatters, 'resolve_capability', resolve_cap) + monkeypatch.setattr(blessings.formatters, + 'split_compound', + uncompoundables) + monkeypatch.setattr(blessings.formatters, + 'resolve_capability', + resolve_cap) tparm = lambda *args: u'~'.join( arg.decode('latin1') if not num else '%s' % (arg,) for num, arg in enumerate(args)).encode('latin1') @@ -286,12 +292,14 @@ def test_resolve_attribute_non_compoundables(monkeypatch): def test_resolve_attribute_recursive_compoundables(monkeypatch): """ Test recursive compounding of resolve_attribute(). """ - import blessed - from blessed.formatters import resolve_attribute, FormattingString + import blessings + from blessings.formatters import resolve_attribute, FormattingString # patch, resolve_cap = lambda term, digit: 'seq-%s' % (digit,) - monkeypatch.setattr(blessed.formatters, 'resolve_capability', resolve_cap) + monkeypatch.setattr(blessings.formatters, + 'resolve_capability', + resolve_cap) tparm = lambda *args: u'~'.join( arg.decode('latin1') if not num else '%s' % (arg,) for num, arg in enumerate(args)).encode('latin1') @@ -316,7 +324,7 @@ def test_resolve_attribute_recursive_compoundables(monkeypatch): def test_pickled_parameterizing_string(monkeypatch): """Test pickle-ability of a formatters.ParameterizingString.""" - from blessed.formatters import ParameterizingString, FormattingString + from blessings.formatters import ParameterizingString, FormattingString # simply send()/recv() over multiprocessing Pipe, a simple # pickle.loads(dumps(...)) did not reproduce this issue, @@ -356,7 +364,7 @@ def test_tparm_returns_null(monkeypatch): """ Test 'tparm() returned NULL' is caught (win32 PDCurses systems). """ # on win32, any calls to tparm raises curses.error with message, # "tparm() returned NULL", function PyCurses_tparm of _cursesmodule.c - from blessed.formatters import ParameterizingString, NullCallableString + from blessings.formatters import ParameterizingString, NullCallableString def tparm(*args): raise curses.error("tparm() returned NULL") @@ -376,7 +384,7 @@ def test_tparm_other_exception(monkeypatch): """ Test 'tparm() returned NULL' is caught (win32 PDCurses systems). """ # on win32, any calls to tparm raises curses.error with message, # "tparm() returned NULL", function PyCurses_tparm of _cursesmodule.c - from blessed.formatters import ParameterizingString, NullCallableString + from blessings.formatters import ParameterizingString, NullCallableString def tparm(*args): raise curses.error("unexpected error in tparm()") diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index 4ea806e..393b248 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -633,7 +633,7 @@ def test_esc_delay_keystroke_input_timout_0(): def test_keystroke_default_args(): "Test keyboard.Keystroke constructor with default arguments." - from blessed.keyboard import Keystroke + from blessings.keyboard import Keystroke ks = Keystroke() assert ks._name is None assert ks.name == ks._name @@ -647,7 +647,7 @@ def test_keystroke_default_args(): def test_a_keystroke(): "Test keyboard.Keystroke constructor with set arguments." - from blessed.keyboard import Keystroke + from blessings.keyboard import Keystroke ks = Keystroke(ucs=u'x', code=1, name=u'the X') assert ks._name is u'the X' assert ks.name == ks._name @@ -660,7 +660,7 @@ def test_a_keystroke(): def test_get_keyboard_codes(): "Test all values returned by get_keyboard_codes are from curses." - from blessed.keyboard import ( + from blessings.keyboard import ( get_keyboard_codes, CURSES_KEYCODE_OVERRIDE_MIXIN, ) @@ -675,7 +675,7 @@ def test_get_keyboard_codes(): def test_alternative_left_right(): "Test _alternative_left_right behavior for space/backspace." - from blessed.keyboard import _alternative_left_right + from blessings.keyboard import _alternative_left_right term = mock.Mock() term._cuf1 = u'' term._cub1 = u'' @@ -692,7 +692,7 @@ def test_alternative_left_right(): def test_cuf1_and_cub1_as_RIGHT_LEFT(all_terms): "Test that cuf1 and cub1 are assigned KEY_RIGHT and KEY_LEFT." - from blessed.keyboard import get_keyboard_sequences + from blessings.keyboard import get_keyboard_sequences @as_subprocess def child(kind): @@ -728,7 +728,7 @@ def test_get_keyboard_sequences_sort_order(xterms): def test_get_keyboard_sequence(monkeypatch): "Test keyboard.get_keyboard_sequence. " import curses.has_key - import blessed.keyboard + import blessings.keyboard (KEY_SMALL, KEY_LARGE, KEY_MIXIN) = range(3) (CAP_SMALL, CAP_LARGE) = 'cap-small cap-large'.split() @@ -749,7 +749,7 @@ def test_get_keyboard_sequence(monkeypatch): (KEY_LARGE, CAP_LARGE,)))) # patch global sequence mix-in - monkeypatch.setattr(blessed.keyboard, + monkeypatch.setattr(blessings.keyboard, 'DEFAULT_SEQUENCE_MIXIN', ( (SEQ_MIXIN.decode('latin1'), KEY_MIXIN),)) @@ -757,7 +757,7 @@ def test_get_keyboard_sequence(monkeypatch): term = mock.Mock() term._cuf1 = SEQ_ALT_CUF1.decode('latin1') term._cub1 = SEQ_ALT_CUB1.decode('latin1') - keymap = blessed.keyboard.get_keyboard_sequences(term) + keymap = blessings.keyboard.get_keyboard_sequences(term) assert list(keymap.items()) == [ (SEQ_LARGE.decode('latin1'), KEY_LARGE), @@ -769,7 +769,7 @@ def test_get_keyboard_sequence(monkeypatch): def test_resolve_sequence(): "Test resolve_sequence for order-dependent mapping." - from blessed.keyboard import resolve_sequence, OrderedDict + from blessings.keyboard import resolve_sequence, OrderedDict mapper = OrderedDict(((u'SEQ1', 1), (u'SEQ2', 2), # takes precedence over LONGSEQ, first-match @@ -841,7 +841,7 @@ def test_keypad_mixins_and_aliases(): @as_subprocess def child(kind): term = TestTerminal(kind=kind, force_styling=True) - from blessed.keyboard import resolve_sequence + from blessings.keyboard import resolve_sequence resolve = functools.partial(resolve_sequence, mapper=term._keymap, diff --git a/blessed/tests/test_length_sequence.py b/blessed/tests/test_length_sequence.py index 88096dc..0eac165 100644 --- a/blessed/tests/test_length_sequence.py +++ b/blessed/tests/test_length_sequence.py @@ -41,7 +41,7 @@ def test_length_ansiart(): @as_subprocess def child(): import codecs - from blessed.sequences import Sequence + from blessings.sequences import Sequence term = TestTerminal(kind='xterm-256color') # this 'ansi' art contributed by xzip!impure for another project, # unlike most CP-437 DOS ansi art, this is actually utf-8 encoded. @@ -276,7 +276,7 @@ def test_sequence_is_movement_false(all_terms): """Test parser about sequences that do not move the cursor.""" @as_subprocess def child_mnemonics_wontmove(kind): - from blessed.sequences import measure_length + from blessings.sequences import measure_length t = TestTerminal(kind=kind) assert (0 == measure_length(u'', t)) # not even a mbs @@ -313,7 +313,7 @@ def test_sequence_is_movement_true(all_terms): """Test parsers about sequences that move the cursor.""" @as_subprocess def child_mnemonics_willmove(kind): - from blessed.sequences import measure_length + from blessings.sequences import measure_length t = TestTerminal(kind=kind) # movements assert (len(t.move(98, 76)) == diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py index b15426c..f42a52c 100644 --- a/blessed/tests/test_sequences.py +++ b/blessed/tests/test_sequences.py @@ -94,8 +94,8 @@ 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 blessed.sequences import _BINTERM_UNSUPPORTED_MSG - from blessed._binterms import binary_terminals + from blessings.sequences import _BINTERM_UNSUPPORTED_MSG + from blessings._binterms import binary_terminals @as_subprocess def child(kind): @@ -122,9 +122,9 @@ 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 blessed._binterms import binary_terminals - from blessed.sequences import (_BINTERM_UNSUPPORTED_MSG, - init_sequence_patterns) + from blessings._binterms import binary_terminals + from blessings.sequences import (_BINTERM_UNSUPPORTED_MSG, + init_sequence_patterns) warnings.filterwarnings("error", category=UserWarning) term = mock.Mock() term.kind = binary_terminals[random.randrange(len(binary_terminals))] @@ -141,7 +141,7 @@ def test_unit_binpacked_unittest(): def test_merge_sequences(): """Test sequences are filtered and ordered longest-first.""" - from blessed.sequences import _merge_sequences + from blessings.sequences import _merge_sequences input_list = [u'a', u'aa', u'aaa', u''] output_expected = [u'aaa', u'aa', u'a'] assert (_merge_sequences(input_list) == output_expected) @@ -495,7 +495,7 @@ def test_null_callable_string(all_terms): def test_bnc_parameter_emits_warning(): """A fake capability without target digits emits a warning.""" import warnings - from blessed.sequences import _build_numeric_capability + from blessings.sequences import _build_numeric_capability # given, warnings.filterwarnings("error", category=UserWarning) @@ -517,7 +517,7 @@ def test_bnc_parameter_emits_warning(): def test_bna_parameter_emits_warning(): """A fake capability without any digits emits a warning.""" import warnings - from blessed.sequences import _build_any_numeric_capability + from blessings.sequences import _build_any_numeric_capability # given, warnings.filterwarnings("error", category=UserWarning) @@ -540,8 +540,8 @@ def test_padd(): """ Test terminal.padd(seq). """ @as_subprocess def child(): - from blessed.sequences import Sequence - from blessed import Terminal + from blessings.sequences import Sequence + from blessings import Terminal term = Terminal('xterm-256color') assert Sequence('xyz\b', term).padd() == u'xy' assert Sequence('xxxx\x1b[3Dzz', term).padd() == u'xzz' diff --git a/docs/Makefile b/docs/Makefile index 47febae..c1d668a 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -72,17 +72,17 @@ qthelp: @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/blessed.qhcp" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/blessings.qhcp" @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/blessed.qhc" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/blessings.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/blessed" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/blessed" + @echo "# mkdir -p $$HOME/.local/share/devhelp/blessings" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/blessings" @echo "# devhelp" epub: diff --git a/docs/conf.py b/docs/conf.py index 6975d7f..7361600 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# blessed documentation build configuration file, created by +# blessings documentation build configuration file, created by # sphinx-quickstart on Thu Mar 31 13:40:27 2011. # # This file is execfile()d with the current directory set to its @@ -20,7 +20,7 @@ import os here = os.path.dirname(__file__) sys.path.insert(0, os.path.abspath(os.path.join(here, '..'))) -import blessed +import blessings # -- General configuration ---------------------------------------------------- @@ -45,7 +45,7 @@ source_suffix = '.rst' master_doc = 'index' # General information about the project. -project = u'Blessed' +project = u'Blessings' copyright = u'2014 Jeff Quast, 2011 Erik Rose' # The version info for the project you're documenting, acts as replacement for @@ -171,7 +171,7 @@ html_static_path = [] # html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'blesseddoc' +htmlhelp_basename = 'blessingsdoc' # -- Options for LaTeX output ------------------------------------------------- @@ -186,7 +186,7 @@ htmlhelp_basename = 'blesseddoc' # (source start file, target name, title, author, documentclass # [howto/manual]). latex_documents = [ - ('index', 'blessed.tex', u'Blessed Documentation', + ('index', 'blessings.tex', u'Blessings Documentation', u'Jeff Quast', 'manual'), ] @@ -219,10 +219,10 @@ latex_documents = [ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'Blessed', u'Blessed Documentation', + ('index', 'Blessings', u'Blessings Documentation', [u'Jeff Quast'], 1) ] autodoc_member_order = 'bysource' -del blessed # imported but unused +del blessings # imported but unused diff --git a/docs/index.rst b/docs/index.rst index 0383b3a..41da922 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,20 +1,20 @@ -===================== -Blessed API Reference -===================== +======================= +Blessings API Reference +======================= Read The Readme First ===================== -This is the API documentation for the Blessed terminal library. +This is the API documentation for the Blessings terminal library. -Because Blessed uses quite a bit of dynamism, you should +Because Blessings uses quite a bit of dynamism, you should `read the readme first`_ for a general guide and overview. However, if you're looking for the documentation of the internal classes, their methods, and related functions that make up the internals, you're in the right place. -.. _`read the readme first`: http://pypi.python.org/pypi/blessed +.. _`read the readme first`: http://pypi.python.org/pypi/blessings API Documentation ================= @@ -24,27 +24,27 @@ Internal modules are as follows. terminal module (primary) ------------------------- -.. automodule:: blessed.terminal +.. automodule:: blessings.terminal :members: :undoc-members: formatters module ----------------- -.. automodule:: blessed.formatters +.. automodule:: blessings.formatters :members: :undoc-members: keyboard module --------------- -.. automodule:: blessed.keyboard +.. automodule:: blessings.keyboard :members: :undoc-members: sequences module ---------------- -.. automodule:: blessed.sequences +.. automodule:: blessings.sequences :members: :undoc-members: diff --git a/docs/make.bat b/docs/make.bat index 21ff37b..c24653f 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -95,9 +95,9 @@ if "%1" == "qthelp" ( echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\blessed.qhcp + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\blessings.qhcp echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\blessed.ghc + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\blessings.ghc goto end ) diff --git a/setup.py b/setup.py index 489ebed..c473cda 100755 --- a/setup.py +++ b/setup.py @@ -37,17 +37,17 @@ def main(): extra['install_requires'].extend(['ordereddict>=1.1']) setuptools.setup( - name='blessed', + name='blessings', version='1.9.5', description="A feature-filled fork of Erik Rose's blessings project", long_description=open(os.path.join(here, 'README.rst')).read(), author='Jeff Quast', author_email='contact@jeffquast.com', license='MIT', - packages=['blessed', 'blessed.tests'], - url='https://github.com/jquast/blessed', + packages=['blessings', 'blessings.tests'], + url='https://github.com/erikrose/blessings', include_package_data=True, - test_suite='blessed.tests', + test_suite='blessings.tests', classifiers=[ 'Intended Audience :: Developers', 'Natural Language :: English', diff --git a/tox.ini b/tox.ini index 5c93fca..ff8b2b6 100644 --- a/tox.ini +++ b/tox.ini @@ -20,7 +20,7 @@ deps = pytest-flakes commands = {envbindir}/py.test \ --strict --pep8 --flakes \ --junit-xml=results.{envname}.xml --verbose \ - --cov blessed blessed/tests --cov-report=term-missing \ + --cov blessings blessings/tests --cov-report=term-missing \ {posargs} /bin/mv {toxinidir}/.coverage {toxinidir}/.coverage.{envname} -- cgit v1.2.1 From 7eb082718b8cd02a7a0c55e873a3f16b96d64ca6 Mon Sep 17 00:00:00 2001 From: Erik Rose Date: Tue, 3 Feb 2015 00:21:48 -0500 Subject: Rename "blessed" dir to "blessings". This makes the tests pass. --- blessed/__init__.py | 16 - blessed/_binterms.py | 872 ------------------------------ blessed/formatters.py | 332 ------------ blessed/keyboard.py | 276 ---------- blessed/sequences.py | 688 ------------------------ blessed/terminal.py | 783 --------------------------- blessed/tests/__init__.py | 0 blessed/tests/accessories.py | 240 --------- blessed/tests/test_core.py | 457 ---------------- blessed/tests/test_formatters.py | 403 -------------- blessed/tests/test_keyboard.py | 902 -------------------------------- blessed/tests/test_length_sequence.py | 342 ------------ blessed/tests/test_sequences.py | 549 ------------------- blessed/tests/test_wrap.py | 105 ---- blessed/tests/wall.ans | 7 - blessings/__init__.py | 16 + blessings/_binterms.py | 872 ++++++++++++++++++++++++++++++ blessings/formatters.py | 332 ++++++++++++ blessings/keyboard.py | 276 ++++++++++ blessings/sequences.py | 688 ++++++++++++++++++++++++ blessings/terminal.py | 783 +++++++++++++++++++++++++++ blessings/tests/__init__.py | 0 blessings/tests/accessories.py | 240 +++++++++ blessings/tests/test_core.py | 457 ++++++++++++++++ blessings/tests/test_formatters.py | 403 ++++++++++++++ blessings/tests/test_keyboard.py | 902 ++++++++++++++++++++++++++++++++ blessings/tests/test_length_sequence.py | 342 ++++++++++++ blessings/tests/test_sequences.py | 549 +++++++++++++++++++ blessings/tests/test_wrap.py | 105 ++++ blessings/tests/wall.ans | 7 + 30 files changed, 5972 insertions(+), 5972 deletions(-) delete mode 100644 blessed/__init__.py delete mode 100644 blessed/_binterms.py delete mode 100644 blessed/formatters.py delete mode 100644 blessed/keyboard.py delete mode 100644 blessed/sequences.py delete mode 100644 blessed/terminal.py delete mode 100644 blessed/tests/__init__.py delete mode 100644 blessed/tests/accessories.py delete mode 100644 blessed/tests/test_core.py delete mode 100644 blessed/tests/test_formatters.py delete mode 100644 blessed/tests/test_keyboard.py delete mode 100644 blessed/tests/test_length_sequence.py delete mode 100644 blessed/tests/test_sequences.py delete mode 100644 blessed/tests/test_wrap.py delete mode 100644 blessed/tests/wall.ans create mode 100644 blessings/__init__.py create mode 100644 blessings/_binterms.py create mode 100644 blessings/formatters.py create mode 100644 blessings/keyboard.py create mode 100644 blessings/sequences.py create mode 100644 blessings/terminal.py create mode 100644 blessings/tests/__init__.py create mode 100644 blessings/tests/accessories.py create mode 100644 blessings/tests/test_core.py create mode 100644 blessings/tests/test_formatters.py create mode 100644 blessings/tests/test_keyboard.py create mode 100644 blessings/tests/test_length_sequence.py create mode 100644 blessings/tests/test_sequences.py create mode 100644 blessings/tests/test_wrap.py create mode 100644 blessings/tests/wall.ans diff --git a/blessed/__init__.py b/blessed/__init__.py deleted file mode 100644 index 3622d0e..0000000 --- a/blessed/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -A thin, practical wrapper around terminal capabilities in Python - -http://pypi.python.org/pypi/blessings -""" -import platform as _platform -if ('3', '0', '0') <= _platform.python_version_tuple() < ('3', '2', '2+'): - # Good till 3.2.10 - # Python 3.x < 3.2.3 has a bug in which tparm() erroneously takes a string. - raise ImportError('Blessings needs Python 3.2.3 or greater for Python 3 ' - 'support due to http://bugs.python.org/issue10570.') - - -from .terminal import Terminal - -__all__ = ('Terminal',) diff --git a/blessed/_binterms.py b/blessed/_binterms.py deleted file mode 100644 index 6c93ee0..0000000 --- a/blessed/_binterms.py +++ /dev/null @@ -1,872 +0,0 @@ -""" 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. -#: -#: This may be generated by exporting TEST_BINTERMS, then analyzing the -#: jUnit result xml written to the project folder. -binary_terminals = u""" -9term -aaa+dec -aaa+rv -aaa+rv-100 -aaa+rv-30 -aaa-rv-unk -abm80 -abm85 -abm85e -abm85h -abm85h-old -act4 -act5 -addrinfo -adds980 -adm+sgr -adm+sgr-100 -adm+sgr-30 -adm11 -adm1178 -adm12 -adm1a -adm2 -adm20 -adm21 -adm22 -adm3 -adm31 -adm31-old -adm3a -adm3a+ -adm42 -adm42-ns -adm5 -aepro -aj510 -aj830 -alto-h19 -altos4 -altos7 -altos7pc -ampex175 -ampex175-b -ampex210 -ampex232 -ampex232w -ampex80 -annarbor4080 -ansi+arrows -ansi+csr -ansi+cup -ansi+enq -ansi+erase -ansi+idc -ansi+idl -ansi+idl1 -ansi+inittabs -ansi+local -ansi+local1 -ansi+pp -ansi+rca -ansi+rep -ansi+sgr -ansi+sgrbold -ansi+sgrdim -ansi+sgrso -ansi+sgrul -ansi+tabs -ansi-color-2-emx -ansi-color-3-emx -ansi-emx -ansi-mini -ansi-mr -ansi-mtabs -ansi-nt -ansi.sys -ansi.sys-old -ansi.sysk -ansi77 -apollo -apple-80 -apple-ae -apple-soroc -apple-uterm -apple-uterm-vb -apple-videx -apple-videx2 -apple-videx3 -apple-vm80 -apple2e -apple2e-p -apple80p -appleII -appleIIgs -atari -att4415+nl -att4420 -att4424m -att5310 -att5310-100 -att5310-30 -att5620-s -avatar -avatar0 -avatar0+ -avt+s -aws -awsc -bantam -basis -beacon -beehive -blit -bq300-8 -bq300-8-pc -bq300-8-pc-rv -bq300-8-pc-w -bq300-8-pc-w-rv -bq300-8rv -bq300-8w -bq300-w-8rv -c100 -c100-rv -c108 -c108-4p -c108-rv -c108-rv-4p -c108-w -ca22851 -cbblit -cbunix -cci -cci-100 -cci-30 -cdc456 -cdc721 -cdc721-esc -cdc721-esc-100 -cdc721-esc-30 -cdc721ll -cdc752 -cdc756 -cit101e -cit101e-132 -cit101e-n -cit101e-n132 -cit80 -citoh -citoh-6lpi -citoh-8lpi -citoh-comp -citoh-elite -citoh-pica -citoh-prop -coco3 -color_xterm -commodore -contel300 -contel301 -cops10 -ct8500 -ctrm -ctrm-100 -ctrm-30 -cyb110 -cyb83 -d132 -d200 -d200-100 -d200-30 -d210-dg -d210-dg-100 -d210-dg-30 -d211-dg -d211-dg-100 -d211-dg-30 -d216-dg -d216-dg-100 -d216-dg-30 -d216-unix -d216-unix-25 -d217-unix -d217-unix-25 -d220 -d220-100 -d220-30 -d220-7b -d220-7b-100 -d220-7b-30 -d220-dg -d220-dg-100 -d220-dg-30 -d230c -d230c-100 -d230c-30 -d230c-dg -d230c-dg-100 -d230c-dg-30 -d400 -d400-100 -d400-30 -d410-dg -d410-dg-100 -d410-dg-30 -d412-dg -d412-dg-100 -d412-dg-30 -d412-unix -d412-unix-25 -d412-unix-s -d412-unix-sr -d412-unix-w -d413-unix -d413-unix-25 -d413-unix-s -d413-unix-sr -d413-unix-w -d414-unix -d414-unix-25 -d414-unix-s -d414-unix-sr -d414-unix-w -d430c-dg -d430c-dg-100 -d430c-dg-30 -d430c-dg-ccc -d430c-dg-ccc-100 -d430c-dg-ccc-30 -d430c-unix -d430c-unix-100 -d430c-unix-25 -d430c-unix-25-100 -d430c-unix-25-30 -d430c-unix-25-ccc -d430c-unix-30 -d430c-unix-ccc -d430c-unix-s -d430c-unix-s-ccc -d430c-unix-sr -d430c-unix-sr-ccc -d430c-unix-w -d430c-unix-w-ccc -d470c -d470c-7b -d470c-dg -d555-dg -d577-dg -d800 -delta -dg+ccc -dg+color -dg+color8 -dg+fixed -dg-generic -dg200 -dg210 -dg211 -dg450 -dg460-ansi -dg6053 -dg6053-old -dgkeys+11 -dgkeys+15 -dgkeys+7b -dgkeys+8b -dgmode+color -dgmode+color8 -dgunix+ccc -dgunix+fixed -diablo1620 -diablo1620-m8 -diablo1640 -diablo1640-lm -diablo1740-lm -digilog -djgpp203 -dm1520 -dm2500 -dm3025 -dm3045 -dmchat -dmterm -dp8242 -dt100 -dt100w -dt110 -dt80-sas -dtc300s -dtc382 -dumb -dw1 -dw2 -dw3 -dw4 -dwk -ecma+color -ecma+sgr -elks -elks-glasstty -elks-vt52 -emu -ep40 -ep48 -esprit -esprit-am -ex155 -f100 -f100-rv -f110 -f110-14 -f110-14w -f110-w -f200 -f200-w -f200vi -f200vi-w -falco -falco-p -fos -fox -gator-52 -gator-52t -glasstty -gnome -gnome+pcfkeys -gnome-2007 -gnome-2008 -gnome-256color -gnome-fc5 -gnome-rh72 -gnome-rh80 -gnome-rh90 -go140 -go140w -gs6300 -gsi -gt40 -gt42 -guru+rv -guru+s -h19 -h19-bs -h19-g -h19-u -h19-us -h19k -ha8675 -ha8686 -hft-old -hmod1 -hp+arrows -hp+color -hp+labels -hp+pfk+arrows -hp+pfk+cr -hp+pfk-cr -hp+printer -hp2 -hp236 -hp262x -hp2641a -hp300h -hp700-wy -hp70092 -hp9837 -hp9845 -hpterm -hpterm-color -hz1000 -hz1420 -hz1500 -hz1510 -hz1520 -hz1520-noesc -hz1552 -hz1552-rv -hz2000 -i100 -i400 -ibcs2 -ibm+16color -ibm+color -ibm-apl -ibm-system1 -ibm3101 -ibm3151 -ibm3161 -ibm3161-C -ibm3162 -ibm3164 -ibm327x -ibm5081-c -ibm8514-c -ibmaed -ibmapa8c -ibmapa8c-c -ibmega -ibmega-c -ibmmono -ibmvga -ibmvga-c -icl6404 -icl6404-w -ifmr -ims-ansi -ims950 -ims950-b -ims950-rv -intertube -intertube2 -intext -intext2 -kaypro -kermit -kermit-am -klone+acs -klone+color -klone+koi8acs -klone+sgr -klone+sgr-dumb -klone+sgr8 -konsole -konsole+pcfkeys -konsole-16color -konsole-256color -konsole-base -konsole-linux -konsole-solaris -konsole-vt100 -konsole-vt420pc -konsole-xf3x -konsole-xf4x -kt7 -kt7ix -ln03 -ln03-w -lpr -luna -megatek -mgterm -microb -mime -mime-fb -mime-hb -mime2a -mime2a-s -mime314 -mime3a -mime3ax -minitel1 -minitel1b -mlterm+pcfkeys -mm340 -modgraph2 -msk227 -msk22714 -msk227am -mt70 -ncr160vppp -ncr160vpwpp -ncr160wy50+pp -ncr160wy50+wpp -ncr160wy60pp -ncr160wy60wpp -ncr260vppp -ncr260vpwpp -ncr260wy325pp -ncr260wy325wpp -ncr260wy350pp -ncr260wy350wpp -ncr260wy50+pp -ncr260wy50+wpp -ncr260wy60pp -ncr260wy60wpp -ncr7900i -ncr7900iv -ncr7901 -ncrvt100an -ncrvt100wan -ndr9500 -ndr9500-25 -ndr9500-25-mc -ndr9500-25-mc-nl -ndr9500-25-nl -ndr9500-mc -ndr9500-mc-nl -ndr9500-nl -nec5520 -newhpkeyboard -nextshell -northstar -nsterm+c -nsterm+c41 -nsterm+s -nwp511 -oblit -oc100 -oldpc3 -origpc3 -osborne -osborne-w -osexec -otek4112 -owl -p19 -pc-coherent -pc-venix -pc6300plus -pcix -pckermit -pckermit120 -pe1251 -pe7000c -pe7000m -pilot -pmcons -prism2 -prism4 -prism5 -pro350 -psterm-fast -psterm-fast-100 -psterm-fast-30 -pt100 -pt210 -pt250 -pty -qansi -qansi-g -qansi-m -qansi-t -qansi-w -qdss -qnx -qnx-100 -qnx-30 -qnxm -qnxm-100 -qnxm-30 -qnxt -qnxt-100 -qnxt-30 -qnxt2 -qnxtmono -qnxtmono-100 -qnxtmono-30 -qnxw -qnxw-100 -qnxw-30 -qume5 -qvt101 -qvt101+ -qvt102 -qvt119+ -qvt119+-25 -qvt119+-25-w -qvt119+-w -rbcomm -rbcomm-nam -rbcomm-w -rca -regent100 -regent20 -regent25 -regent40 -regent40+ -regent60 -rt6221 -rt6221-w -rtpc -rxvt+pcfkeys -scanset -screen+fkeys -screen-16color -screen-16color-bce -screen-16color-bce-s -screen-16color-s -screen-256color -screen-256color-bce -screen-256color-bce-s -screen-256color-s -screen-bce -screen-s -screen-w -screen.linux -screen.rxvt -screen.teraterm -screen.xterm-r6 -screen2 -screen3 -screwpoint -sibo -simterm -soroc120 -soroc140 -st52 -superbee-xsb -superbeeic -superbrain -swtp -synertek -t10 -t1061 -t1061f -t3700 -t3800 -tandem6510 -tandem653 -tandem653-100 -tandem653-30 -tek -tek4013 -tek4014 -tek4014-sm -tek4015 -tek4015-sm -tek4023 -tek4105 -tek4107 -tek4113-nd -tek4205 -tek4205-100 -tek4205-30 -tek4207-s -teraterm -teraterm2.3 -teraterm4.59 -terminet1200 -ti700 -ti931 -trs16 -trs2 -tt -tty33 -tty37 -tty43 -tvi803 -tvi9065 -tvi910 -tvi910+ -tvi912 -tvi912b -tvi912b+2p -tvi912b+dim -tvi912b+dim-100 -tvi912b+dim-30 -tvi912b+mc -tvi912b+mc-100 -tvi912b+mc-30 -tvi912b+printer -tvi912b+vb -tvi912b-2p -tvi912b-2p-mc -tvi912b-2p-mc-100 -tvi912b-2p-mc-30 -tvi912b-2p-p -tvi912b-2p-unk -tvi912b-mc -tvi912b-mc-100 -tvi912b-mc-30 -tvi912b-p -tvi912b-unk -tvi912b-vb -tvi912b-vb-mc -tvi912b-vb-mc-100 -tvi912b-vb-mc-30 -tvi912b-vb-p -tvi912b-vb-unk -tvi920b -tvi920b+fn -tvi920b-2p -tvi920b-2p-mc -tvi920b-2p-mc-100 -tvi920b-2p-mc-30 -tvi920b-2p-p -tvi920b-2p-unk -tvi920b-mc -tvi920b-mc-100 -tvi920b-mc-30 -tvi920b-p -tvi920b-unk -tvi920b-vb -tvi920b-vb-mc -tvi920b-vb-mc-100 -tvi920b-vb-mc-30 -tvi920b-vb-p -tvi920b-vb-unk -tvi921 -tvi924 -tvi925 -tvi925-hi -tvi92B -tvi92D -tvi950 -tvi950-2p -tvi950-4p -tvi950-rv -tvi950-rv-2p -tvi950-rv-4p -tvipt -unknown -vanilla -vc303 -vc404 -vc404-s -vc414 -vc415 -vi200 -vi200-f -vi200-rv -vi50 -vi500 -vi50adm -vi55 -viewpoint -vp3a+ -vp60 -vp90 -vremote -vt100+enq -vt100+fnkeys -vt100+keypad -vt100+pfkeys -vt100-s -vt102+enq -vt200-js -vt220+keypad -vt50h -vt52 -vt61 -wsiris -wy100 -wy100q -wy120 -wy120-25 -wy120-vb -wy160 -wy160-25 -wy160-42 -wy160-43 -wy160-tek -wy160-tek-100 -wy160-tek-30 -wy160-vb -wy30 -wy30-mc -wy30-mc-100 -wy30-mc-30 -wy30-vb -wy325 -wy325-25 -wy325-42 -wy325-43 -wy325-vb -wy350 -wy350-100 -wy350-30 -wy350-vb -wy350-vb-100 -wy350-vb-30 -wy350-w -wy350-w-100 -wy350-w-30 -wy350-wvb -wy350-wvb-100 -wy350-wvb-30 -wy370 -wy370-100 -wy370-105k -wy370-105k-100 -wy370-105k-30 -wy370-30 -wy370-EPC -wy370-EPC-100 -wy370-EPC-30 -wy370-nk -wy370-nk-100 -wy370-nk-30 -wy370-rv -wy370-rv-100 -wy370-rv-30 -wy370-tek -wy370-tek-100 -wy370-tek-30 -wy370-vb -wy370-vb-100 -wy370-vb-30 -wy370-w -wy370-w-100 -wy370-w-30 -wy370-wvb -wy370-wvb-100 -wy370-wvb-30 -wy50 -wy50-mc -wy50-mc-100 -wy50-mc-30 -wy50-vb -wy60 -wy60-25 -wy60-42 -wy60-43 -wy60-vb -wy99-ansi -wy99a-ansi -wy99f -wy99fa -wy99gt -wy99gt-25 -wy99gt-vb -wy99gt-tek -wyse-vp -xerox1720 -xerox820 -xfce -xnuppc+100x37 -xnuppc+112x37 -xnuppc+128x40 -xnuppc+128x48 -xnuppc+144x48 -xnuppc+160x64 -xnuppc+200x64 -xnuppc+200x75 -xnuppc+256x96 -xnuppc+80x25 -xnuppc+80x30 -xnuppc+90x30 -xnuppc+c -xnuppc+c-100 -xnuppc+c-30 -xtalk -xtalk-100 -xtalk-30 -xterm+256color -xterm+256color-100 -xterm+256color-30 -xterm+88color -xterm+88color-100 -xterm+88color-30 -xterm+app -xterm+edit -xterm+noapp -xterm+pc+edit -xterm+pcc0 -xterm+pcc1 -xterm+pcc2 -xterm+pcc3 -xterm+pce2 -xterm+pcf0 -xterm+pcf2 -xterm+pcfkeys -xterm+r6f2 -xterm+vt+edit -xterm-vt52 -z100 -z100bw -z29 -zen30 -zen50 -ztx -""".split() - -__all__ = ('binary_terminals',) diff --git a/blessed/formatters.py b/blessed/formatters.py deleted file mode 100644 index 54f090c..0000000 --- a/blessed/formatters.py +++ /dev/null @@ -1,332 +0,0 @@ -"This sub-module provides formatting functions." -import curses -import sys - -_derivatives = ('on', 'bright', 'on_bright',) - -_colors = set('black red green yellow blue magenta cyan white'.split()) -_compoundables = set('bold underline reverse blink dim italic shadow ' - 'standout subscript superscript'.split()) - -#: Valid colors and their background (on), bright, and bright-bg derivatives. -COLORS = set(['_'.join((derivitive, color)) - for derivitive in _derivatives - for color in _colors]) | _colors - -#: All valid compoundable names. -COMPOUNDABLES = (COLORS | _compoundables) - -if sys.version_info[0] == 3: - text_type = str - basestring = str -else: - text_type = unicode # noqa - - -class ParameterizingString(text_type): - """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') - u'\x1b[91mcolor #9\x1b(B\x1b[m' - """ - - def __new__(cls, *args): - """P.__new__(cls, cap, [normal, [name]]) - - :arg cap: parameterized string suitable for curses.tparm() - :arg normal: terminating sequence for this capability. - :arg name: name of this terminal capability. - """ - assert len(args) and len(args) < 4, args - new = text_type.__new__(cls, args[0]) - new._normal = len(args) > 1 and args[1] or u'' - new._name = len(args) > 2 and args[2] or u'' - return new - - def __call__(self, *args): - """P(*args) -> FormattingString() - - Return evaluated terminal capability (self), receiving arguments - ``*args``, followed by the terminating sequence (self.normal) into - a FormattingString capable of being called. - """ - try: - # Re-encode the cap, because tparm() takes a bytestring in Python - # 3. However, appear to be a plain Unicode string otherwise so - # concats work. - attr = curses.tparm(self.encode('latin1'), *args).decode('latin1') - return FormattingString(attr, self._normal) - except TypeError as err: - # If the first non-int (i.e. incorrect) arg was a string, suggest - # something intelligent: - if len(args) and isinstance(args[0], basestring): - raise TypeError( - "A native or nonexistent capability template, %r received" - " invalid argument %r: %s. You probably misspelled a" - " formatting call like `bright_red'" % ( - self._name, args, err)) - # Somebody passed a non-string; I don't feel confident - # guessing what they were trying to do. - raise - except curses.error as err: - # ignore 'tparm() returned NULL', you won't get any styling, - # even if does_styling is True. This happens on win32 platforms - # with http://www.lfd.uci.edu/~gohlke/pythonlibs/#curses installed - if "tparm() returned NULL" not in text_type(err): - raise - return NullCallableString() - - -class ParameterizingProxyString(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' - """ - - def __new__(cls, *args): - """P.__new__(cls, (fmt, callable), [normal, [name]]) - - :arg fmt: format string suitable for displaying terminal sequences. - :arg callable: receives __call__ arguments for formatting fmt. - :arg normal: terminating sequence for this capability. - :arg name: name of this terminal capability. - """ - assert len(args) and len(args) < 4, args - assert type(args[0]) is tuple, args[0] - assert callable(args[0][1]), args[0][1] - new = text_type.__new__(cls, args[0][0]) - new._fmt_args = args[0][1] - new._normal = len(args) > 1 and args[1] or u'' - new._name = len(args) > 2 and args[2] or u'' - return new - - def __call__(self, *args): - """P(*args) -> FormattingString() - - Return evaluated terminal capability format, (self), using callable - ``self._fmt_args`` receiving arguments ``*args``, followed by the - terminating sequence (self.normal) into a FormattingString capable - of being called. - """ - return FormattingString(self.format(*self._fmt_args(*args)), - self._normal) - - -def get_proxy_string(term, attr): - """ 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. - """ - # normalize 'screen-256color', or 'ansi.sys' to its basic names - term_kind = next(iter(_kind for _kind in ('screen', 'ansi',) - if term.kind.startswith(_kind)), term) - return { - 'screen': { - # proxy move_x/move_y for 'screen' terminal type. - 'hpa': ParameterizingProxyString( - (u'\x1b[{0}G', lambda *arg: (arg[0] + 1,)), term.normal, attr), - 'vpa': ParameterizingProxyString( - (u'\x1b[{0}d', lambda *arg: (arg[0] + 1,)), term.normal, attr), - }, - 'ansi': { - # proxy show/hide cursor for 'ansi' terminal type. - 'civis': ParameterizingProxyString( - (u'\x1b[?25l', lambda *arg: ()), term.normal, attr), - 'cnorm': ParameterizingProxyString( - (u'\x1b[?25h', lambda *arg: ()), term.normal, attr), - 'hpa': ParameterizingProxyString( - (u'\x1b[{0}G', lambda *arg: (arg[0] + 1,)), term.normal, attr), - 'vpa': ParameterizingProxyString( - (u'\x1b[{0}d', lambda *arg: (arg[0] + 1,)), term.normal, attr), - } - }.get(term_kind, {}).get(attr, None) - - -class FormattingString(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' - """ - - def __new__(cls, *args): - """P.__new__(cls, sequence, [normal]) - :arg sequence: terminal attribute sequence. - :arg normal: terminating sequence for this attribute. - """ - assert 1 <= len(args) <= 2, args - new = text_type.__new__(cls, args[0]) - new._normal = len(args) > 1 and args[1] or u'' - return new - - def __call__(self, text): - """P(text) -> unicode - - Return string ``text``, joined by specified video attribute, - (self), and followed by reset attribute sequence (term.normal). - """ - if len(self): - return u''.join((self, text, self._normal)) - return text - - -class NullCallableString(text_type): - """A dummy callable Unicode to stand in for ``FormattingString`` and - ``ParameterizingString`` for terminals that cannot perform styling. - """ - def __new__(cls): - new = 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). - - 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 - terminfo(5) which accept non-int arguments, they are seldom used. - - When called with a non-int as the first arg (no no args at all), return - the first arg, acting in place of ``FormattingString`` without - any attributes. - """ - if len(args) != 1 or isinstance(args[0], int): - # I am acting as a ParameterizingString. - - # tparm can take not only ints but also (at least) strings as its - # 2nd...nth argument. But we don't support callable parameterizing - # capabilities that take non-ints yet, so we can cheap out here. - - # TODO(erikrose): Go through enough of the motions in the - # capability resolvers to determine which of 2 special-purpose - # classes, NullParameterizableString or NullFormattingString, - # to return, and retire this one. - - # As a NullCallableString, even when provided with a parameter, - # such as t.color(5), we must also still be callable, fe: - # - # >>> t.color(5)('shmoo') - # - # is actually simplified result of NullCallable()() on terminals - # without color support, so turtles all the way down: we return - # another instance. - return NullCallableString() - return args[0] - - -def split_compound(compound): - """Split a possibly compound format string into segments. - - >>> split_compound('bold_underline_bright_blue_on_red') - ['bold', 'underline', 'bright_blue', 'on_red'] - - """ - merged_segs = [] - # These occur only as prefixes, so they can always be merged: - mergeable_prefixes = ['on', 'bright', 'on_bright'] - for s in compound.split('_'): - if merged_segs and merged_segs[-1] in mergeable_prefixes: - merged_segs[-1] += '_' + s - else: - merged_segs.append(s) - return merged_segs - - -def resolve_capability(term, attr): - """Return a Unicode string for the terminal capability ``attr``, - or an empty string if not found, or if terminal is without styling - capabilities. - """ - # Decode sequences as latin1, as they are always 8-bit bytes, so when - # b'\xff' is returned, this must be decoded to u'\xff'. - if not term.does_styling: - return u'' - val = curses.tigetstr(term._sugar.get(attr, attr)) - return u'' if val is None else val.decode('latin1') - - -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``. - """ - # NOTE(erikrose): Does curses automatically exchange red and blue and cyan - # and yellow when a terminal supports setf/setb rather than setaf/setab? - # I'll be blasted if I can find any documentation. The following - # assumes it does. - color_cap = (term._background_color if 'on_' in color else - term._foreground_color) - - # curses constants go up to only 7, so add an offset to get at the - # bright colors at 8-15: - offset = 8 if 'bright_' in color else 0 - base_color = color.rsplit('_', 1)[-1] - if term.number_of_colors == 0: - return NullCallableString() - - attr = 'COLOR_%s' % (base_color.upper(),) - fmt_attr = color_cap(getattr(curses, attr) + offset) - return FormattingString(fmt_attr, term.normal) - - -def resolve_attribute(term, attr): - """Resolve a sugary or plain capability name, color, or compound - formatting name into a *callable* unicode string capability, - ``ParameterizingString`` or ``FormattingString``. - """ - # A simple color, such as `red' or `blue'. - if attr in COLORS: - return resolve_color(term, attr) - - # A direct compoundable, such as `bold' or `on_red'. - if attr in COMPOUNDABLES: - sequence = resolve_capability(term, attr) - return FormattingString(sequence, term.normal) - - # Given `bold_on_red', resolve to ('bold', 'on_red'), RECURSIVE - # call for each compounding section, joined and returned as - # a completed completed FormattingString. - formatters = split_compound(attr) - if all(fmt in COMPOUNDABLES for fmt in formatters): - resolution = (resolve_attribute(term, fmt) for fmt in formatters) - return FormattingString(u''.join(resolution), term.normal) - else: - # otherwise, this is our end-game: given a sequence such as 'csr' - # (change scrolling region), return a ParameterizingString instance, - # that when called, performs and returns the final string after curses - # capability lookup is performed. - tparm_capseq = resolve_capability(term, attr) - if not tparm_capseq: - # and, for special terminals, such as 'screen', provide a Proxy - # ParameterizingString for attributes they do not claim to support, - # but actually do! (such as 'hpa' and 'vpa'). - proxy = get_proxy_string(term, term._sugar.get(attr, attr)) - if proxy is not None: - return proxy - return ParameterizingString(tparm_capseq, term.normal, attr) diff --git a/blessed/keyboard.py b/blessed/keyboard.py deleted file mode 100644 index 280606d..0000000 --- a/blessed/keyboard.py +++ /dev/null @@ -1,276 +0,0 @@ -"This sub-module provides 'keyboard awareness'." - -__author__ = 'Jeff Quast ' -__license__ = 'MIT' - -__all__ = ('Keystroke', 'get_keyboard_codes', 'get_keyboard_sequences',) - -import curses.has_key -import collections -import curses -import sys - -if hasattr(collections, 'OrderedDict'): - OrderedDict = collections.OrderedDict -else: - # python 2.6 requires 3rd party library - import ordereddict - OrderedDict = ordereddict.OrderedDict - -get_curses_keycodes = lambda: dict( - ((keyname, getattr(curses, keyname)) - for keyname in dir(curses) - if keyname.startswith('KEY_')) -) - -# override a few curses constants with easier mnemonics, -# there may only be a 1:1 mapping, so for those who desire -# to use 'KEY_DC' from, perhaps, ported code, recommend -# that they simply compare with curses.KEY_DC. -CURSES_KEYCODE_OVERRIDE_MIXIN = ( - ('KEY_DELETE', curses.KEY_DC), - ('KEY_INSERT', curses.KEY_IC), - ('KEY_PGUP', curses.KEY_PPAGE), - ('KEY_PGDOWN', curses.KEY_NPAGE), - ('KEY_ESCAPE', curses.KEY_EXIT), - ('KEY_SUP', curses.KEY_SR), - ('KEY_SDOWN', curses.KEY_SF), - ('KEY_UP_LEFT', curses.KEY_A1), - ('KEY_UP_RIGHT', curses.KEY_A3), - ('KEY_CENTER', curses.KEY_B2), - ('KEY_BEGIN', curses.KEY_BEG), -) - -# Inject KEY_{names} that we think would be useful, there are no curses -# definitions for the keypad keys. We need keys that generate multibyte -# sequences, though it is useful to have some aliases for basic control -# characters such as TAB. -_lastval = max(get_curses_keycodes().values()) -for key in ('TAB', 'KP_MULTIPLY', 'KP_ADD', 'KP_SEPARATOR', 'KP_SUBTRACT', - 'KP_DECIMAL', 'KP_DIVIDE', 'KP_EQUAL', 'KP_0', 'KP_1', 'KP_2', - 'KP_3', 'KP_4', 'KP_5', 'KP_6', 'KP_7', 'KP_8', 'KP_9'): - _lastval += 1 - setattr(curses, 'KEY_{0}'.format(key), _lastval) - -if sys.version_info[0] == 3: - text_type = str - unichr = chr -else: - text_type = unicode # noqa - - -class Keystroke(text_type): - """A unicode-derived class for describing keyboard input returned by - the ``inkey()`` method of ``Terminal``, which may, at times, be a - multibyte sequence, providing properties ``is_sequence`` as ``True`` - when the string is a known sequence, and ``code``, which returns an - integer value that may be compared against the terminal class attributes - such as ``KEY_LEFT``. - """ - def __new__(cls, ucs='', code=None, name=None): - new = text_type.__new__(cls, ucs) - new._name = name - new._code = code - return new - - @property - def is_sequence(self): - "Whether the value represents a multibyte sequence (bool)." - return self._code is not None - - def __repr__(self): - return self._name is None and text_type.__repr__(self) or self._name - __repr__.__doc__ = text_type.__doc__ - - @property - def name(self): - "String-name of key sequence, such as ``'KEY_LEFT'`` (str)." - return self._name - - @property - def code(self): - "Integer keycode value of multibyte sequence (int)." - return self._code - - -def get_keyboard_codes(): - """get_keyboard_codes() -> dict - - Returns dictionary of (code, name) pairs for curses keyboard constant - values and their mnemonic name. Such as key ``260``, with the value of - its identity, ``KEY_LEFT``. These are derived from the attributes by the - same of the curses module, with the following exceptions: - - * ``KEY_DELETE`` in place of ``KEY_DC`` - * ``KEY_INSERT`` in place of ``KEY_IC`` - * ``KEY_PGUP`` in place of ``KEY_PPAGE`` - * ``KEY_PGDOWN`` in place of ``KEY_NPAGE`` - * ``KEY_ESCAPE`` in place of ``KEY_EXIT`` - * ``KEY_SUP`` in place of ``KEY_SR`` - * ``KEY_SDOWN`` in place of ``KEY_SF`` - """ - keycodes = OrderedDict(get_curses_keycodes()) - keycodes.update(CURSES_KEYCODE_OVERRIDE_MIXIN) - - # invert dictionary (key, values) => (values, key), preferring the - # last-most inserted value ('KEY_DELETE' over 'KEY_DC'). - return dict(zip(keycodes.values(), keycodes.keys())) - - -def _alternative_left_right(term): - """_alternative_left_right(T) -> dict - - Return dict of sequences ``term._cuf1``, and ``term._cub1``, - valued as ``KEY_RIGHT``, ``KEY_LEFT`` when appropriate if available. - - some terminals report a different value for *kcuf1* than *cuf1*, but - actually send the value of *cuf1* for right arrow key (which is - non-destructive space). - """ - keymap = dict() - if term._cuf1 and term._cuf1 != u' ': - keymap[term._cuf1] = curses.KEY_RIGHT - if term._cub1 and term._cub1 != u'\b': - keymap[term._cub1] = curses.KEY_LEFT - return keymap - - -def get_keyboard_sequences(term): - """get_keyboard_sequences(T) -> (OrderedDict) - - Initialize and return a keyboard map and sequence lookup table, - (sequence, constant) from blessings Terminal instance ``term``, - where ``sequence`` is a multibyte input sequence, such as u'\x1b[D', - and ``constant`` is a constant, such as term.KEY_LEFT. The return - value is an OrderedDict instance, with their keys sorted longest-first. - """ - # A small gem from curses.has_key that makes this all possible, - # _capability_names: a lookup table of terminal capability names for - # keyboard sequences (fe. kcub1, key_left), keyed by the values of - # constants found beginning with KEY_ in the main curses module - # (such as KEY_LEFT). - # - # latin1 encoding is used so that bytes in 8-bit range of 127-255 - # have equivalent chr() and unichr() values, so that the sequence - # of a kermit or avatar terminal, for example, remains unchanged - # in its byte sequence values even when represented by unicode. - # - capability_names = curses.has_key._capability_names - sequence_map = dict(( - (seq.decode('latin1'), val) - for (seq, val) in ( - (curses.tigetstr(cap), val) - for (val, cap) in capability_names.items() - ) if seq - ) if term.does_styling else ()) - - sequence_map.update(_alternative_left_right(term)) - sequence_map.update(DEFAULT_SEQUENCE_MIXIN) - - # This is for fast lookup matching of sequences, preferring - # full-length sequence such as ('\x1b[D', KEY_LEFT) - # over simple sequences such as ('\x1b', KEY_EXIT). - return OrderedDict(( - (seq, sequence_map[seq]) for seq in sorted( - sequence_map.keys(), key=len, reverse=True))) - - -def resolve_sequence(text, mapper, codes): - """resolve_sequence(text, mapper, codes) -> Keystroke() - - Returns first matching Keystroke() instance for sequences found in - ``mapper`` beginning with input ``text``, where ``mapper`` is an - OrderedDict of unicode multibyte sequences, such as u'\x1b[D' paired by - their integer value (260), and ``codes`` is a dict of integer values (260) - paired by their mnemonic name, 'KEY_LEFT'. - """ - for sequence, code in mapper.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. - -This goes for many: rxvt, putty, iTerm. - -These "mixins" are used for *all* terminals, regardless of their type. - -Furthermore, curses does not provide sequences sent by the keypad, -at least, it does not provide a way to distinguish between keypad 0 -and numeric 0. -""" -DEFAULT_SEQUENCE_MIXIN = ( - # these common control characters (and 127, ctrl+'?') mapped to - # an application key definition. - (unichr(10), curses.KEY_ENTER), - (unichr(13), curses.KEY_ENTER), - (unichr(8), curses.KEY_BACKSPACE), - (unichr(9), curses.KEY_TAB), - (unichr(27), curses.KEY_EXIT), - (unichr(127), curses.KEY_DC), - - (u"\x1b[A", curses.KEY_UP), - (u"\x1b[B", curses.KEY_DOWN), - (u"\x1b[C", curses.KEY_RIGHT), - (u"\x1b[D", curses.KEY_LEFT), - (u"\x1b[F", curses.KEY_END), - (u"\x1b[H", curses.KEY_HOME), - # not sure where these are from .. please report - (u"\x1b[K", curses.KEY_END), - (u"\x1b[U", curses.KEY_NPAGE), - (u"\x1b[V", curses.KEY_PPAGE), - - # keys sent after term.smkx (keypad_xmit) is emitted, source: - # http://www.xfree86.org/current/ctlseqs.html#PC-Style%20Function%20Keys - # http://fossies.org/linux/rxvt/doc/rxvtRef.html#KeyCodes - # - # keypad, numlock on - (u"\x1bOM", curses.KEY_ENTER), # return - (u"\x1bOj", curses.KEY_KP_MULTIPLY), # * - (u"\x1bOk", curses.KEY_KP_ADD), # + - (u"\x1bOl", curses.KEY_KP_SEPARATOR), # , - (u"\x1bOm", curses.KEY_KP_SUBTRACT), # - - (u"\x1bOn", curses.KEY_KP_DECIMAL), # . - (u"\x1bOo", curses.KEY_KP_DIVIDE), # / - (u"\x1bOX", curses.KEY_KP_EQUAL), # = - (u"\x1bOp", curses.KEY_KP_0), # 0 - (u"\x1bOq", curses.KEY_KP_1), # 1 - (u"\x1bOr", curses.KEY_KP_2), # 2 - (u"\x1bOs", curses.KEY_KP_3), # 3 - (u"\x1bOt", curses.KEY_KP_4), # 4 - (u"\x1bOu", curses.KEY_KP_5), # 5 - (u"\x1bOv", curses.KEY_KP_6), # 6 - (u"\x1bOw", curses.KEY_KP_7), # 7 - (u"\x1bOx", curses.KEY_KP_8), # 8 - (u"\x1bOy", curses.KEY_KP_9), # 9 - - # keypad, numlock off - (u"\x1b[1~", curses.KEY_FIND), # find - (u"\x1b[2~", curses.KEY_IC), # insert (0) - (u"\x1b[3~", curses.KEY_DC), # delete (.), "Execute" - (u"\x1b[4~", curses.KEY_SELECT), # select - (u"\x1b[5~", curses.KEY_PPAGE), # pgup (9) - (u"\x1b[6~", curses.KEY_NPAGE), # pgdown (3) - (u"\x1b[7~", curses.KEY_HOME), # home - (u"\x1b[8~", curses.KEY_END), # end - (u"\x1b[OA", curses.KEY_UP), # up (8) - (u"\x1b[OB", curses.KEY_DOWN), # down (2) - (u"\x1b[OC", curses.KEY_RIGHT), # right (6) - (u"\x1b[OD", curses.KEY_LEFT), # left (4) - (u"\x1b[OF", curses.KEY_END), # end (1) - (u"\x1b[OH", curses.KEY_HOME), # home (7) - - # The vt220 placed F1-F4 above the keypad, in place of actual - # F1-F4 were local functions (hold screen, print screen, - # set up, data/talk, break). - (u"\x1bOP", curses.KEY_F1), - (u"\x1bOQ", curses.KEY_F2), - (u"\x1bOR", curses.KEY_F3), - (u"\x1bOS", curses.KEY_F4), -) diff --git a/blessed/sequences.py b/blessed/sequences.py deleted file mode 100644 index 80649c3..0000000 --- a/blessed/sequences.py +++ /dev/null @@ -1,688 +0,0 @@ -# encoding: utf-8 -" This sub-module provides 'sequence awareness' for blessings." - -__author__ = 'Jeff Quast ' -__license__ = 'MIT' - -__all__ = ('init_sequence_patterns', 'Sequence', 'SequenceTextWrapper',) - -# built-ins -import functools -import textwrap -import warnings -import math -import sys -import re - -# local -from ._binterms import binary_terminals as _BINTERM_UNSUPPORTED - -# 3rd-party -import wcwidth # https://github.com/jquast/wcwidth - -_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.") - -if sys.version_info[0] == 3: - text_type = str -else: - text_type = unicode # noqa - - -def _merge_sequences(inp): - """Merge a list of input sequence patterns for use in a regular expression. - Order by lengthyness (full sequence set precedent over subset), - and exclude any empty (u'') sequences. - """ - return sorted(list(filter(None, inp)), key=len, reverse=True) - - -def _build_numeric_capability(term, cap, optional=False, - base_num=99, nparams=1): - """ Build regexp from capabilities having matching numeric - parameter contained within termcap value: n->(\d+). - """ - _cap = getattr(term, cap) - opt = '?' if optional else '' - if _cap: - args = (base_num,) * nparams - cap_re = re.escape(_cap(*args)) - for num in range(base_num - 1, base_num + 2): - # search for matching ascii, n-1 through n+1 - if str(num) in cap_re: - # modify & return n to matching digit expression - cap_re = cap_re.replace(str(num), r'(\d+)%s' % (opt,)) - return cap_re - warnings.warn('Unknown parameter in %r (%r, %r)' % (cap, _cap, cap_re)) - return None # no such capability - - -def _build_any_numeric_capability(term, cap, num=99, nparams=1): - """ Build regexp from capabilities having *any* digit parameters - (substitute matching \d with pattern \d and return). - """ - _cap = getattr(term, cap) - if _cap: - cap_re = re.escape(_cap(*((num,) * nparams))) - cap_re = re.sub('(\d+)', r'(\d+)', cap_re) - if r'(\d+)' in cap_re: - return cap_re - warnings.warn('Missing numerics in %r, %r' % (cap, cap_re)) - return None # no such capability - - -def get_movement_sequence_patterns(term): - """ Build and return set of regexp for capabilities of ``term`` known - to cause movement. - """ - bnc = functools.partial(_build_numeric_capability, term) - - return set([ - # carriage_return - re.escape(term.cr), - # column_address: Horizontal position, absolute - bnc(cap='hpa'), - # row_address: Vertical position #1 absolute - bnc(cap='vpa'), - # cursor_address: Move to row #1 columns #2 - bnc(cap='cup', nparams=2), - # cursor_down: Down one line - re.escape(term.cud1), - # cursor_home: Home cursor (if no cup) - re.escape(term.home), - # cursor_left: Move left one space - re.escape(term.cub1), - # cursor_right: Non-destructive space (move right one space) - re.escape(term.cuf1), - # cursor_up: Up one line - re.escape(term.cuu1), - # param_down_cursor: Down #1 lines - bnc(cap='cud', optional=True), - # restore_cursor: Restore cursor to position of last save_cursor - re.escape(term.rc), - # clear_screen: clear screen and home cursor - re.escape(term.clear), - # enter/exit_fullscreen: switch to alternate screen buffer - re.escape(term.enter_fullscreen), - re.escape(term.exit_fullscreen), - # forward cursor - term._cuf, - # backward cursor - term._cub, - ]) - - -def get_wontmove_sequence_patterns(term): - """ Build and return set of regexp for capabilities of ``term`` known - not to cause any movement. - """ - bnc = functools.partial(_build_numeric_capability, term) - bna = functools.partial(_build_any_numeric_capability, term) - - return list([ - # print_screen: Print contents of screen - re.escape(term.mc0), - # prtr_off: Turn off printer - re.escape(term.mc4), - # prtr_on: Turn on printer - re.escape(term.mc5), - # save_cursor: Save current cursor position (P) - re.escape(term.sc), - # set_tab: Set a tab in every row, current columns - re.escape(term.hts), - # enter_bold_mode: Turn on bold (extra bright) mode - re.escape(term.bold), - # enter_standout_mode - re.escape(term.standout), - # enter_subscript_mode - re.escape(term.subscript), - # enter_superscript_mode - re.escape(term.superscript), - # enter_underline_mode: Begin underline mode - re.escape(term.underline), - # enter_blink_mode: Turn on blinking - re.escape(term.blink), - # enter_dim_mode: Turn on half-bright mode - re.escape(term.dim), - # cursor_invisible: Make cursor invisible - re.escape(term.civis), - # cursor_visible: Make cursor very visible - re.escape(term.cvvis), - # cursor_normal: Make cursor appear normal (undo civis/cvvis) - re.escape(term.cnorm), - # clear_all_tabs: Clear all tab stops - re.escape(term.tbc), - # change_scroll_region: Change region to line #1 to line #2 - bnc(cap='csr', nparams=2), - # clr_bol: Clear to beginning of line - re.escape(term.el1), - # clr_eol: Clear to end of line - re.escape(term.el), - # clr_eos: Clear to end of screen - re.escape(term.clear_eos), - # delete_character: Delete character - re.escape(term.dch1), - # delete_line: Delete line (P*) - re.escape(term.dl1), - # erase_chars: Erase #1 characters - bnc(cap='ech'), - # insert_line: Insert line (P*) - re.escape(term.il1), - # parm_dch: Delete #1 characters - bnc(cap='dch'), - # parm_delete_line: Delete #1 lines - bnc(cap='dl'), - # exit_alt_charset_mode: End alternate character set (P) - re.escape(term.rmacs), - # exit_am_mode: Turn off automatic margins - re.escape(term.rmam), - # exit_attribute_mode: Turn off all attributes - re.escape(term.sgr0), - # exit_ca_mode: Strings to end programs using cup - re.escape(term.rmcup), - # exit_insert_mode: Exit insert mode - re.escape(term.rmir), - # exit_standout_mode: Exit standout mode - re.escape(term.rmso), - # exit_underline_mode: Exit underline mode - re.escape(term.rmul), - # flash_hook: Flash switch hook - re.escape(term.hook), - # flash_screen: Visible bell (may not move cursor) - re.escape(term.flash), - # keypad_local: Leave 'keyboard_transmit' mode - re.escape(term.rmkx), - # keypad_xmit: Enter 'keyboard_transmit' mode - re.escape(term.smkx), - # meta_off: Turn off meta mode - re.escape(term.rmm), - # meta_on: Turn on meta mode (8th-bit on) - re.escape(term.smm), - # orig_pair: Set default pair to its original value - re.escape(term.op), - # parm_ich: Insert #1 characters - bnc(cap='ich'), - # parm_index: Scroll forward #1 - bnc(cap='indn'), - # parm_insert_line: Insert #1 lines - bnc(cap='il'), - # erase_chars: Erase #1 characters - bnc(cap='ech'), - # parm_rindex: Scroll back #1 lines - bnc(cap='rin'), - # parm_up_cursor: Up #1 lines - bnc(cap='cuu'), - # scroll_forward: Scroll text up (P) - re.escape(term.ind), - # scroll_reverse: Scroll text down (P) - re.escape(term.rev), - # tab: Tab to next 8-space hardware tab stop - re.escape(term.ht), - # set_a_background: Set background color to #1, using ANSI escape - bna(cap='setab', num=1), - bna(cap='setab', num=(term.number_of_colors - 1)), - # set_a_foreground: Set foreground color to #1, using ANSI escape - bna(cap='setaf', num=1), - bna(cap='setaf', num=(term.number_of_colors - 1)), - ] + [ - # set_attributes: Define video attributes #1-#9 (PG9) - # ( not *exactly* legal, being extra forgiving. ) - bna(cap='sgr', nparams=_num) for _num in range(1, 10) - # reset_{1,2,3}string: Reset string - ] + list(map(re.escape, (term.r1, term.r2, term.r3,)))) - - -def init_sequence_patterns(term): - """Given a Terminal instance, ``term``, this function processes - and parses several known terminal capabilities, and builds and - returns a dictionary database of regular expressions, which may - be re-attached to the terminal by attributes of the same key-name: - - ``_re_will_move`` - any sequence matching this pattern will cause the terminal - cursor to move (such as *term.home*). - - ``_re_wont_move`` - any sequence matching this pattern will not cause the cursor - to move (such as *term.bold*). - - ``_re_cuf`` - regular expression that matches term.cuf(N) (move N characters forward), - or None if temrinal is without cuf sequence. - - ``_cuf1`` - *term.cuf1* sequence (cursor forward 1 character) as a static value. - - ``_re_cub`` - regular expression that matches term.cub(N) (move N characters backward), - or None if terminal is without cub sequence. - - ``_cub1`` - *term.cuf1* sequence (cursor backward 1 character) as a static value. - - These attributes make it possible to perform introspection on strings - containing sequences generated by this terminal, to determine the - printable length of a string. - """ - if term.kind in _BINTERM_UNSUPPORTED: - warnings.warn(_BINTERM_UNSUPPORTED_MSG.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)) - - # 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 += [ - # some last-ditch match efforts; well, xterm and aixterm is going - # to throw \x1b(B and other oddities all around, so, when given - # input such as ansi art (see test using wall.ans), and well, - # theres no reason a vt220 terminal shouldn't be able to recognize - # blue_on_red, even if it didn't cause it to be generated. these - # are final "ok, i will match this, anyway" - re.escape(u'\x1b') + r'\[(\d+)m', - re.escape(u'\x1b') + r'\[(\d+)\;(\d+)m', - re.escape(u'\x1b') + r'\[(\d+)\;(\d+)\;(\d+)m', - re.escape(u'\x1b') + r'\[(\d+)\;(\d+)\;(\d+)\;(\d+)m', - re.escape(u'\x1b(B'), - ] - - # compile as regular expressions, OR'd. - _re_will_move = re.compile('(%s)' % ('|'.join(_will_move))) - _re_wont_move = re.compile('(%s)' % ('|'.join(_wont_move))) - - # static pattern matching for horizontal_distance(ucs, term) - bnc = functools.partial(_build_numeric_capability, term) - - # parm_right_cursor: Move #1 characters to the right - _cuf = bnc(cap='cuf', optional=True) - _re_cuf = re.compile(_cuf) if _cuf else None - - # cursor_right: Non-destructive space (move right one space) - _cuf1 = term.cuf1 - - # parm_left_cursor: Move #1 characters to the left - _cub = bnc(cap='cub', optional=True) - _re_cub = re.compile(_cub) if _cub else None - - # cursor_left: Move left one space - _cub1 = term.cub1 - - return {'_re_will_move': _re_will_move, - '_re_wont_move': _re_wont_move, - '_re_cuf': _re_cuf, - '_re_cub': _re_cub, - '_cuf1': _cuf1, - '_cub1': _cub1, } - - -class SequenceTextWrapper(textwrap.TextWrapper): - def __init__(self, width, term, **kwargs): - 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. - """ - lines = [] - if self.width <= 0 or not isinstance(self.width, int): - raise ValueError("invalid width %r(%s) (must be integer > 0)" % ( - self.width, type(self.width))) - term = self.term - drop_whitespace = not hasattr(self, 'drop_whitespace' - ) or self.drop_whitespace - chunks.reverse() - while chunks: - cur_line = [] - cur_len = 0 - if lines: - indent = self.subsequent_indent - else: - indent = self.initial_indent - width = self.width - len(indent) - if drop_whitespace and ( - Sequence(chunks[-1], term).strip() == '' and lines): - del chunks[-1] - while chunks: - chunk_len = Sequence(chunks[-1], term).length() - if cur_len + chunk_len <= width: - cur_line.append(chunks.pop()) - cur_len += chunk_len - else: - break - if chunks and Sequence(chunks[-1], term).length() > width: - self._handle_long_word(chunks, cur_line, cur_len, width) - if drop_whitespace and ( - cur_line and Sequence(cur_line[-1], term).strip() == ''): - del cur_line[-1] - if cur_line: - lines.append(indent + u''.join(cur_line)) - return lines - - def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width): - """_handle_long_word(chunks : [string], - cur_line : [string], - cur_len : int, width : int) - - Handle a chunk of text (most likely a word, not whitespace) that - is too long to fit in any line. - """ - # Figure out when indent is larger than the specified width, and make - # sure at least one character is stripped off on every pass - if width < 1: - space_left = 1 - else: - space_left = width - cur_len - - # If we're allowed to break long words, then do so: put as much - # of the next chunk onto the current line as will fit. - if self.break_long_words: - term = self.term - chunk = reversed_chunks[-1] - nxt = 0 - for idx in range(0, len(chunk)): - if idx == nxt: - # at sequence, point beyond it, - nxt = idx + measure_length(chunk[idx:], term) - if nxt <= idx: - # point beyond next sequence, if any, - # otherwise point to next character - nxt = idx + measure_length(chunk[idx:], term) + 1 - if Sequence(chunk[:nxt], term).length() > space_left: - break - else: - # our text ends with a sequence, such as in text - # u'!\x1b(B\x1b[m', set index at at end (nxt) - idx = nxt - - cur_line.append(chunk[:idx]) - reversed_chunks[-1] = chunk[idx:] - - # Otherwise, we have to preserve the long word intact. Only add - # it to the current line if there's nothing already there -- - # that minimizes how much we violate the width constraint. - elif not cur_line: - cur_line.append(reversed_chunks.pop()) - - # If we're not allowed to break long words, and there's already - # text on the current line, do nothing. Next time through the - # main loop of _wrap_chunks(), we'll wind up here again, but - # cur_len will be zero, so the next line will be entirely - # devoted to the long word that we can't handle right now. - - -SequenceTextWrapper.__doc__ = textwrap.TextWrapper.__doc__ - - -class Sequence(text_type): - """ - This unicode-derived class understands the effect of escape sequences - of printable length, allowing a properly implemented .rjust(), .ljust(), - .center(), and .len() - """ - - def __new__(cls, sequence_text, term): - """Sequence(sequence_text, term) -> unicode object - - :arg sequence_text: A string containing sequences. - :arg term: Terminal instance this string was created with. - """ - new = text_type.__new__(cls, sequence_text) - new._term = term - return new - - def ljust(self, width, fillchar=u' '): - """S.ljust(width, fillchar) -> unicode - - 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))) - return u''.join((self, rightside)) - - def rjust(self, width, fillchar=u' '): - """S.rjust(width, fillchar=u'') -> unicode - - 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))) - return u''.join((leftside, self)) - - def center(self, width, fillchar=u' '): - """S.center(width, fillchar=u'') -> unicode - - Returns string derived from unicode string ``S``, centered - and surrounded with whitespace padding ``fillchar``.""" - split = max(0.0, float(width) - self.length()) / 2 - leftside = fillchar * int((max(0.0, math.floor(split))) - / 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. - - Strings containing ``term.left`` or ``\b`` will cause "overstrike", - but a length less than 0 is not ever returned. So ``_\b+`` is a - length of 1 (``+``), but ``\b`` is simply a length of 0. - - Some characters may consume more than one cell, mainly those CJK - Unified Ideographs (Chinese, Japanese, Korean) defined by Unicode - as half or full-width characters. - - For example: - >>> from 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 - - Return a copy of the string S with terminal sequences removed, and - leading and trailing whitespace removed. - - If chars is given and not None, remove characters in chars instead. - """ - return self.strip_seqs().strip(chars) - - def lstrip(self, chars=None): - """S.lstrip([chars]) -> unicode - - Return a copy of the string S with terminal sequences and leading - whitespace removed. - - If chars is given and not None, remove characters in chars instead. - """ - return self.strip_seqs().lstrip(chars) - - def rstrip(self, chars=None): - """S.rstrip([chars]) -> unicode - - Return a copy of the string S with terminal sequences and trailing - whitespace removed. - - If chars is given and not None, remove characters in chars instead. - """ - return self.strip_seqs().rstrip(chars) - - def strip_seqs(self): - """S.strip_seqs() -> unicode - - Return a string without sequences for a string that contains - sequences for the Terminal with which they were created. - - Where sequence ``move_right(n)`` is detected, it is replaced with - ``n * u' '``, and where ``move_left()`` or ``\\b`` is detected, - those last-most characters are destroyed. - - 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' - """ - # nxt: points to first character beyond current escape sequence. - # width: currently estimated display length. - input = self.padd() - outp = u'' - nxt = 0 - for idx in range(0, len(input)): - if idx == nxt: - # at sequence, point beyond it, - nxt = idx + measure_length(input[idx:], self._term) - if nxt <= idx: - # append non-sequence to outp, - outp += input[idx] - # point beyond next sequence, if any, - # otherwise point to next character - nxt = idx + measure_length(input[idx:], self._term) + 1 - return outp - - def padd(self): - """S.padd() -> unicode - Make non-destructive space or backspace into destructive ones. - - Where sequence ``move_right(n)`` is detected, it is replaced with - ``n * u' '``. Where sequence ``move_left(n)`` or ``\\b`` is - detected, those last-most characters are destroyed. - """ - outp = u'' - nxt = 0 - for idx in range(0, text_type.__len__(self)): - width = horizontal_distance(self[idx:], self._term) - if width != 0: - nxt = idx + measure_length(self[idx:], self._term) - if width > 0: - outp += u' ' * width - elif width < 0: - outp = outp[:width] - if nxt <= idx: - outp += self[idx] - nxt = idx + 1 - return outp - - -def measure_length(ucs, term): - """measure_length(S, term) -> int - - Returns non-zero for string ``S`` that begins with a terminal sequence, - that is: the width of the first unprintable sequence found in S. For use - as a *next* pointer to skip past sequences. If string ``S`` is not a - sequence, 0 is returned. - - A sequence may be a typical terminal sequence beginning with Escape - (``\x1b``), especially a Control Sequence Initiator (``CSI``, ``\x1b[``, - ...), or those of ``\a``, ``\b``, ``\r``, ``\n``, ``\xe0`` (shift in), - ``\x0f`` (shift out). They do not necessarily have to begin with CSI, they - need only match the capabilities of attributes ``_re_will_move`` and - ``_re_wont_move`` of terminal ``term``. - """ - - # simple terminal control characters, - ctrl_seqs = u'\a\b\r\n\x0e\x0f' - - if any([ucs.startswith(_ch) for _ch in ctrl_seqs]): - return 1 - - # known multibyte sequences, - matching_seq = term and ( - term._re_will_move.match(ucs) or - term._re_wont_move.match(ucs) or - term._re_cub and term._re_cub.match(ucs) or - term._re_cuf and term._re_cuf.match(ucs) - ) - - if matching_seq: - start, end = matching_seq.span() - return end - - # none found, must be printable! - return 0 - - -def termcap_distance(ucs, cap, unit, term): - """termcap_distance(S, cap, unit, term) -> int - - Match horizontal distance by simple ``cap`` capability name, ``cub1`` or - ``cuf1``, with string matching the sequences identified by Terminal - instance ``term`` and a distance of ``unit`` *1* or *-1*, for right and - left, respectively. - - Otherwise, by regular expression (using dynamic regular expressions built - using ``cub(n)`` and ``cuf(n)``. Failing that, any of the standard SGR - sequences (``\033[C``, ``\033[D``, ``\033[nC``, ``\033[nD``). - - Returns 0 if unmatched. - """ - assert cap in ('cuf', 'cub') - # match cub1(left), cuf1(right) - one = getattr(term, '_%s1' % (cap,)) - if one and ucs.startswith(one): - return unit - - # match cub(n), cuf(n) using regular expressions - re_pattern = getattr(term, '_re_%s' % (cap,)) - _dist = re_pattern and re_pattern.match(ucs) - if _dist: - return unit * int(_dist.group(1)) - - return 0 - - -def horizontal_distance(ucs, term): - """horizontal_distance(S, term) -> int - - Returns Integer ```` in SGR sequence of form ``[C`` - (T.move_right(n)), or ``-(n)`` in sequence of form ``[D`` - (T.move_left(n)). Returns -1 for backspace (0x08), Otherwise 0. - - Tabstop (``\t``) cannot be correctly calculated, as the relative column - position cannot be determined: 8 is always (and, incorrectly) returned. - """ - - if ucs.startswith('\b'): - return -1 - - elif ucs.startswith('\t'): - # As best as I can prove it, a tabstop is always 8 by default. - # Though, given that blessings is: - # - # 1. unaware of the output device's current cursor position, and - # 2. unaware of the location the callee may chose to output any - # given string, - # - # It is not possible to determine how many cells any particular - # \t would consume on the output device! - return 8 - - return (termcap_distance(ucs, 'cub', -1, term) or - termcap_distance(ucs, 'cuf', 1, term) or - 0) diff --git a/blessed/terminal.py b/blessed/terminal.py deleted file mode 100644 index 60fecbe..0000000 --- a/blessed/terminal.py +++ /dev/null @@ -1,783 +0,0 @@ -"This primary module provides the Terminal class." -# standard modules -import collections -import contextlib -import functools -import warnings -import platform -import codecs -import curses -import locale -import select -import struct -import time -import sys -import os - -try: - import termios - import fcntl - import tty -except ImportError: - tty_methods = ('setraw', 'cbreak', 'kbhit', 'height', 'width') - msg_nosupport = ( - "One or more of the modules: 'termios', 'fcntl', and 'tty' " - "are not found on your platform '{0}'. The following methods " - "of Terminal are dummy/no-op unless a deriving class overrides " - "them: {1}".format(sys.platform.lower(), ', '.join(tty_methods))) - warnings.warn(msg_nosupport) - HAS_TTY = False -else: - HAS_TTY = True - -try: - from io import UnsupportedOperation as IOUnsupportedOperation -except ImportError: - class IOUnsupportedOperation(Exception): - """A dummy exception to take the place of Python 3's - ``io.UnsupportedOperation`` in Python 2.5""" - -try: - _ = InterruptedError - del _ -except NameError: - # alias py2 exception to py3 - InterruptedError = select.error - -# local imports -from .formatters import ( - ParameterizingString, - NullCallableString, - resolve_capability, - resolve_attribute, -) - -from .sequences import ( - init_sequence_patterns, - SequenceTextWrapper, - Sequence, -) - -from .keyboard import ( - get_keyboard_sequences, - get_keyboard_codes, - resolve_sequence, -) - - -class Terminal(object): - """A wrapper for curses and related terminfo(5) terminal capabilities. - - Instance attributes: - - ``stream`` - The stream the terminal outputs to. It's convenient to pass the stream - around with the terminal; it's almost always needed when the terminal - is and saves sticking lots of extra args on client functions in - practice. - """ - - #: Sugary names for commonly-used capabilities - _sugar = dict( - save='sc', - restore='rc', - # 'clear' clears the whole screen. - clear_eol='el', - clear_bol='el1', - clear_eos='ed', - position='cup', # deprecated - enter_fullscreen='smcup', - exit_fullscreen='rmcup', - move='cup', - move_x='hpa', - move_y='vpa', - move_left='cub1', - move_right='cuf1', - move_up='cuu1', - move_down='cud1', - hide_cursor='civis', - normal_cursor='cnorm', - reset_colors='op', # oc doesn't work on my OS X terminal. - normal='sgr0', - reverse='rev', - italic='sitm', - no_italic='ritm', - shadow='sshm', - no_shadow='rshm', - standout='smso', - no_standout='rmso', - subscript='ssubm', - no_subscript='rsubm', - superscript='ssupm', - no_superscript='rsupm', - underline='smul', - no_underline='rmul') - - def __init__(self, kind=None, stream=None, force_styling=False): - """Initialize the terminal. - - If ``stream`` is not a tty, I will default to returning an empty - Unicode string for all capability values, so things like piping your - output to a file won't strew escape sequences all over the place. The - ``ls`` command sets a precedent for this: it defaults to columnar - output when being sent to a tty and one-item-per-line when not. - - :arg kind: A terminal string as taken by ``setupterm()``. Defaults to - the value of the ``TERM`` environment variable. - :arg stream: A file-like object representing the terminal. Defaults to - the original value of stdout, like ``curses.initscr()`` does. - :arg force_styling: Whether to force the emission of capabilities, even - if we don't seem to be in a terminal. This comes in handy if users - are trying to pipe your output through something like ``less -r``, - which supports terminal codes just fine but doesn't appear itself - to be a terminal. Just expose a command-line option, and set - ``force_styling`` based on it. Terminal initialization sequences - will be sent to ``stream`` if it has a file descriptor and to - ``sys.__stdout__`` otherwise. (``setupterm()`` demands to send them - somewhere, and stdout is probably where the output is ultimately - headed. If not, stderr is probably bound to the same terminal.) - - If you want to force styling to not happen, pass - ``force_styling=None``. - - """ - global _CUR_TERM - self.keyboard_fd = None - - # default stream is stdout, keyboard only valid as stdin when - # output stream is stdout and output stream is a tty - if stream is None or stream == sys.__stdout__: - stream = sys.__stdout__ - self.keyboard_fd = sys.__stdin__.fileno() - - try: - stream_fd = (stream.fileno() if hasattr(stream, 'fileno') - and callable(stream.fileno) else None) - except IOUnsupportedOperation: - stream_fd = None - - self._is_a_tty = stream_fd is not None and os.isatty(stream_fd) - self._does_styling = ((self.is_a_tty or force_styling) and - force_styling is not None) - - # keyboard_fd only non-None if both stdin and stdout is a tty. - self.keyboard_fd = (self.keyboard_fd - if self.keyboard_fd is not None and - self.is_a_tty and os.isatty(self.keyboard_fd) - else None) - self._normal = None # cache normal attr, preventing recursive lookups - - # The descriptor to direct terminal initialization sequences to. - # sys.__stdout__ seems to always have a descriptor of 1, even if output - # is redirected. - self._init_descriptor = (stream_fd is None and sys.__stdout__.fileno() - or stream_fd) - self._kind = kind or os.environ.get('TERM', 'unknown') - - if self.does_styling: - # Make things like tigetstr() work. Explicit args make setupterm() - # work even when -s is passed to nosetests. Lean toward sending - # init sequences to the stream if it has a file descriptor, and - # send them to stdout as a fallback, since they have to go - # somewhere. - try: - if (platform.python_implementation() == 'PyPy' and - isinstance(self._kind, unicode)): - # pypy/2.4.0_2/libexec/lib_pypy/_curses.py, line 1131 - # TypeError: initializer for ctype 'char *' must be a str - curses.setupterm(self._kind.encode('ascii'), self._init_descriptor) - else: - curses.setupterm(self._kind, self._init_descriptor) - except curses.error as err: - warnings.warn('Failed to setupterm(kind={0!r}): {1}' - .format(self._kind, err)) - self._kind = None - self._does_styling = False - else: - if _CUR_TERM is None or self._kind == _CUR_TERM: - _CUR_TERM = self._kind - else: - warnings.warn( - 'A terminal of kind "%s" has been requested; due to an' - ' internal python curses bug, terminal capabilities' - ' for a terminal of kind "%s" will continue to be' - ' returned for the remainder of this process.' % ( - self._kind, _CUR_TERM,)) - - for re_name, re_val in init_sequence_patterns(self).items(): - setattr(self, re_name, re_val) - - # build database of int code <=> KEY_NAME - self._keycodes = get_keyboard_codes() - - # store attributes as: self.KEY_NAME = code - for key_code, key_name in self._keycodes.items(): - setattr(self, key_name, key_code) - - # build database of sequence <=> KEY_NAME - self._keymap = get_keyboard_sequences(self) - - self._keyboard_buf = collections.deque() - if self.keyboard_fd is not None: - locale.setlocale(locale.LC_ALL, '') - self._encoding = locale.getpreferredencoding() or 'ascii' - try: - self._keyboard_decoder = codecs.getincrementaldecoder( - self._encoding)() - except LookupError as err: - warnings.warn('%s, fallback to ASCII for keyboard.' % (err,)) - self._encoding = 'ascii' - self._keyboard_decoder = codecs.getincrementaldecoder( - self._encoding)() - - self.stream = stream - - def __getattr__(self, attr): - """Return a terminal capability as Unicode string. - - For example, ``term.bold`` is a unicode string that may be prepended - to text to set the video attribute for bold, which should also be - terminated with the pairing ``term.normal``. - - This capability is also callable, so you can use ``term.bold("hi")`` - which results in the joining of (term.bold, "hi", term.normal). - - Compound formatters may also be used, for example: - ``term.bold_blink_red_on_green("merry x-mas!")``. - - For a parametrized capability such as ``cup`` (cursor_address), pass - the parameters as arguments ``some_term.cup(line, column)``. See - manual page terminfo(5) for a complete list of capabilities. - """ - if not self.does_styling: - return NullCallableString() - val = resolve_attribute(self, attr) - # Cache capability codes. - setattr(self, attr, val) - return val - - @property - def kind(self): - """Name of this terminal type as string.""" - return self._kind - - @property - def does_styling(self): - """Whether this instance will emit terminal sequences (bool).""" - return self._does_styling - - @property - def is_a_tty(self): - """Whether the ``stream`` associated with this instance is a terminal - (bool).""" - return self._is_a_tty - - @property - def height(self): - """T.height -> int - - The height of the terminal in characters. - """ - return self._height_and_width().ws_row - - @property - def width(self): - """T.width -> int - - The width of the terminal in characters. - """ - return self._height_and_width().ws_col - - @staticmethod - def _winsize(fd): - """T._winsize -> WINSZ(ws_row, ws_col, ws_xpixel, ws_ypixel) - - The tty connected by file desriptor fd is queried for its window size, - and returned as a collections.namedtuple instance WINSZ. - - May raise exception IOError. - """ - if HAS_TTY: - data = fcntl.ioctl(fd, termios.TIOCGWINSZ, WINSZ._BUF) - return WINSZ(*struct.unpack(WINSZ._FMT, data)) - return WINSZ(ws_row=24, ws_col=80, ws_xpixel=0, ws_ypixel=0) - - def _height_and_width(self): - """Return a tuple of (terminal height, terminal width). - """ - # TODO(jquast): hey kids, even if stdout is redirected to a file, - # we can still query sys.__stdin__.fileno() for our terminal size. - # -- of course, if both are redirected, we have no use for this fd. - for fd in (self._init_descriptor, sys.__stdout__): - try: - if fd is not None: - return self._winsize(fd) - except IOError: - pass - - return WINSZ(ws_row=int(os.getenv('LINES', '25')), - ws_col=int(os.getenv('COLUMNS', '80')), - ws_xpixel=None, - ws_ypixel=None) - - @contextlib.contextmanager - def location(self, x=None, y=None): - """Return a context manager for temporarily moving the cursor. - - Move the cursor to a certain position on entry, let you print stuff - there, then return the cursor to its original position:: - - term = Terminal() - with term.location(2, 5): - print 'Hello, world!' - for x in xrange(10): - print 'I can do it %i times!' % x - - Specify ``x`` to move to a certain column, ``y`` to move to a certain - row, both, or neither. If you specify neither, only the saving and - restoration of cursor position will happen. This can be useful if you - simply want to restore your place after doing some manual cursor - movement. - - """ - # Save position and move to the requested column, row, or both: - self.stream.write(self.save) - if x is not None and y is not None: - self.stream.write(self.move(y, x)) - elif x is not None: - self.stream.write(self.move_x(x)) - elif y is not None: - self.stream.write(self.move_y(y)) - try: - yield - finally: - # Restore original cursor position: - self.stream.write(self.restore) - - @contextlib.contextmanager - def fullscreen(self): - """Return a context manager that enters fullscreen mode while inside it - and restores normal mode on leaving. - - Fullscreen mode is characterized by instructing the terminal emulator - to store and save the current screen state (all screen output), switch - to "alternate screen". Upon exiting, the previous screen state is - returned. - - This call may not be tested; only one screen state may be saved at a - time. - """ - self.stream.write(self.enter_fullscreen) - try: - yield - finally: - self.stream.write(self.exit_fullscreen) - - @contextlib.contextmanager - def hidden_cursor(self): - """Return a context manager that hides the cursor upon entering, - and makes it visible again upon exiting.""" - self.stream.write(self.hide_cursor) - try: - yield - finally: - self.stream.write(self.normal_cursor) - - @property - def color(self): - """Returns capability that sets the foreground color. - - The capability is unparameterized until called and passed a number - (0-15), at which point it returns another string which represents a - specific color change. This second string can further be called to - color a piece of text and set everything back to normal afterward. - - :arg num: The number, 0-15, of the color - - """ - if not self.does_styling: - return NullCallableString() - return ParameterizingString(self._foreground_color, - self.normal, 'color') - - @property - def on_color(self): - "Returns capability that sets the background color." - if not self.does_styling: - return NullCallableString() - return ParameterizingString(self._background_color, - self.normal, 'on_color') - - @property - def normal(self): - "Returns sequence that resets video attribute." - if self._normal: - return self._normal - self._normal = resolve_capability(self, 'normal') - return self._normal - - @property - def number_of_colors(self): - """Return the number of colors the terminal supports. - - Common values are 0, 8, 16, 88, and 256. Most commonly - this may be used to test color capabilities at all:: - - if term.number_of_colors: - ...""" - # trim value to 0, as tigetnum('colors') returns -1 if no support, - # -2 if no such capability. - return max(0, self.does_styling and curses.tigetnum('colors') or -1) - - @property - def _foreground_color(self): - return self.setaf or self.setf - - @property - def _background_color(self): - return self.setab or self.setb - - def ljust(self, text, width=None, fillchar=u' '): - """T.ljust(text, [width], [fillchar]) -> unicode - - Return string ``text``, left-justified by printable length ``width``. - Padding is done using the specified fill character (default is a - space). Default ``width`` is the attached terminal's width. ``text`` - may contain terminal sequences.""" - if width is None: - width = self.width - return Sequence(text, self).ljust(width, fillchar) - - def rjust(self, text, width=None, fillchar=u' '): - """T.rjust(text, [width], [fillchar]) -> unicode - - Return string ``text``, right-justified by printable length ``width``. - Padding is done using the specified fill character (default is a - space). Default ``width`` is the attached terminal's width. ``text`` - may contain terminal sequences.""" - if width is None: - width = self.width - return Sequence(text, self).rjust(width, fillchar) - - def center(self, text, width=None, fillchar=u' '): - """T.center(text, [width], [fillchar]) -> unicode - - Return string ``text``, centered by printable length ``width``. - Padding is done using the specified fill character (default is a - space). Default ``width`` is the attached terminal's width. ``text`` - may contain terminal sequences.""" - if width is None: - width = self.width - return Sequence(text, self).center(width, fillchar) - - def length(self, text): - """T.length(text) -> int - - Return the printable length of string ``text``, which may contain - terminal sequences. Strings containing sequences such as 'clear', - which repositions the cursor, does not give accurate results, and - their printable length is evaluated *0*.. - """ - return Sequence(text, self).length() - - def strip(self, text, chars=None): - """T.strip(text) -> unicode - - Return string ``text`` with terminal sequences removed, and leading - and trailing whitespace removed. - """ - return Sequence(text, self).strip(chars) - - def rstrip(self, text, chars=None): - """T.rstrip(text) -> unicode - - Return string ``text`` with terminal sequences and trailing whitespace - removed. - """ - return Sequence(text, self).rstrip(chars) - - def lstrip(self, text, chars=None): - """T.lstrip(text) -> unicode - - Return string ``text`` with terminal sequences and leading whitespace - removed. - """ - return Sequence(text, self).lstrip(chars) - - def strip_seqs(self, text): - """T.strip_seqs(text) -> unicode - - Return string ``text`` stripped only of its sequences. - """ - return Sequence(text, self).strip_seqs() - - def wrap(self, text, width=None, **kwargs): - """T.wrap(text, [width=None, **kwargs ..]) -> list[unicode] - - Wrap paragraphs containing escape sequences ``text`` to the full - ``width`` of Terminal instance *T*, unless ``width`` is specified. - Wrapped by the virtual printable length, irregardless of the video - attribute sequences it may contain, allowing text containing colors, - bold, underline, etc. to be wrapped. - - Returns a list of strings that may contain escape sequences. See - ``textwrap.TextWrapper`` for all available additional kwargs to - customize wrapping behavior such as ``subsequent_indent``. - """ - width = self.width if width is None else width - lines = [] - for line in text.splitlines(): - lines.extend( - (_linewrap for _linewrap in SequenceTextWrapper( - width=width, term=self, **kwargs).wrap(text)) - if line.strip() else (u'',)) - - return lines - - def _next_char(self): - """T._next_char() -> unicode - - Read and decode next byte from keyboard stream. May return u'' - if decoding is not yet complete, or completed unicode character. - Should always return bytes when self._char_is_ready() returns True. - - Implementors of input streams other than os.read() on the stdin fd - should derive and override this method. - """ - assert self.keyboard_fd is not None - byte = os.read(self.keyboard_fd, 1) - return self._keyboard_decoder.decode(byte, final=False) - - def _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. - """ - # Special care is taken to handle a custom SIGWINCH handler, which - # causes select() to be interrupted with errno 4 (EAGAIN) -- - # it is ignored, and a new timeout value is derived from the previous, - # unless timeout becomes negative, because signal handler has blocked - # beyond timeout, then False is returned. Otherwise, when timeout is 0, - # we continue to block indefinitely (default). - stime = time.time() - check_w, check_x, ready_r = [], [], [None, ] - check_r = [self.keyboard_fd] if self.keyboard_fd is not None else [] - - while HAS_TTY and True: - try: - ready_r, ready_w, ready_x = select.select( - check_r, check_w, check_x, timeout) - except InterruptedError: - if not interruptable: - return u'' - if timeout is not None: - # subtract time already elapsed, - timeout -= time.time() - stime - if timeout > 0: - continue - # no time remains after handling exception (rare) - ready_r = [] - break - else: - break - - return False if self.keyboard_fd is None else check_r == ready_r - - @contextlib.contextmanager - def 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 - """ - if HAS_TTY and self.keyboard_fd is not None: - # Save current terminal mode: - save_mode = termios.tcgetattr(self.keyboard_fd) - mode_setter = tty.setraw if raw else tty.setcbreak - mode_setter(self.keyboard_fd, termios.TCSANOW) - try: - yield - finally: - # Restore prior mode: - termios.tcsetattr(self.keyboard_fd, - termios.TCSAFLUSH, - save_mode) - else: - yield - - @contextlib.contextmanager - def keypad(self): - """ - Context manager that enables keypad input (*keyboard_transmit* mode). - - This enables the effect of calling the curses function keypad(3x): - display terminfo(5) capability `keypad_xmit` (smkx) upon entering, - and terminfo(5) capability `keypad_local` (rmkx) upon exiting. - - On an IBM-PC keypad of ttype *xterm*, with numlock off, the - lower-left diagonal key transmits sequence ``\\x1b[F``, ``KEY_END``. - - However, upon entering keypad mode, ``\\x1b[OF`` is transmitted, - translating to ``KEY_LL`` (lower-left key), allowing diagonal - direction keys to be determined. - """ - try: - self.stream.write(self.smkx) - yield - finally: - self.stream.write(self.rmkx) - - def 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') ? - - if timeout is None and self.keyboard_fd is None: - raise NoKeyboard( - 'Waiting for a keystroke on a terminal with no keyboard ' - 'attached and no timeout would take a long time. Add a ' - 'timeout and revise your program logic.') - - def time_left(stime, timeout): - """time_left(stime, timeout) -> float - - Returns time-relative time remaining before ``timeout`` - after time elapsed since ``stime``. - """ - if timeout is not None: - if timeout == 0: - return 0 - return max(0, timeout - (time.time() - stime)) - - resolve = functools.partial(resolve_sequence, - mapper=self._keymap, - codes=self._keycodes) - - stime = time.time() - - # re-buffer previously received keystrokes, - ucs = u'' - while self._keyboard_buf: - ucs += self._keyboard_buf.pop() - - # receive all immediately available bytes - while self._char_is_ready(0): - ucs += self._next_char() - - # decode keystroke, if any - ks = resolve(text=ucs) - - # so long as the most immediately received or buffered keystroke is - # incomplete, (which may be a multibyte encoding), block until until - # one is received. - while not ks and self._char_is_ready(time_left(stime, timeout), - interruptable): - ucs += self._next_char() - ks = resolve(text=ucs) - - # handle escape key (KEY_ESCAPE) vs. escape sequence (which begins - # with KEY_ESCAPE, \x1b[, \x1bO, or \x1b?), up to esc_delay when - # received. This is not optimal, but causes least delay when - # (currently unhandled, and rare) "meta sends escape" is used, - # or when an unsupported sequence is sent. - if ks.code == self.KEY_ESCAPE: - esctime = time.time() - while (ks.code == self.KEY_ESCAPE and - self._char_is_ready(time_left(esctime, esc_delay))): - ucs += self._next_char() - ks = resolve(text=ucs) - - # buffer any remaining text received - self._keyboard_buf.extendleft(ucs[len(ks):]) - return ks - - -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 - -WINSZ = collections.namedtuple('WINSZ', ( - 'ws_row', # /* rows, in characters */ - 'ws_col', # /* columns, in characters */ - 'ws_xpixel', # /* horizontal size, pixels */ - 'ws_ypixel', # /* vertical size, pixels */ -)) -#: format of termios structure -WINSZ._FMT = 'hhhh' -#: buffer of termios structure appropriate for ioctl argument -WINSZ._BUF = '\x00' * struct.calcsize(WINSZ._FMT) diff --git a/blessed/tests/__init__.py b/blessed/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/blessed/tests/accessories.py b/blessed/tests/accessories.py deleted file mode 100644 index 8cb3f5b..0000000 --- a/blessed/tests/accessories.py +++ /dev/null @@ -1,240 +0,0 @@ -# -*- coding: utf-8 -*- -"""Accessories for automated py.test runner.""" -# std -from __future__ import with_statement -import contextlib -import subprocess -import functools -import traceback -import termios -import random -import codecs -import curses -import sys -import pty -import os - -# local -from blessings import Terminal - -# 3rd -import pytest - -if sys.version_info[0] == 3: - text_type = str -else: - text_type = unicode # noqa - -TestTerminal = functools.partial(Terminal, kind='xterm-256color') -SEND_SEMAPHORE = SEMAPHORE = b'SEMAPHORE\n' -RECV_SEMAPHORE = b'SEMAPHORE\r\n' -all_xterms_params = ['xterm', 'xterm-256color'] -many_lines_params = [30, 100] -many_columns_params = [1, 10] -from blessings._binterms import binary_terminals -default_all_terms = ['screen', 'vt220', 'rxvt', 'cons25', 'linux', 'ansi'] -if os.environ.get('TEST_ALLTERMS'): - try: - available_terms = [ - _term.split(None, 1)[0] for _term in - subprocess.Popen(('toe', '-a'), - stdout=subprocess.PIPE, - close_fds=True) - .communicate()[0].splitlines()] - except OSError: - all_terms_params = default_all_terms -else: - available_terms = default_all_terms -all_terms_params = list(set(available_terms) - ( - set(binary_terminals) if not os.environ.get('TEST_BINTERMS') - else set())) or default_all_terms - - -class as_subprocess(object): - """This helper executes test cases in a child process, - avoiding a python-internal bug of _curses: setupterm() - may not be called more than once per process. - """ - _CHILD_PID = 0 - encoding = 'utf8' - - def __init__(self, func): - self.func = func - - def __call__(self, *args, **kwargs): - pid, master_fd = pty.fork() - if pid is self._CHILD_PID: - # child process executes function, raises exception - # if failed, causing a non-zero exit code, using the - # protected _exit() function of ``os``; to prevent the - # 'SystemExit' exception from being thrown. - try: - try: - cov = __import__('cov_core_init').init() - except ImportError: - cov = None - self.func(*args, **kwargs) - except Exception: - e_type, e_value, e_tb = sys.exc_info() - o_err = list() - for line in traceback.format_tb(e_tb): - o_err.append(line.rstrip().encode('utf-8')) - o_err.append(('-=' * 20).encode('ascii')) - o_err.extend([_exc.rstrip().encode('utf-8') for _exc in - traceback.format_exception_only( - e_type, e_value)]) - os.write(sys.__stdout__.fileno(), b'\n'.join(o_err)) - os.close(sys.__stdout__.fileno()) - os.close(sys.__stderr__.fileno()) - os.close(sys.__stdin__.fileno()) - if cov is not None: - cov.stop() - cov.save() - os._exit(1) - else: - if cov is not None: - cov.stop() - cov.save() - os._exit(0) - - exc_output = text_type() - decoder = codecs.getincrementaldecoder(self.encoding)() - while True: - try: - _exc = os.read(master_fd, 65534) - except OSError: - # linux EOF - break - if not _exc: - # bsd EOF - break - exc_output += decoder.decode(_exc) - - # parent process asserts exit code is 0, causing test - # to fail if child process raised an exception/assertion - pid, status = os.waitpid(pid, 0) - os.close(master_fd) - - # Display any output written by child process - # (esp. any AssertionError exceptions written to stderr). - exc_output_msg = 'Output in child process:\n%s\n%s\n%s' % ( - u'=' * 40, exc_output, u'=' * 40,) - assert exc_output == '', exc_output_msg - - # Also test exit status is non-zero - assert os.WEXITSTATUS(status) == 0 - - -def read_until_semaphore(fd, semaphore=RECV_SEMAPHORE, - encoding='utf8', timeout=10): - """Read file descriptor ``fd`` until ``semaphore`` is found. - - Used to ensure the child process is awake and ready. For timing - tests; without a semaphore, the time to fork() would be (incorrectly) - included in the duration of the test, which can be very length on - continuous integration servers (such as Travis-CI). - """ - # note that when a child process writes xyz\\n, the parent - # process will read xyz\\r\\n -- this is how pseudo terminals - # behave; a virtual terminal requires both carriage return and - # line feed, it is only for convenience that \\n does both. - outp = text_type() - decoder = codecs.getincrementaldecoder(encoding)() - semaphore = semaphore.decode('ascii') - while not outp.startswith(semaphore): - try: - _exc = os.read(fd, 1) - except OSError: # Linux EOF - break - if not _exc: # BSD EOF - break - outp += decoder.decode(_exc, final=False) - assert outp.startswith(semaphore), ( - 'Semaphore not recv before EOF ' - '(expected: %r, got: %r)' % (semaphore, outp,)) - return outp[len(semaphore):] - - -def read_until_eof(fd, encoding='utf8'): - """Read file descriptor ``fd`` until EOF. Return decoded string.""" - decoder = codecs.getincrementaldecoder(encoding)() - outp = text_type() - while True: - try: - _exc = os.read(fd, 100) - except OSError: # linux EOF - break - if not _exc: # bsd EOF - break - outp += decoder.decode(_exc, final=False) - return outp - - -@contextlib.contextmanager -def echo_off(fd): - """Ensure any bytes written to pty fd are not duplicated as output.""" - try: - attrs = termios.tcgetattr(fd) - attrs[3] = attrs[3] & ~termios.ECHO - termios.tcsetattr(fd, termios.TCSANOW, attrs) - yield - finally: - attrs[3] = attrs[3] | termios.ECHO - termios.tcsetattr(fd, termios.TCSANOW, attrs) - - -def unicode_cap(cap): - """Return the result of ``tigetstr`` except as Unicode.""" - try: - val = curses.tigetstr(cap) - except curses.error: - val = None - if val: - return val.decode('latin1') - return u'' - - -def unicode_parm(cap, *parms): - """Return the result of ``tparm(tigetstr())`` except as Unicode.""" - try: - cap = curses.tigetstr(cap) - except curses.error: - cap = None - if cap: - try: - val = curses.tparm(cap, *parms) - except curses.error: - val = None - if val: - return val.decode('latin1') - return u'' - - -@pytest.fixture(params=binary_terminals) -def unsupported_sequence_terminals(request): - """Terminals that emit warnings for unsupported sequence-awareness.""" - return request.param - - -@pytest.fixture(params=all_xterms_params) -def xterms(request): - """Common kind values for xterm terminals.""" - return request.param - - -@pytest.fixture(params=all_terms_params) -def all_terms(request): - """Common kind values for all kinds of terminals.""" - return request.param - - -@pytest.fixture(params=many_lines_params) -def many_lines(request): - """Various number of lines for screen height.""" - return request.param - - -@pytest.fixture(params=many_columns_params) -def many_columns(request): - """Various number of columns for screen width.""" - return request.param diff --git a/blessed/tests/test_core.py b/blessed/tests/test_core.py deleted file mode 100644 index ce1e838..0000000 --- a/blessed/tests/test_core.py +++ /dev/null @@ -1,457 +0,0 @@ -# -*- coding: utf-8 -*- -"Core blessings Terminal() tests." - -# std -try: - from StringIO import StringIO -except ImportError: - from io import StringIO -import collections -import warnings -import platform -import locale -import sys -import imp -import os - -# local -from .accessories import ( - as_subprocess, - TestTerminal, - unicode_cap, - all_terms -) - -# 3rd party -import mock -import pytest - - -def test_export_only_Terminal(): - "Ensure only Terminal instance is exported for import * statements." - import blessings - assert blessings.__all__ == ('Terminal',) - - -def test_null_location(all_terms): - "Make sure ``location()`` with no args just does position restoration." - @as_subprocess - def child(kind): - t = TestTerminal(stream=StringIO(), force_styling=True) - with t.location(): - pass - expected_output = u''.join( - (unicode_cap('sc'), unicode_cap('rc'))) - assert (t.stream.getvalue() == expected_output) - - child(all_terms) - - -def test_flipped_location_move(all_terms): - "``location()`` and ``move()`` receive counter-example arguments." - @as_subprocess - def child(kind): - buf = StringIO() - t = TestTerminal(stream=buf, force_styling=True) - y, x = 10, 20 - with t.location(y, x): - xy_val = t.move(x, y) - yx_val = buf.getvalue()[len(t.sc):] - assert xy_val == yx_val - - child(all_terms) - - -def test_yield_keypad(): - "Ensure ``keypad()`` writes keyboard_xmit and keyboard_local." - @as_subprocess - def child(kind): - # given, - t = TestTerminal(stream=StringIO(), force_styling=True) - expected_output = u''.join((t.smkx, t.rmkx)) - - # exercise, - with t.keypad(): - pass - - # verify. - assert (t.stream.getvalue() == expected_output) - - child(kind='xterm') - - -def test_null_fileno(): - "Make sure ``Terminal`` works when ``fileno`` is ``None``." - @as_subprocess - def child(): - # This simulates piping output to another program. - out = StringIO() - out.fileno = None - t = TestTerminal(stream=out) - assert (t.save == u'') - - child() - - -def test_number_of_colors_without_tty(): - "``number_of_colors`` should return 0 when there's no tty." - @as_subprocess - def child_256_nostyle(): - t = TestTerminal(stream=StringIO()) - assert (t.number_of_colors == 0) - - @as_subprocess - def child_256_forcestyle(): - t = TestTerminal(stream=StringIO(), force_styling=True) - assert (t.number_of_colors == 256) - - @as_subprocess - def child_8_forcestyle(): - t = TestTerminal(kind='ansi', stream=StringIO(), - force_styling=True) - assert (t.number_of_colors == 8) - - @as_subprocess - def child_0_forcestyle(): - t = TestTerminal(kind='vt220', stream=StringIO(), - force_styling=True) - assert (t.number_of_colors == 0) - - child_0_forcestyle() - child_8_forcestyle() - child_256_forcestyle() - child_256_nostyle() - - -def test_number_of_colors_with_tty(): - "test ``number_of_colors`` 0, 8, and 256." - @as_subprocess - def child_256(): - t = TestTerminal() - assert (t.number_of_colors == 256) - - @as_subprocess - def child_8(): - t = TestTerminal(kind='ansi') - assert (t.number_of_colors == 8) - - @as_subprocess - def child_0(): - t = TestTerminal(kind='vt220') - assert (t.number_of_colors == 0) - - child_0() - child_8() - child_256() - - -def test_init_descriptor_always_initted(all_terms): - "Test height and width with non-tty Terminals." - @as_subprocess - def child(kind): - t = TestTerminal(kind=kind, stream=StringIO()) - assert t._init_descriptor == sys.__stdout__.fileno() - assert (isinstance(t.height, int)) - assert (isinstance(t.width, int)) - assert t.height == t._height_and_width()[0] - assert t.width == t._height_and_width()[1] - - child(all_terms) - - -def test_force_styling_none(all_terms): - "If ``force_styling=None`` is used, don't ever do styling." - @as_subprocess - def child(kind): - t = TestTerminal(kind=kind, force_styling=None) - assert (t.save == '') - assert (t.color(9) == '') - assert (t.bold('oi') == 'oi') - - child(all_terms) - - -def test_setupterm_singleton_issue33(): - "A warning is emitted if a new terminal ``kind`` is used per process." - @as_subprocess - def child(): - warnings.filterwarnings("error", category=UserWarning) - - # instantiate first terminal, of type xterm-256color - term = TestTerminal(force_styling=True) - - try: - # a second instantiation raises UserWarning - term = TestTerminal(kind="vt220", force_styling=True) - except UserWarning: - err = sys.exc_info()[1] - assert (err.args[0].startswith( - 'A terminal of kind "vt220" has been requested') - ), err.args[0] - assert ('a terminal of kind "xterm-256color" will ' - 'continue to be returned' in err.args[0]), err.args[0] - else: - # unless term is not a tty and setupterm() is not called - assert not term.is_a_tty or False, 'Should have thrown exception' - warnings.resetwarnings() - - child() - - -def test_setupterm_invalid_issue39(): - "A warning is emitted if TERM is invalid." - # https://bugzilla.mozilla.org/show_bug.cgi?id=878089 - - # if TERM is unset, defaults to 'unknown', which should - # fail to lookup and emit a warning, only. - @as_subprocess - def child(): - warnings.filterwarnings("error", category=UserWarning) - - try: - term = TestTerminal(kind='unknown', force_styling=True) - except UserWarning: - err = sys.exc_info()[1] - assert err.args[0] == ( - "Failed to setupterm(kind='unknown'): " - "setupterm: could not find terminal") - else: - assert not term.is_a_tty and not term.does_styling, ( - 'Should have thrown exception') - warnings.resetwarnings() - - child() - - -def test_setupterm_invalid_has_no_styling(): - "An unknown TERM type does not perform styling." - # https://bugzilla.mozilla.org/show_bug.cgi?id=878089 - - # if TERM is unset, defaults to 'unknown', which should - # fail to lookup and emit a warning, only. - @as_subprocess - def child(): - warnings.filterwarnings("ignore", category=UserWarning) - - term = TestTerminal(kind='unknown', force_styling=True) - assert term.kind is None - assert term.does_styling is False - assert term.number_of_colors == 0 - warnings.resetwarnings() - - child() - - -@pytest.mark.skipif(platform.python_implementation() == 'PyPy', - reason='PyPy freezes') -def test_missing_ordereddict_uses_module(monkeypatch): - "ordereddict module is imported when without collections.OrderedDict." - import blessings.keyboard - - if hasattr(collections, 'OrderedDict'): - monkeypatch.delattr('collections.OrderedDict') - - try: - imp.reload(blessings.keyboard) - except ImportError as err: - assert err.args[0] in ("No module named ordereddict", # py2 - "No module named 'ordereddict'") # py3 - sys.modules['ordereddict'] = mock.Mock() - sys.modules['ordereddict'].OrderedDict = -1 - imp.reload(blessings.keyboard) - assert blessings.keyboard.OrderedDict == -1 - del sys.modules['ordereddict'] - monkeypatch.undo() - imp.reload(blessings.keyboard) - else: - assert platform.python_version_tuple() < ('2', '7') # reached by py2.6 - - -@pytest.mark.skipif(platform.python_implementation() == 'PyPy', - reason='PyPy freezes') -def test_python3_2_raises_exception(monkeypatch): - "Test python version 3.0 through 3.2 raises an exception." - import blessings - - monkeypatch.setattr('platform.python_version_tuple', - lambda: ('3', '2', '2')) - - try: - imp.reload(blessings) - except ImportError as err: - assert err.args[0] == ( - 'Blessings needs Python 3.2.3 or greater for Python 3 ' - 'support due to http://bugs.python.org/issue10570.') - monkeypatch.undo() - imp.reload(blessings) - else: - 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 - assert '_' not in dir(blessings.terminal) - - -def test_IOUnsupportedOperation(): - "Ensure stream that throws IOUnsupportedOperation results in non-tty." - @as_subprocess - def child(): - import blessings.terminal - - def side_effect(): - raise blessings.terminal.IOUnsupportedOperation - - mock_stream = mock.Mock() - mock_stream.fileno = side_effect - - term = TestTerminal(stream=mock_stream) - assert term.stream == mock_stream - assert term.does_styling is False - assert term.is_a_tty is False - assert term.number_of_colors is 0 - - child() - - -def test_winsize_IOError_returns_environ(): - """When _winsize raises IOError, defaults from os.environ given.""" - @as_subprocess - def child(): - def side_effect(fd): - raise IOError - - term = TestTerminal() - term._winsize = side_effect - os.environ['COLUMNS'] = '1984' - os.environ['LINES'] = '1888' - assert term._height_and_width() == (1888, 1984, None, None) - - child() - - -def test_yield_fullscreen(all_terms): - "Ensure ``fullscreen()`` writes enter_fullscreen and exit_fullscreen." - @as_subprocess - def child(kind): - t = TestTerminal(stream=StringIO(), force_styling=True) - t.enter_fullscreen = u'BEGIN' - t.exit_fullscreen = u'END' - with t.fullscreen(): - pass - expected_output = u''.join((t.enter_fullscreen, t.exit_fullscreen)) - assert (t.stream.getvalue() == expected_output) - - child(all_terms) - - -def test_yield_hidden_cursor(all_terms): - "Ensure ``hidden_cursor()`` writes hide_cursor and normal_cursor." - @as_subprocess - def child(kind): - t = TestTerminal(stream=StringIO(), force_styling=True) - t.hide_cursor = u'BEGIN' - t.normal_cursor = u'END' - with t.hidden_cursor(): - pass - expected_output = u''.join((t.hide_cursor, t.normal_cursor)) - assert (t.stream.getvalue() == expected_output) - - child(all_terms) - - -def test_no_preferredencoding_fallback_ascii(): - "Ensure empty preferredencoding value defaults to ascii." - @as_subprocess - def child(): - with mock.patch('locale.getpreferredencoding') as get_enc: - get_enc.return_value = u'' - t = TestTerminal() - assert t._encoding == 'ascii' - - child() - - -def test_unknown_preferredencoding_warned_and_fallback_ascii(): - "Ensure a locale without a codecs incrementaldecoder emits a warning." - @as_subprocess - def child(): - with mock.patch('locale.getpreferredencoding') as get_enc: - with warnings.catch_warnings(record=True) as warned: - get_enc.return_value = '---unknown--encoding---' - t = TestTerminal() - assert t._encoding == 'ascii' - assert len(warned) == 1 - assert issubclass(warned[-1].category, UserWarning) - assert "fallback to ASCII" in str(warned[-1].message) - - child() - - -def test_win32_missing_tty_modules(monkeypatch): - "Ensure dummy exception is used when io is without UnsupportedOperation." - @as_subprocess - def child(): - OLD_STYLE = False - try: - original_import = getattr(__builtins__, '__import__') - OLD_STYLE = True - except AttributeError: - original_import = __builtins__['__import__'] - - tty_modules = ('termios', 'fcntl', 'tty') - - def __import__(name, *args, **kwargs): - if name in tty_modules: - raise ImportError - return original_import(name, *args, **kwargs) - - for module in tty_modules: - sys.modules.pop(module, None) - - warnings.filterwarnings("error", category=UserWarning) - try: - if OLD_STYLE: - __builtins__.__import__ = __import__ - else: - __builtins__['__import__'] = __import__ - try: - import blessings.terminal - imp.reload(blessings.terminal) - except UserWarning: - err = sys.exc_info()[1] - assert err.args[0] == blessings.terminal.msg_nosupport - - warnings.filterwarnings("ignore", category=UserWarning) - import blessings.terminal - imp.reload(blessings.terminal) - assert blessings.terminal.HAS_TTY is False - term = blessings.terminal.Terminal('ansi') - assert term.height == 24 - assert term.width == 80 - - finally: - if OLD_STYLE: - setattr(__builtins__, '__import__', original_import) - else: - __builtins__['__import__'] = original_import - warnings.resetwarnings() - import blessings.terminal - imp.reload(blessings.terminal) - - child() diff --git a/blessed/tests/test_formatters.py b/blessed/tests/test_formatters.py deleted file mode 100644 index 3184140..0000000 --- a/blessed/tests/test_formatters.py +++ /dev/null @@ -1,403 +0,0 @@ -# -*- coding: utf-8 -*- -"""Tests string formatting functions.""" -import curses -import mock - - -def test_parameterizing_string_args_unspecified(monkeypatch): - """Test default args of formatters.ParameterizingString.""" - from blessings.formatters import ParameterizingString, FormattingString - # first argument to tparm() is the sequence name, returned as-is; - # subsequent arguments are usually Integers. - tparm = lambda *args: u'~'.join( - arg.decode('latin1') if not num else '%s' % (arg,) - for num, arg in enumerate(args)).encode('latin1') - - monkeypatch.setattr(curses, 'tparm', tparm) - - # given, - pstr = ParameterizingString(u'') - - # excersize __new__ - assert str(pstr) == u'' - assert pstr._normal == u'' - assert pstr._name == u'' - - # excersize __call__ - zero = pstr(0) - assert type(zero) is FormattingString - assert zero == u'~0' - assert zero('text') == u'~0text' - - # excersize __call__ with multiple args - onetwo = pstr(1, 2) - assert type(onetwo) is FormattingString - assert onetwo == u'~1~2' - assert onetwo('text') == u'~1~2text' - - -def test_parameterizing_string_args(monkeypatch): - """Test basic formatters.ParameterizingString.""" - from blessings.formatters import ParameterizingString, FormattingString - - # first argument to tparm() is the sequence name, returned as-is; - # subsequent arguments are usually Integers. - tparm = lambda *args: u'~'.join( - arg.decode('latin1') if not num else '%s' % (arg,) - for num, arg in enumerate(args)).encode('latin1') - - monkeypatch.setattr(curses, 'tparm', tparm) - - # given, - pstr = ParameterizingString(u'cap', u'norm', u'seq-name') - - # excersize __new__ - assert str(pstr) == u'cap' - assert pstr._normal == u'norm' - assert pstr._name == u'seq-name' - - # excersize __call__ - zero = pstr(0) - assert type(zero) is FormattingString - assert zero == u'cap~0' - assert zero('text') == u'cap~0textnorm' - - # excersize __call__ with multiple args - onetwo = pstr(1, 2) - assert type(onetwo) is FormattingString - assert onetwo == u'cap~1~2' - assert onetwo('text') == u'cap~1~2textnorm' - - -def test_parameterizing_string_type_error(monkeypatch): - """Test formatters.ParameterizingString raising TypeError""" - from blessings.formatters import ParameterizingString - - def tparm_raises_TypeError(*args): - raise TypeError('custom_err') - - monkeypatch.setattr(curses, 'tparm', tparm_raises_TypeError) - - # given, - pstr = ParameterizingString(u'cap', u'norm', u'cap-name') - - # ensure TypeError when given a string raises custom exception - try: - pstr('XYZ') - assert False, "previous call should have raised TypeError" - except TypeError as err: - assert (err.args[0] == ( # py3x - "A native or nonexistent capability template, " - "'cap-name' received invalid argument ('XYZ',): " - "custom_err. You probably misspelled a " - "formatting call like `bright_red'") or - err.args[0] == ( - "A native or nonexistent capability template, " - "u'cap-name' received invalid argument ('XYZ',): " - "custom_err. You probably misspelled a " - "formatting call like `bright_red'")) - - # ensure TypeError when given an integer raises its natural exception - try: - pstr(0) - assert False, "previous call should have raised TypeError" - except TypeError as err: - assert err.args[0] == "custom_err" - - -def test_formattingstring(monkeypatch): - """Test formatters.FormattingString""" - from blessings.formatters import FormattingString - - # given, with arg - pstr = FormattingString(u'attr', u'norm') - - # excersize __call__, - assert pstr._normal == u'norm' - assert str(pstr) == u'attr' - assert pstr('text') == u'attrtextnorm' - - # given, without arg - pstr = FormattingString(u'', u'norm') - assert pstr('text') == u'text' - - -def test_nullcallablestring(monkeypatch): - """Test formatters.NullCallableString""" - from blessings.formatters import (NullCallableString) - - # given, with arg - pstr = NullCallableString() - - # excersize __call__, - assert str(pstr) == u'' - assert pstr('text') == u'text' - assert pstr('text', 1) == u'' - assert pstr('text', 'moretext') == u'' - assert pstr(99, 1) == u'' - assert pstr() == u'' - assert pstr(0) == u'' - - -def test_split_compound(): - """Test formatters.split_compound.""" - from blessings.formatters import split_compound - - assert split_compound(u'') == [u''] - assert split_compound(u'a_b_c') == [u'a', u'b', u'c'] - assert split_compound(u'a_on_b_c') == [u'a', u'on_b', u'c'] - assert split_compound(u'a_bright_b_c') == [u'a', u'bright_b', u'c'] - assert split_compound(u'a_on_bright_b_c') == [u'a', u'on_bright_b', u'c'] - - -def test_resolve_capability(monkeypatch): - """Test formatters.resolve_capability and term sugaring """ - from blessings.formatters import resolve_capability - - # given, always returns a b'seq' - tigetstr = lambda attr: ('seq-%s' % (attr,)).encode('latin1') - monkeypatch.setattr(curses, 'tigetstr', tigetstr) - term = mock.Mock() - term._sugar = dict(mnemonic='xyz') - - # excersize - assert resolve_capability(term, 'mnemonic') == u'seq-xyz' - assert resolve_capability(term, 'natural') == u'seq-natural' - - # given, where tigetstr returns None - tigetstr_none = lambda attr: None - monkeypatch.setattr(curses, 'tigetstr', tigetstr_none) - - # excersize, - assert resolve_capability(term, 'natural') == u'' - - # given, where does_styling is False - def raises_exception(*args): - assert False, "Should not be called" - term.does_styling = False - monkeypatch.setattr(curses, 'tigetstr', raises_exception) - - # excersize, - assert resolve_capability(term, 'natural') == u'' - - -def test_resolve_color(monkeypatch): - """Test formatters.resolve_color.""" - from blessings.formatters import (resolve_color, - FormattingString, - NullCallableString) - - color_cap = lambda digit: 'seq-%s' % (digit,) - monkeypatch.setattr(curses, 'COLOR_RED', 1984) - - # given, terminal with color capabilities - term = mock.Mock() - term._background_color = color_cap - term._foreground_color = color_cap - term.number_of_colors = -1 - term.normal = 'seq-normal' - - # excersize, - red = resolve_color(term, 'red') - assert type(red) == FormattingString - assert red == u'seq-1984' - assert red('text') == u'seq-1984textseq-normal' - - # excersize bold, +8 - bright_red = resolve_color(term, 'bright_red') - assert type(bright_red) == FormattingString - assert bright_red == u'seq-1992' - assert bright_red('text') == u'seq-1992textseq-normal' - - # given, terminal without color - term.number_of_colors = 0 - - # excersize, - red = resolve_color(term, 'red') - assert type(red) == NullCallableString - assert red == u'' - assert red('text') == u'text' - - # excesize bold, - bright_red = resolve_color(term, 'bright_red') - assert type(bright_red) == NullCallableString - assert bright_red == u'' - assert bright_red('text') == u'text' - - -def test_resolve_attribute_as_color(monkeypatch): - """ Test simple resolve_attribte() given color name. """ - import blessings - from blessings.formatters import resolve_attribute - - resolve_color = lambda term, digit: 'seq-%s' % (digit,) - COLORS = set(['COLORX', 'COLORY']) - COMPOUNDABLES = set(['JOINT', 'COMPOUND']) - monkeypatch.setattr(blessings.formatters, 'resolve_color', resolve_color) - monkeypatch.setattr(blessings.formatters, 'COLORS', COLORS) - monkeypatch.setattr(blessings.formatters, 'COMPOUNDABLES', COMPOUNDABLES) - term = mock.Mock() - assert resolve_attribute(term, 'COLORX') == u'seq-COLORX' - - -def test_resolve_attribute_as_compoundable(monkeypatch): - """ Test simple resolve_attribte() given a compoundable. """ - import blessings - from blessings.formatters import resolve_attribute, FormattingString - - resolve_cap = lambda term, digit: 'seq-%s' % (digit,) - COMPOUNDABLES = set(['JOINT', 'COMPOUND']) - monkeypatch.setattr(blessings.formatters, - 'resolve_capability', - resolve_cap) - monkeypatch.setattr(blessings.formatters, 'COMPOUNDABLES', COMPOUNDABLES) - term = mock.Mock() - term.normal = 'seq-normal' - - compound = resolve_attribute(term, 'JOINT') - assert type(compound) is FormattingString - assert str(compound) == u'seq-JOINT' - assert compound('text') == u'seq-JOINTtextseq-normal' - - -def test_resolve_attribute_non_compoundables(monkeypatch): - """ Test recursive compounding of resolve_attribute(). """ - import blessings - from blessings.formatters import resolve_attribute, ParameterizingString - uncompoundables = lambda attr: ['split', 'compound'] - resolve_cap = lambda term, digit: 'seq-%s' % (digit,) - monkeypatch.setattr(blessings.formatters, - 'split_compound', - uncompoundables) - monkeypatch.setattr(blessings.formatters, - 'resolve_capability', - resolve_cap) - tparm = lambda *args: u'~'.join( - arg.decode('latin1') if not num else '%s' % (arg,) - for num, arg in enumerate(args)).encode('latin1') - monkeypatch.setattr(curses, 'tparm', tparm) - - term = mock.Mock() - term.normal = 'seq-normal' - - # given - pstr = resolve_attribute(term, 'not-a-compoundable') - assert type(pstr) == ParameterizingString - assert str(pstr) == u'seq-not-a-compoundable' - # this is like calling term.move_x(3) - assert pstr(3) == u'seq-not-a-compoundable~3' - # this is like calling term.move_x(3)('text') - assert pstr(3)('text') == u'seq-not-a-compoundable~3textseq-normal' - - -def test_resolve_attribute_recursive_compoundables(monkeypatch): - """ Test recursive compounding of resolve_attribute(). """ - import blessings - from blessings.formatters import resolve_attribute, FormattingString - - # patch, - resolve_cap = lambda term, digit: 'seq-%s' % (digit,) - monkeypatch.setattr(blessings.formatters, - 'resolve_capability', - resolve_cap) - tparm = lambda *args: u'~'.join( - arg.decode('latin1') if not num else '%s' % (arg,) - for num, arg in enumerate(args)).encode('latin1') - monkeypatch.setattr(curses, 'tparm', tparm) - monkeypatch.setattr(curses, 'COLOR_RED', 6502) - monkeypatch.setattr(curses, 'COLOR_BLUE', 6800) - - color_cap = lambda digit: 'seq-%s' % (digit,) - term = mock.Mock() - term._background_color = color_cap - term._foreground_color = color_cap - term.normal = 'seq-normal' - - # given, - pstr = resolve_attribute(term, 'bright_blue_on_red') - - # excersize, - assert type(pstr) == FormattingString - assert str(pstr) == 'seq-6808seq-6502' - assert pstr('text') == 'seq-6808seq-6502textseq-normal' - - -def test_pickled_parameterizing_string(monkeypatch): - """Test pickle-ability of a formatters.ParameterizingString.""" - from blessings.formatters import ParameterizingString, FormattingString - - # simply send()/recv() over multiprocessing Pipe, a simple - # pickle.loads(dumps(...)) did not reproduce this issue, - from multiprocessing import Pipe - import pickle - - # first argument to tparm() is the sequence name, returned as-is; - # subsequent arguments are usually Integers. - tparm = lambda *args: u'~'.join( - arg.decode('latin1') if not num else '%s' % (arg,) - for num, arg in enumerate(args)).encode('latin1') - - monkeypatch.setattr(curses, 'tparm', tparm) - - # given, - pstr = ParameterizingString(u'seqname', u'norm', u'cap-name') - - # multiprocessing Pipe implicitly pickles. - r, w = Pipe() - - # excersize picklability of ParameterizingString - for proto_num in range(pickle.HIGHEST_PROTOCOL): - assert pstr == pickle.loads(pickle.dumps(pstr, protocol=proto_num)) - w.send(pstr) - r.recv() == pstr - - # excersize picklability of FormattingString - # -- the return value of calling ParameterizingString - zero = pstr(0) - for proto_num in range(pickle.HIGHEST_PROTOCOL): - assert zero == pickle.loads(pickle.dumps(zero, protocol=proto_num)) - w.send(zero) - r.recv() == zero - - -def test_tparm_returns_null(monkeypatch): - """ Test 'tparm() returned NULL' is caught (win32 PDCurses systems). """ - # on win32, any calls to tparm raises curses.error with message, - # "tparm() returned NULL", function PyCurses_tparm of _cursesmodule.c - from blessings.formatters import ParameterizingString, NullCallableString - - def tparm(*args): - raise curses.error("tparm() returned NULL") - - monkeypatch.setattr(curses, 'tparm', tparm) - - term = mock.Mock() - term.normal = 'seq-normal' - - pstr = ParameterizingString(u'cap', u'norm', u'seq-name') - - value = pstr(u'x') - assert type(value) is NullCallableString - - -def test_tparm_other_exception(monkeypatch): - """ Test 'tparm() returned NULL' is caught (win32 PDCurses systems). """ - # on win32, any calls to tparm raises curses.error with message, - # "tparm() returned NULL", function PyCurses_tparm of _cursesmodule.c - from blessings.formatters import ParameterizingString, NullCallableString - - def tparm(*args): - raise curses.error("unexpected error in tparm()") - - monkeypatch.setattr(curses, 'tparm', tparm) - - term = mock.Mock() - term.normal = 'seq-normal' - - pstr = ParameterizingString(u'cap', u'norm', u'seq-name') - - try: - pstr(u'x') - assert False, "previous call should have raised curses.error" - except curses.error: - pass diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py deleted file mode 100644 index 393b248..0000000 --- a/blessed/tests/test_keyboard.py +++ /dev/null @@ -1,902 +0,0 @@ -# -*- coding: utf-8 -*- -"Tests for keyboard support." -# std imports -import functools -import tempfile -try: - from StringIO import StringIO -except ImportError: - import io - StringIO = io.StringIO -import platform -import signal -import curses -import time -import math -import tty # NOQA -import pty -import sys -import os - -# local -from .accessories import ( - read_until_eof, - read_until_semaphore, - SEND_SEMAPHORE, - RECV_SEMAPHORE, - as_subprocess, - TestTerminal, - SEMAPHORE, - all_terms, - echo_off, - xterms, -) - -# 3rd-party -import pytest -import mock - -if sys.version_info[0] == 3: - unichr = chr - - -def test_char_is_ready_interrupted(): - "_char_is_ready() should not be interrupted with a signal handler." - pid, master_fd = pty.fork() - if pid is 0: - try: - cov = __import__('cov_core_init').init() - except ImportError: - cov = None - - # child pauses, writes semaphore and begins awaiting input - global got_sigwinch - got_sigwinch = False - - def on_resize(sig, action): - global got_sigwinch - got_sigwinch = True - - term = TestTerminal() - signal.signal(signal.SIGWINCH, on_resize) - read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) - os.write(sys.__stdout__.fileno(), SEMAPHORE) - with term.keystroke_input(raw=True): - assert term.keystroke(timeout=1.05) == u'' - os.write(sys.__stdout__.fileno(), b'complete') - assert got_sigwinch is True - if cov is not None: - cov.stop() - cov.save() - os._exit(0) - - with echo_off(master_fd): - os.write(master_fd, SEND_SEMAPHORE) - read_until_semaphore(master_fd) - stime = time.time() - os.kill(pid, signal.SIGWINCH) - output = read_until_eof(master_fd) - - pid, status = os.waitpid(pid, 0) - assert output == u'complete' - assert os.WEXITSTATUS(status) == 0 - assert math.floor(time.time() - stime) == 1.0 - - -def test_char_is_ready_interrupted_nonetype(): - "_char_is_ready() should also allow interruption with timeout of None." - pid, master_fd = pty.fork() - if pid is 0: - try: - cov = __import__('cov_core_init').init() - except ImportError: - cov = None - - # child pauses, writes semaphore and begins awaiting input - global got_sigwinch - got_sigwinch = False - - def on_resize(sig, action): - global got_sigwinch - got_sigwinch = True - - term = TestTerminal() - signal.signal(signal.SIGWINCH, on_resize) - read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) - os.write(sys.__stdout__.fileno(), SEMAPHORE) - with term.keystroke_input(raw=True): - term.keystroke(timeout=1) - os.write(sys.__stdout__.fileno(), b'complete') - assert got_sigwinch is True - if cov is not None: - cov.stop() - cov.save() - os._exit(0) - - with echo_off(master_fd): - os.write(master_fd, SEND_SEMAPHORE) - read_until_semaphore(master_fd) - stime = time.time() - time.sleep(0.05) - os.kill(pid, signal.SIGWINCH) - output = read_until_eof(master_fd) - - pid, status = os.waitpid(pid, 0) - assert output == u'complete' - assert os.WEXITSTATUS(status) == 0 - assert math.floor(time.time() - stime) == 1.0 - - -def test_char_is_ready_interrupted_interruptable(): - "_char_is_ready() may be interrupted when interruptable=False." - pid, master_fd = pty.fork() - if pid is 0: - try: - cov = __import__('cov_core_init').init() - except ImportError: - cov = None - - # child pauses, writes semaphore and begins awaiting input - global got_sigwinch - got_sigwinch = False - - def on_resize(sig, action): - global got_sigwinch - got_sigwinch = True - - term = TestTerminal() - signal.signal(signal.SIGWINCH, on_resize) - read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) - os.write(sys.__stdout__.fileno(), SEMAPHORE) - with term.keystroke_input(raw=True): - term.keystroke(timeout=1.05, interruptable=False) - os.write(sys.__stdout__.fileno(), b'complete') - assert got_sigwinch is True - if cov is not None: - cov.stop() - cov.save() - os._exit(0) - - with echo_off(master_fd): - os.write(master_fd, SEND_SEMAPHORE) - read_until_semaphore(master_fd) - stime = time.time() - time.sleep(0.05) - os.kill(pid, signal.SIGWINCH) - output = read_until_eof(master_fd) - - pid, status = os.waitpid(pid, 0) - assert output == u'complete' - assert os.WEXITSTATUS(status) == 0 - assert math.floor(time.time() - stime) == 0.0 - - -def test_char_is_ready_interrupted_nonetype_interruptable(): - """_char_is_ready() may be interrupted when interruptable=False with - timeout None.""" - pid, master_fd = pty.fork() - if pid is 0: - try: - cov = __import__('cov_core_init').init() - except ImportError: - cov = None - - # child pauses, writes semaphore and begins awaiting input - global got_sigwinch - got_sigwinch = False - - def on_resize(sig, action): - global got_sigwinch - got_sigwinch = True - - term = TestTerminal() - signal.signal(signal.SIGWINCH, on_resize) - read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) - os.write(sys.__stdout__.fileno(), SEMAPHORE) - with term.keystroke_input(raw=True): - term.keystroke(timeout=None, interruptable=False) - os.write(sys.__stdout__.fileno(), b'complete') - assert got_sigwinch is True - if cov is not None: - cov.stop() - cov.save() - os._exit(0) - - with echo_off(master_fd): - os.write(master_fd, SEND_SEMAPHORE) - read_until_semaphore(master_fd) - stime = time.time() - time.sleep(0.05) - os.kill(pid, signal.SIGWINCH) - os.write(master_fd, b'X') - output = read_until_eof(master_fd) - - pid, status = os.waitpid(pid, 0) - assert output == u'complete' - assert os.WEXITSTATUS(status) == 0 - assert math.floor(time.time() - stime) == 0.0 - - -def test_keystroke_input_no_kb(): - "keystroke_input() should not call tty.setcbreak() without keyboard." - @as_subprocess - def child(): - with tempfile.NamedTemporaryFile() as stream: - term = TestTerminal(stream=stream) - with mock.patch("tty.setcbreak") as mock_setcbreak: - with term.keystroke_input(): - assert not mock_setcbreak.called - assert term.keyboard_fd is None - child() - - -def test_notty_kb_is_None(): - "keyboard_fd should be None when os.isatty returns False." - # in this scenerio, stream is sys.__stdout__, - # but os.isatty(0) is False, - # such as when piping output to less(1) - @as_subprocess - def child(): - with mock.patch("os.isatty") as mock_isatty: - mock_isatty.return_value = False - term = TestTerminal() - assert term.keyboard_fd is None - child() - - -def test_raw_input_no_kb(): - "keystroke_input(raw=True) should not call tty.setraw() without keyboard." - @as_subprocess - def child(): - with tempfile.NamedTemporaryFile() as stream: - term = TestTerminal(stream=stream) - with mock.patch("tty.setraw") as mock_setraw: - with term.keystroke_input(raw=True): - assert not mock_setraw.called - assert term.keyboard_fd is None - child() - - -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()) - stime = time.time() - assert term.keyboard_fd is None - assert term._char_is_ready(timeout=1.1) is False - assert (math.floor(time.time() - stime) == 1.0) - child() - - -def test_keystroke_0s_keystroke_input_noinput(): - "0-second keystroke without input; '' should be returned." - @as_subprocess - def child(): - term = TestTerminal() - with term.keystroke_input(): - stime = time.time() - inp = term.keystroke(timeout=0) - assert (inp == u'') - assert (math.floor(time.time() - stime) == 0.0) - child() - - -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()) - with term.keystroke_input(): - stime = time.time() - inp = term.keystroke(timeout=0) - assert (inp == u'') - assert (math.floor(time.time() - stime) == 0.0) - child() - - -def test_keystroke_1s_keystroke_input_noinput(): - "1-second keystroke without input; '' should be returned after ~1 second." - @as_subprocess - def child(): - term = TestTerminal() - with term.keystroke_input(): - stime = time.time() - inp = term.keystroke(timeout=1) - assert (inp == u'') - assert (math.floor(time.time() - stime) == 1.0) - child() - - -def test_keystroke_1s_keystroke_input_noinput_nokb(): - "1-second keystroke without input or keyboard." - @as_subprocess - def child(): - term = TestTerminal(stream=StringIO()) - with term.keystroke_input(): - stime = time.time() - inp = term.keystroke(timeout=1) - assert (inp == u'') - assert (math.floor(time.time() - stime) == 1.0) - child() - - -def test_keystroke_0s_keystroke_input_with_input(): - "0-second keystroke with input; Keypress should be immediately returned." - pid, master_fd = pty.fork() - if pid is 0: - try: - cov = __import__('cov_core_init').init() - except ImportError: - cov = None - # child pauses, writes semaphore and begins awaiting input - term = TestTerminal() - read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) - os.write(sys.__stdout__.fileno(), SEMAPHORE) - with term.keystroke_input(): - inp = term.keystroke(timeout=0) - os.write(sys.__stdout__.fileno(), inp.encode('utf-8')) - if cov is not None: - cov.stop() - cov.save() - os._exit(0) - - with echo_off(master_fd): - os.write(master_fd, SEND_SEMAPHORE) - os.write(master_fd, u'x'.encode('ascii')) - read_until_semaphore(master_fd) - stime = time.time() - output = read_until_eof(master_fd) - - pid, status = os.waitpid(pid, 0) - assert output == u'x' - assert os.WEXITSTATUS(status) == 0 - assert math.floor(time.time() - stime) == 0.0 - - -def test_keystroke_keystroke_input_with_input_slowly(): - "0-second keystroke with input; Keypress should be immediately returned." - pid, master_fd = pty.fork() - if pid is 0: - try: - cov = __import__('cov_core_init').init() - except ImportError: - cov = None - # child pauses, writes semaphore and begins awaiting input - term = TestTerminal() - read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) - os.write(sys.__stdout__.fileno(), SEMAPHORE) - with term.keystroke_input(): - while True: - inp = term.keystroke(timeout=0.5) - os.write(sys.__stdout__.fileno(), inp.encode('utf-8')) - if inp == 'X': - break - if cov is not None: - cov.stop() - cov.save() - os._exit(0) - - with echo_off(master_fd): - os.write(master_fd, SEND_SEMAPHORE) - os.write(master_fd, u'a'.encode('ascii')) - time.sleep(0.1) - os.write(master_fd, u'b'.encode('ascii')) - time.sleep(0.1) - os.write(master_fd, u'cdefgh'.encode('ascii')) - time.sleep(0.1) - os.write(master_fd, u'X'.encode('ascii')) - read_until_semaphore(master_fd) - stime = time.time() - output = read_until_eof(master_fd) - - pid, status = os.waitpid(pid, 0) - assert output == u'abcdefghX' - assert os.WEXITSTATUS(status) == 0 - assert math.floor(time.time() - stime) == 0.0 - - -def test_keystroke_0s_keystroke_input_multibyte_utf8(): - "0-second keystroke with multibyte utf-8 input; should decode immediately." - # utf-8 bytes represent "latin capital letter upsilon". - pid, master_fd = pty.fork() - if pid is 0: # child - try: - cov = __import__('cov_core_init').init() - except ImportError: - cov = None - term = TestTerminal() - read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) - os.write(sys.__stdout__.fileno(), SEMAPHORE) - with term.keystroke_input(): - inp = term.keystroke(timeout=0) - os.write(sys.__stdout__.fileno(), inp.encode('utf-8')) - if cov is not None: - cov.stop() - cov.save() - os._exit(0) - - with echo_off(master_fd): - os.write(master_fd, SEND_SEMAPHORE) - os.write(master_fd, u'\u01b1'.encode('utf-8')) - read_until_semaphore(master_fd) - stime = time.time() - output = read_until_eof(master_fd) - pid, status = os.waitpid(pid, 0) - assert output == u'Ʊ' - assert os.WEXITSTATUS(status) == 0 - assert math.floor(time.time() - stime) == 0.0 - - -@pytest.mark.skipif(os.environ.get('TRAVIS', None) is not None or - platform.python_implementation() == 'PyPy', - reason="travis-ci nor pypy handle ^C very well.") -def test_keystroke_0s_raw_input_ctrl_c(): - "0-second keystroke with raw allows receiving ^C." - pid, master_fd = pty.fork() - if pid is 0: # child - try: - cov = __import__('cov_core_init').init() - except ImportError: - cov = None - term = TestTerminal() - read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) - with term.keystroke_input(raw=True): - os.write(sys.__stdout__.fileno(), RECV_SEMAPHORE) - inp = term.keystroke(timeout=0) - os.write(sys.__stdout__.fileno(), inp.encode('latin1')) - if cov is not None: - cov.stop() - cov.save() - os._exit(0) - - with echo_off(master_fd): - os.write(master_fd, SEND_SEMAPHORE) - # ensure child is in raw mode before sending ^C, - read_until_semaphore(master_fd) - os.write(master_fd, u'\x03'.encode('latin1')) - stime = time.time() - output = read_until_eof(master_fd) - pid, status = os.waitpid(pid, 0) - assert (output == u'\x03' or - output == u'' and not os.isatty(0)) - assert os.WEXITSTATUS(status) == 0 - assert math.floor(time.time() - stime) == 0.0 - - -def test_keystroke_0s_keystroke_input_sequence(): - "0-second keystroke with multibyte sequence; should decode immediately." - pid, master_fd = pty.fork() - if pid is 0: # child - try: - cov = __import__('cov_core_init').init() - except ImportError: - cov = None - term = TestTerminal() - os.write(sys.__stdout__.fileno(), SEMAPHORE) - with term.keystroke_input(): - inp = term.keystroke(timeout=0) - os.write(sys.__stdout__.fileno(), inp.name.encode('ascii')) - sys.stdout.flush() - if cov is not None: - cov.stop() - cov.save() - os._exit(0) - - with echo_off(master_fd): - os.write(master_fd, u'\x1b[D'.encode('ascii')) - read_until_semaphore(master_fd) - stime = time.time() - output = read_until_eof(master_fd) - pid, status = os.waitpid(pid, 0) - assert output == u'KEY_LEFT' - assert os.WEXITSTATUS(status) == 0 - assert math.floor(time.time() - stime) == 0.0 - - -def test_keystroke_1s_keystroke_input_with_input(): - "1-second keystroke w/multibyte sequence; should return after ~1 second." - pid, master_fd = pty.fork() - if pid is 0: # child - try: - cov = __import__('cov_core_init').init() - except ImportError: - cov = None - term = TestTerminal() - os.write(sys.__stdout__.fileno(), SEMAPHORE) - with term.keystroke_input(): - inp = term.keystroke(timeout=3) - os.write(sys.__stdout__.fileno(), inp.name.encode('utf-8')) - sys.stdout.flush() - if cov is not None: - cov.stop() - cov.save() - os._exit(0) - - with echo_off(master_fd): - read_until_semaphore(master_fd) - stime = time.time() - time.sleep(1) - os.write(master_fd, u'\x1b[C'.encode('ascii')) - output = read_until_eof(master_fd) - - pid, status = os.waitpid(pid, 0) - assert output == u'KEY_RIGHT' - assert os.WEXITSTATUS(status) == 0 - assert math.floor(time.time() - stime) == 1.0 - - -def test_esc_delay_keystroke_input_035(): - "esc_delay will cause a single ESC (\\x1b) to delay for 0.35." - pid, master_fd = pty.fork() - if pid is 0: # child - try: - cov = __import__('cov_core_init').init() - except ImportError: - cov = None - term = TestTerminal() - os.write(sys.__stdout__.fileno(), SEMAPHORE) - with term.keystroke_input(): - stime = time.time() - inp = term.keystroke(timeout=5) - measured_time = (time.time() - stime) * 100 - os.write(sys.__stdout__.fileno(), ( - '%s %i' % (inp.name, measured_time,)).encode('ascii')) - sys.stdout.flush() - if cov is not None: - cov.stop() - cov.save() - os._exit(0) - - with echo_off(master_fd): - read_until_semaphore(master_fd) - stime = time.time() - os.write(master_fd, u'\x1b'.encode('ascii')) - key_name, duration_ms = read_until_eof(master_fd).split() - - pid, status = os.waitpid(pid, 0) - assert key_name == u'KEY_ESCAPE' - assert os.WEXITSTATUS(status) == 0 - assert math.floor(time.time() - stime) == 0.0 - assert 34 <= int(duration_ms) <= 45, duration_ms - - -def test_esc_delay_keystroke_input_135(): - "esc_delay=1.35 will cause a single ESC (\\x1b) to delay for 1.35." - pid, master_fd = pty.fork() - if pid is 0: # child - try: - cov = __import__('cov_core_init').init() - except ImportError: - cov = None - term = TestTerminal() - os.write(sys.__stdout__.fileno(), SEMAPHORE) - with term.keystroke_input(): - stime = time.time() - inp = term.keystroke(timeout=5, esc_delay=1.35) - measured_time = (time.time() - stime) * 100 - os.write(sys.__stdout__.fileno(), ( - '%s %i' % (inp.name, measured_time,)).encode('ascii')) - sys.stdout.flush() - if cov is not None: - cov.stop() - cov.save() - os._exit(0) - - with echo_off(master_fd): - read_until_semaphore(master_fd) - stime = time.time() - os.write(master_fd, u'\x1b'.encode('ascii')) - key_name, duration_ms = read_until_eof(master_fd).split() - - pid, status = os.waitpid(pid, 0) - assert key_name == u'KEY_ESCAPE' - assert os.WEXITSTATUS(status) == 0 - assert math.floor(time.time() - stime) == 1.0 - assert 134 <= int(duration_ms) <= 145, int(duration_ms) - - -def test_esc_delay_keystroke_input_timout_0(): - """esc_delay still in effect with timeout of 0 ("nonblocking").""" - pid, master_fd = pty.fork() - if pid is 0: # child - try: - cov = __import__('cov_core_init').init() - except ImportError: - cov = None - term = TestTerminal() - os.write(sys.__stdout__.fileno(), SEMAPHORE) - with term.keystroke_input(): - stime = time.time() - inp = term.keystroke(timeout=0) - measured_time = (time.time() - stime) * 100 - os.write(sys.__stdout__.fileno(), ( - '%s %i' % (inp.name, measured_time,)).encode('ascii')) - sys.stdout.flush() - if cov is not None: - cov.stop() - cov.save() - os._exit(0) - - with echo_off(master_fd): - os.write(master_fd, u'\x1b'.encode('ascii')) - read_until_semaphore(master_fd) - stime = time.time() - key_name, duration_ms = read_until_eof(master_fd).split() - - pid, status = os.waitpid(pid, 0) - assert key_name == u'KEY_ESCAPE' - assert os.WEXITSTATUS(status) == 0 - assert math.floor(time.time() - stime) == 0.0 - assert 34 <= int(duration_ms) <= 45, int(duration_ms) - - -def test_keystroke_default_args(): - "Test keyboard.Keystroke constructor with default arguments." - from blessings.keyboard import Keystroke - ks = Keystroke() - assert ks._name is None - assert ks.name == ks._name - assert ks._code is None - assert ks.code == ks._code - assert u'x' == u'x' + ks - assert ks.is_sequence is False - assert repr(ks) in ("u''", # py26, 27 - "''",) # py33 - - -def test_a_keystroke(): - "Test keyboard.Keystroke constructor with set arguments." - from blessings.keyboard import Keystroke - ks = Keystroke(ucs=u'x', code=1, name=u'the X') - assert ks._name is u'the X' - assert ks.name == ks._name - assert ks._code is 1 - assert ks.code == ks._code - assert u'xx' == u'x' + ks - assert ks.is_sequence is True - assert repr(ks) == "the X" - - -def test_get_keyboard_codes(): - "Test all values returned by get_keyboard_codes are from curses." - from blessings.keyboard import ( - get_keyboard_codes, - CURSES_KEYCODE_OVERRIDE_MIXIN, - ) - exemptions = dict(CURSES_KEYCODE_OVERRIDE_MIXIN) - for value, keycode in get_keyboard_codes().items(): - if keycode in exemptions: - assert value == exemptions[keycode] - continue - assert hasattr(curses, keycode) - assert getattr(curses, keycode) == value - - -def test_alternative_left_right(): - "Test _alternative_left_right behavior for space/backspace." - from blessings.keyboard import _alternative_left_right - term = mock.Mock() - term._cuf1 = u'' - term._cub1 = u'' - assert not bool(_alternative_left_right(term)) - term._cuf1 = u' ' - term._cub1 = u'\b' - assert not bool(_alternative_left_right(term)) - term._cuf1 = u'seq-right' - term._cub1 = u'seq-left' - assert (_alternative_left_right(term) == { - u'seq-right': curses.KEY_RIGHT, - u'seq-left': curses.KEY_LEFT}) - - -def test_cuf1_and_cub1_as_RIGHT_LEFT(all_terms): - "Test that cuf1 and cub1 are assigned KEY_RIGHT and KEY_LEFT." - from blessings.keyboard import get_keyboard_sequences - - @as_subprocess - def child(kind): - term = TestTerminal(kind=kind, force_styling=True) - keymap = get_keyboard_sequences(term) - if term._cuf1: - assert term._cuf1 in keymap - assert keymap[term._cuf1] == term.KEY_RIGHT - if term._cub1: - assert term._cub1 in keymap - if term._cub1 == '\b': - assert keymap[term._cub1] == term.KEY_BACKSPACE - else: - assert keymap[term._cub1] == term.KEY_LEFT - - child(all_terms) - - -def test_get_keyboard_sequences_sort_order(xterms): - "ordereddict ensures sequences are ordered longest-first." - @as_subprocess - def child(): - term = TestTerminal(force_styling=True) - maxlen = None - for sequence, code in term._keymap.items(): - if maxlen is not None: - assert len(sequence) <= maxlen - assert sequence - maxlen = len(sequence) - child() - - -def test_get_keyboard_sequence(monkeypatch): - "Test keyboard.get_keyboard_sequence. " - import curses.has_key - import blessings.keyboard - - (KEY_SMALL, KEY_LARGE, KEY_MIXIN) = range(3) - (CAP_SMALL, CAP_LARGE) = 'cap-small cap-large'.split() - (SEQ_SMALL, SEQ_LARGE, SEQ_MIXIN, SEQ_ALT_CUF1, SEQ_ALT_CUB1) = ( - b'seq-small-a', - b'seq-large-abcdefg', - b'seq-mixin', - b'seq-alt-cuf1', - b'seq-alt-cub1_') - - # patch curses functions - monkeypatch.setattr(curses, 'tigetstr', - lambda cap: {CAP_SMALL: SEQ_SMALL, - CAP_LARGE: SEQ_LARGE}[cap]) - - monkeypatch.setattr(curses.has_key, '_capability_names', - dict(((KEY_SMALL, CAP_SMALL,), - (KEY_LARGE, CAP_LARGE,)))) - - # patch global sequence mix-in - monkeypatch.setattr(blessings.keyboard, - 'DEFAULT_SEQUENCE_MIXIN', ( - (SEQ_MIXIN.decode('latin1'), KEY_MIXIN),)) - - # patch for _alternative_left_right - term = mock.Mock() - term._cuf1 = SEQ_ALT_CUF1.decode('latin1') - term._cub1 = SEQ_ALT_CUB1.decode('latin1') - keymap = blessings.keyboard.get_keyboard_sequences(term) - - assert list(keymap.items()) == [ - (SEQ_LARGE.decode('latin1'), KEY_LARGE), - (SEQ_ALT_CUB1.decode('latin1'), curses.KEY_LEFT), - (SEQ_ALT_CUF1.decode('latin1'), curses.KEY_RIGHT), - (SEQ_SMALL.decode('latin1'), KEY_SMALL), - (SEQ_MIXIN.decode('latin1'), KEY_MIXIN)] - - -def test_resolve_sequence(): - "Test resolve_sequence for order-dependent mapping." - from blessings.keyboard import resolve_sequence, OrderedDict - mapper = OrderedDict(((u'SEQ1', 1), - (u'SEQ2', 2), - # takes precedence over LONGSEQ, first-match - (u'KEY_LONGSEQ_longest', 3), - (u'LONGSEQ', 4), - # wont match, LONGSEQ is first-match in this order - (u'LONGSEQ_longer', 5), - # falls through for L{anything_else} - (u'L', 6))) - codes = {1: u'KEY_SEQ1', - 2: u'KEY_SEQ2', - 3: u'KEY_LONGSEQ_longest', - 4: u'KEY_LONGSEQ', - 5: u'KEY_LONGSEQ_longer', - 6: u'KEY_L'} - ks = resolve_sequence(u'', mapper, codes) - assert ks == u'' - assert ks.name is None - assert ks.code is None - assert ks.is_sequence is False - assert repr(ks) in ("u''", # py26, 27 - "''",) # py33 - - ks = resolve_sequence(u'notfound', mapper=mapper, codes=codes) - assert ks == u'n' - assert ks.name is None - assert ks.code is None - assert ks.is_sequence is False - assert repr(ks) in (u"u'n'", "'n'",) - - ks = resolve_sequence(u'SEQ1', mapper, codes) - assert ks == u'SEQ1' - assert ks.name == u'KEY_SEQ1' - assert ks.code is 1 - assert ks.is_sequence is True - assert repr(ks) in (u"KEY_SEQ1", "KEY_SEQ1") - - ks = resolve_sequence(u'LONGSEQ_longer', mapper, codes) - assert ks == u'LONGSEQ' - assert ks.name == u'KEY_LONGSEQ' - assert ks.code is 4 - assert ks.is_sequence is True - assert repr(ks) in (u"KEY_LONGSEQ", "KEY_LONGSEQ") - - ks = resolve_sequence(u'LONGSEQ', mapper, codes) - assert ks == u'LONGSEQ' - assert ks.name == u'KEY_LONGSEQ' - assert ks.code is 4 - assert ks.is_sequence is True - assert repr(ks) in (u"KEY_LONGSEQ", "KEY_LONGSEQ") - - ks = resolve_sequence(u'Lxxxxx', mapper, codes) - assert ks == u'L' - assert ks.name == u'KEY_L' - assert ks.code is 6 - assert ks.is_sequence is True - assert repr(ks) in (u"KEY_L", "KEY_L") - - -def test_keypad_mixins_and_aliases(): - """ Test PC-Style function key translations when in ``keypad`` mode.""" - # Key plain app modified - # Up ^[[A ^[OA ^[[1;mA - # Down ^[[B ^[OB ^[[1;mB - # Right ^[[C ^[OC ^[[1;mC - # Left ^[[D ^[OD ^[[1;mD - # End ^[[F ^[OF ^[[1;mF - # Home ^[[H ^[OH ^[[1;mH - @as_subprocess - def child(kind): - term = TestTerminal(kind=kind, force_styling=True) - from blessings.keyboard import resolve_sequence - - resolve = functools.partial(resolve_sequence, - mapper=term._keymap, - codes=term._keycodes) - - assert resolve(unichr(10)).name == "KEY_ENTER" - assert resolve(unichr(13)).name == "KEY_ENTER" - assert resolve(unichr(8)).name == "KEY_BACKSPACE" - assert resolve(unichr(9)).name == "KEY_TAB" - assert resolve(unichr(27)).name == "KEY_ESCAPE" - assert resolve(unichr(127)).name == "KEY_DELETE" - assert resolve(u"\x1b[A").name == "KEY_UP" - assert resolve(u"\x1b[B").name == "KEY_DOWN" - assert resolve(u"\x1b[C").name == "KEY_RIGHT" - assert resolve(u"\x1b[D").name == "KEY_LEFT" - assert resolve(u"\x1b[U").name == "KEY_PGDOWN" - assert resolve(u"\x1b[V").name == "KEY_PGUP" - assert resolve(u"\x1b[H").name == "KEY_HOME" - assert resolve(u"\x1b[F").name == "KEY_END" - assert resolve(u"\x1b[K").name == "KEY_END" - assert resolve(u"\x1bOM").name == "KEY_ENTER" - assert resolve(u"\x1bOj").name == "KEY_KP_MULTIPLY" - assert resolve(u"\x1bOk").name == "KEY_KP_ADD" - assert resolve(u"\x1bOl").name == "KEY_KP_SEPARATOR" - assert resolve(u"\x1bOm").name == "KEY_KP_SUBTRACT" - assert resolve(u"\x1bOn").name == "KEY_KP_DECIMAL" - assert resolve(u"\x1bOo").name == "KEY_KP_DIVIDE" - assert resolve(u"\x1bOX").name == "KEY_KP_EQUAL" - assert resolve(u"\x1bOp").name == "KEY_KP_0" - assert resolve(u"\x1bOq").name == "KEY_KP_1" - assert resolve(u"\x1bOr").name == "KEY_KP_2" - assert resolve(u"\x1bOs").name == "KEY_KP_3" - assert resolve(u"\x1bOt").name == "KEY_KP_4" - assert resolve(u"\x1bOu").name == "KEY_KP_5" - assert resolve(u"\x1bOv").name == "KEY_KP_6" - assert resolve(u"\x1bOw").name == "KEY_KP_7" - assert resolve(u"\x1bOx").name == "KEY_KP_8" - assert resolve(u"\x1bOy").name == "KEY_KP_9" - assert resolve(u"\x1b[1~").name == "KEY_FIND" - assert resolve(u"\x1b[2~").name == "KEY_INSERT" - assert resolve(u"\x1b[3~").name == "KEY_DELETE" - assert resolve(u"\x1b[4~").name == "KEY_SELECT" - assert resolve(u"\x1b[5~").name == "KEY_PGUP" - assert resolve(u"\x1b[6~").name == "KEY_PGDOWN" - assert resolve(u"\x1b[7~").name == "KEY_HOME" - assert resolve(u"\x1b[8~").name == "KEY_END" - assert resolve(u"\x1b[OA").name == "KEY_UP" - assert resolve(u"\x1b[OB").name == "KEY_DOWN" - assert resolve(u"\x1b[OC").name == "KEY_RIGHT" - assert resolve(u"\x1b[OD").name == "KEY_LEFT" - assert resolve(u"\x1b[OF").name == "KEY_END" - assert resolve(u"\x1b[OH").name == "KEY_HOME" - assert resolve(u"\x1bOP").name == "KEY_F1" - assert resolve(u"\x1bOQ").name == "KEY_F2" - assert resolve(u"\x1bOR").name == "KEY_F3" - assert resolve(u"\x1bOS").name == "KEY_F4" - - child('xterm') diff --git a/blessed/tests/test_length_sequence.py b/blessed/tests/test_length_sequence.py deleted file mode 100644 index 0eac165..0000000 --- a/blessed/tests/test_length_sequence.py +++ /dev/null @@ -1,342 +0,0 @@ -# encoding: utf-8 -import itertools -import platform -import termios -import struct -import fcntl -import sys -import os -try: - from StringIO import StringIO -except ImportError: - from io import StringIO - -from .accessories import ( - all_terms, - as_subprocess, - TestTerminal, - many_columns, - many_lines, -) - -import pytest - - -def test_length_cjk(): - @as_subprocess - def child(): - term = TestTerminal(kind='xterm-256color') - - # given, - given = term.bold_red(u'コンニチハ, セカイ!') - expected = sum((2, 2, 2, 2, 2, 1, 1, 2, 2, 2, 1,)) - - # exercise, - assert term.length(given) == expected - - child() - - -def test_length_ansiart(): - @as_subprocess - def child(): - import codecs - from blessings.sequences import Sequence - term = TestTerminal(kind='xterm-256color') - # this 'ansi' art contributed by xzip!impure for another project, - # unlike most CP-437 DOS ansi art, this is actually utf-8 encoded. - fname = os.path.join(os.path.dirname(__file__), 'wall.ans') - lines = codecs.open(fname, 'r', 'utf-8').readlines() - assert term.length(lines[0]) == 67 # ^[[64C^[[34m▄▓▄ - assert term.length(lines[1]) == 75 - assert term.length(lines[2]) == 78 - assert term.length(lines[3]) == 78 - assert term.length(lines[4]) == 78 - assert term.length(lines[5]) == 78 - assert term.length(lines[6]) == 77 - child() - - -def test_sequence_length(all_terms): - """Ensure T.length(string containing sequence) is correct.""" - @as_subprocess - def child(kind): - t = TestTerminal(kind=kind) - # Create a list of ascii characters, to be separated - # by word, to be zipped up with a cycling list of - # terminal sequences. Then, compare the length of - # each, the basic plain_text.__len__ vs. the Terminal - # method length. They should be equal. - plain_text = (u'The softest things of the world ' - u'Override the hardest things of the world ' - u'That which has no substance ' - u'Enters into that which has no openings') - if t.bold: - assert (t.length(t.bold) == 0) - assert (t.length(t.bold(u'x')) == 1) - assert (t.length(t.bold_red) == 0) - assert (t.length(t.bold_red(u'x')) == 1) - assert (t.strip(t.bold) == u'') - assert (t.rstrip(t.bold) == u'') - assert (t.lstrip(t.bold) == u'') - assert (t.strip(t.bold(u' x ')) == u'x') - assert (t.strip(t.bold(u'z x q'), 'zq') == u' x ') - assert (t.rstrip(t.bold(u' x ')) == u' x') - assert (t.lstrip(t.bold(u' x ')) == u'x ') - assert (t.strip(t.bold_red) == u'') - assert (t.rstrip(t.bold_red) == u'') - assert (t.lstrip(t.bold_red) == u'') - assert (t.strip(t.bold_red(u' x ')) == u'x') - assert (t.rstrip(t.bold_red(u' x ')) == u' x') - assert (t.lstrip(t.bold_red(u' x ')) == u'x ') - assert (t.strip_seqs(t.bold) == u'') - assert (t.strip_seqs(t.bold(u' x ')) == u' x ') - assert (t.strip_seqs(t.bold_red) == u'') - assert (t.strip_seqs(t.bold_red(u' x ')) == u' x ') - - if t.underline: - assert (t.length(t.underline) == 0) - assert (t.length(t.underline(u'x')) == 1) - assert (t.length(t.underline_red) == 0) - assert (t.length(t.underline_red(u'x')) == 1) - assert (t.strip(t.underline) == u'') - assert (t.strip(t.underline(u' x ')) == u'x') - assert (t.strip(t.underline_red) == u'') - assert (t.strip(t.underline_red(u' x ')) == u'x') - assert (t.rstrip(t.underline_red(u' x ')) == u' x') - assert (t.lstrip(t.underline_red(u' x ')) == u'x ') - assert (t.strip_seqs(t.underline) == u'') - assert (t.strip_seqs(t.underline(u' x ')) == u' x ') - assert (t.strip_seqs(t.underline_red) == u'') - assert (t.strip_seqs(t.underline_red(u' x ')) == u' x ') - - if t.reverse: - assert (t.length(t.reverse) == 0) - assert (t.length(t.reverse(u'x')) == 1) - assert (t.length(t.reverse_red) == 0) - assert (t.length(t.reverse_red(u'x')) == 1) - assert (t.strip(t.reverse) == u'') - assert (t.strip(t.reverse(u' x ')) == u'x') - assert (t.strip(t.reverse_red) == u'') - assert (t.strip(t.reverse_red(u' x ')) == u'x') - assert (t.rstrip(t.reverse_red(u' x ')) == u' x') - assert (t.lstrip(t.reverse_red(u' x ')) == u'x ') - assert (t.strip_seqs(t.reverse) == u'') - assert (t.strip_seqs(t.reverse(u' x ')) == u' x ') - assert (t.strip_seqs(t.reverse_red) == u'') - assert (t.strip_seqs(t.reverse_red(u' x ')) == u' x ') - - if t.blink: - assert (t.length(t.blink) == 0) - assert (t.length(t.blink(u'x')) == 1) - assert (t.length(t.blink_red) == 0) - assert (t.length(t.blink_red(u'x')) == 1) - assert (t.strip(t.blink) == u'') - assert (t.strip(t.blink(u' x ')) == u'x') - assert (t.strip(t.blink(u'z x q'), u'zq') == u' x ') - assert (t.strip(t.blink_red) == u'') - assert (t.strip(t.blink_red(u' x ')) == u'x') - assert (t.strip_seqs(t.blink) == u'') - assert (t.strip_seqs(t.blink(u' x ')) == u' x ') - assert (t.strip_seqs(t.blink_red) == u'') - assert (t.strip_seqs(t.blink_red(u' x ')) == u' x ') - - if t.home: - assert (t.length(t.home) == 0) - assert (t.strip(t.home) == u'') - if t.clear_eol: - assert (t.length(t.clear_eol) == 0) - assert (t.strip(t.clear_eol) == u'') - if t.enter_fullscreen: - assert (t.length(t.enter_fullscreen) == 0) - assert (t.strip(t.enter_fullscreen) == u'') - if t.exit_fullscreen: - assert (t.length(t.exit_fullscreen) == 0) - assert (t.strip(t.exit_fullscreen) == u'') - - # horizontally, we decide move_down and move_up are 0, - assert (t.length(t.move_down) == 0) - assert (t.length(t.move_down(2)) == 0) - assert (t.length(t.move_up) == 0) - assert (t.length(t.move_up(2)) == 0) - - # other things aren't so simple, somewhat edge cases, - # moving backwards and forwards horizontally must be - # accounted for as a "length", as - # will result in a printed column length of 12 (even - # though columns 2-11 are non-destructive space - assert (t.length(u'x\b') == 0) - assert (t.strip(u'x\b') == u'') - - # XXX why are some terminals width of 9 here ?? - assert (t.length(u'\t') in (8, 9)) - assert (t.strip(u'\t') == u'') - assert (t.length(u'_' + t.move_left) == 0) - - if t.cub: - assert (t.length((u'_' * 10) + t.cub(10)) == 0) - - assert (t.length(t.move_right) == 1) - - if t.cuf: - assert (t.length(t.cuf(10)) == 10) - - # vertical spacing is unaccounted as a 'length' - assert (t.length(t.move_up) == 0) - assert (t.length(t.cuu(10)) == 0) - assert (t.length(t.move_down) == 0) - assert (t.length(t.cud(10)) == 0) - - # this is how manpages perform underlining, this is done - # with the 'overstrike' capability of teletypes, and aparently - # less(1), '123' -> '1\b_2\b_3\b_' - text_wseqs = u''.join(itertools.chain( - *zip(plain_text, itertools.cycle(['\b_'])))) - assert (t.length(text_wseqs) == len(plain_text)) - - child(all_terms) - - -def test_env_winsize(): - """Test height and width is appropriately queried in a pty.""" - @as_subprocess - def child(): - # set the pty's virtual window size - os.environ['COLUMNS'] = '99' - os.environ['LINES'] = '11' - t = TestTerminal(stream=StringIO()) - save_init = t._init_descriptor - save_stdout = sys.__stdout__ - try: - t._init_descriptor = None - sys.__stdout__ = None - winsize = t._height_and_width() - width = t.width - height = t.height - finally: - t._init_descriptor = save_init - sys.__stdout__ = save_stdout - assert winsize.ws_col == width == 99 - assert winsize.ws_row == height == 11 - - child() - - -@pytest.mark.skipif(platform.python_implementation() == 'PyPy', - reason='PyPy fails TIOCSWINSZ') -def test_winsize(many_lines, many_columns): - """Test height and width is appropriately queried in a pty.""" - @as_subprocess - def child(lines=25, cols=80): - # set the pty's virtual window size - val = struct.pack('HHHH', lines, cols, 0, 0) - fcntl.ioctl(sys.__stdout__.fileno(), termios.TIOCSWINSZ, val) - t = TestTerminal() - winsize = t._height_and_width() - assert t.width == cols - assert t.height == lines - assert winsize.ws_col == cols - assert winsize.ws_row == lines - - child(lines=many_lines, cols=many_columns) - - -@pytest.mark.skipif(platform.python_implementation() == 'PyPy', - reason='PyPy fails TIOCSWINSZ') -def test_Sequence_alignment(all_terms): - """Tests methods related to Sequence class, namely ljust, rjust, center.""" - @as_subprocess - def child(kind, lines=25, cols=80): - # set the pty's virtual window size - val = struct.pack('HHHH', lines, cols, 0, 0) - fcntl.ioctl(sys.__stdout__.fileno(), termios.TIOCSWINSZ, val) - t = TestTerminal(kind=kind) - - pony_msg = 'pony express, all aboard, choo, choo!' - pony_len = len(pony_msg) - pony_colored = u''.join( - ['%s%s' % (t.color(n % 7), ch,) - for n, ch in enumerate(pony_msg)]) - pony_colored += t.normal - ladjusted = t.ljust(pony_colored) - radjusted = t.rjust(pony_colored) - centered = t.center(pony_colored) - assert (t.length(pony_colored) == pony_len) - assert (t.length(centered.strip()) == pony_len) - assert (t.length(centered) == len(pony_msg.center(t.width))) - assert (t.length(ladjusted.strip()) == pony_len) - assert (t.length(ladjusted) == len(pony_msg.ljust(t.width))) - assert (t.length(radjusted.strip()) == pony_len) - assert (t.length(radjusted) == len(pony_msg.rjust(t.width))) - - child(kind=all_terms) - - -def test_sequence_is_movement_false(all_terms): - """Test parser about sequences that do not move the cursor.""" - @as_subprocess - def child_mnemonics_wontmove(kind): - from blessings.sequences import measure_length - t = TestTerminal(kind=kind) - assert (0 == measure_length(u'', t)) - # not even a mbs - assert (0 == measure_length(u'xyzzy', t)) - # negative numbers, though printable as %d, do not result - # in movement; just garbage. Also not a valid sequence. - assert (0 == measure_length(t.cuf(-333), t)) - assert (len(t.clear_eol) == measure_length(t.clear_eol, t)) - # various erases don't *move* - assert (len(t.clear_bol) == measure_length(t.clear_bol, t)) - assert (len(t.clear_eos) == measure_length(t.clear_eos, t)) - assert (len(t.bold) == measure_length(t.bold, t)) - # various paints don't move - assert (len(t.red) == measure_length(t.red, t)) - assert (len(t.civis) == measure_length(t.civis, t)) - if t.cvvis: - assert (len(t.cvvis) == measure_length(t.cvvis, t)) - assert (len(t.underline) == measure_length(t.underline, t)) - assert (len(t.reverse) == measure_length(t.reverse, t)) - for _num in range(t.number_of_colors): - assert (len(t.color(_num)) == measure_length(t.color(_num), t)) - assert (len(t.normal) == measure_length(t.normal, t)) - assert (len(t.normal_cursor) == measure_length(t.normal_cursor, t)) - assert (len(t.hide_cursor) == measure_length(t.hide_cursor, t)) - assert (len(t.save) == measure_length(t.save, t)) - assert (len(t.italic) == measure_length(t.italic, t)) - assert (len(t.standout) == measure_length(t.standout, t) - ), (t.standout, t._wont_move) - - child_mnemonics_wontmove(all_terms) - - -def test_sequence_is_movement_true(all_terms): - """Test parsers about sequences that move the cursor.""" - @as_subprocess - def child_mnemonics_willmove(kind): - from blessings.sequences import measure_length - t = TestTerminal(kind=kind) - # movements - assert (len(t.move(98, 76)) == - measure_length(t.move(98, 76), t)) - assert (len(t.move(54)) == - measure_length(t.move(54), t)) - assert not t.cud1 or (len(t.cud1) == - measure_length(t.cud1, t)) - assert not t.cub1 or (len(t.cub1) == - measure_length(t.cub1, t)) - assert not t.cuf1 or (len(t.cuf1) == - measure_length(t.cuf1, t)) - assert not t.cuu1 or (len(t.cuu1) == - measure_length(t.cuu1, t)) - assert not t.cub or (len(t.cub(333)) == - measure_length(t.cub(333), t)) - assert not t.cuf or (len(t.cuf(333)) == - measure_length(t.cuf(333), t)) - assert not t.home or (len(t.home) == - measure_length(t.home, t)) - assert not t.restore or (len(t.restore) == - measure_length(t.restore, t)) - assert not t.clear or (len(t.clear) == - measure_length(t.clear, t)) - - child_mnemonics_willmove(all_terms) diff --git a/blessed/tests/test_sequences.py b/blessed/tests/test_sequences.py deleted file mode 100644 index f42a52c..0000000 --- a/blessed/tests/test_sequences.py +++ /dev/null @@ -1,549 +0,0 @@ -# -*- coding: utf-8 -*- -"""Tests for Terminal() sequences and sequence-awareness.""" -# std imports -try: - from StringIO import StringIO -except ImportError: - from io import StringIO -import platform -import random -import sys -import os - -# local -from .accessories import ( - unsupported_sequence_terminals, - all_terms, - as_subprocess, - TestTerminal, - unicode_parm, - many_columns, - unicode_cap, -) - -# 3rd-party -import pytest -import mock - - -def test_capability(): - """Check that capability lookup works.""" - @as_subprocess - def child(): - # Also test that Terminal grabs a reasonable default stream. This test - # assumes it will be run from a tty. - t = TestTerminal() - sc = unicode_cap('sc') - assert t.save == sc - assert t.save == sc # Make sure caching doesn't screw it up. - - child() - - -def test_capability_without_tty(): - """Assert capability templates are '' when stream is not a tty.""" - @as_subprocess - def child(): - t = TestTerminal(stream=StringIO()) - assert t.save == u'' - assert t.red == u'' - - child() - - -def test_capability_with_forced_tty(): - """force styling should return sequences even for non-ttys.""" - @as_subprocess - def child(): - t = TestTerminal(stream=StringIO(), force_styling=True) - assert t.save == unicode_cap('sc') - - child() - - -def test_parametrization(): - """Test parameterizing a capability.""" - @as_subprocess - def child(): - assert TestTerminal().cup(3, 4) == unicode_parm('cup', 3, 4) - - child() - - -def test_height_and_width(): - """Assert that ``height_and_width()`` returns full integers.""" - @as_subprocess - def child(): - t = TestTerminal() # kind shouldn't matter. - assert isinstance(t.height, int) - assert isinstance(t.width, int) - - child() - - -def test_stream_attr(): - """Make sure Terminal ``stream`` is stdout by default.""" - @as_subprocess - def child(): - assert TestTerminal().stream == sys.__stdout__ - - child() - - -@pytest.mark.skipif(os.environ.get('TRAVIS', None) is not None, - reason="travis-ci does not have binary-packed terminals.") -def test_emit_warnings_about_binpacked(): - """Test known binary-packed terminals (kermit, avatar) emit a warning.""" - from blessings.sequences import _BINTERM_UNSUPPORTED_MSG - from blessings._binterms import binary_terminals - - @as_subprocess - def child(kind): - import warnings - warnings.filterwarnings("error", category=RuntimeWarning) - warnings.filterwarnings("error", category=UserWarning) - - try: - TestTerminal(kind=kind, force_styling=True) - except UserWarning: - err = sys.exc_info()[1] - assert (err.args[0] == _BINTERM_UNSUPPORTED_MSG.format(kind) or - err.args[0].startswith('Unknown parameter in ') or - err.args[0].startswith('Failed to setupterm(') - ), err - else: - assert 'warnings should have been emitted.' - warnings.resetwarnings() - - # any binary terminal should do. - child(binary_terminals[random.randrange(len(binary_terminals))]) - - -def test_unit_binpacked_unittest(): - """Unit Test known binary-packed terminals emit a warning (travis-safe).""" - import warnings - from blessings._binterms import binary_terminals - from blessings.sequences import (_BINTERM_UNSUPPORTED_MSG, - init_sequence_patterns) - warnings.filterwarnings("error", category=UserWarning) - term = mock.Mock() - term.kind = binary_terminals[random.randrange(len(binary_terminals))] - - try: - init_sequence_patterns(term) - except UserWarning: - err = sys.exc_info()[1] - assert err.args[0] == _BINTERM_UNSUPPORTED_MSG.format(term.kind) - else: - assert False, 'Previous stmt should have raised exception.' - warnings.resetwarnings() - - -def test_merge_sequences(): - """Test sequences are filtered and ordered longest-first.""" - from blessings.sequences import _merge_sequences - input_list = [u'a', u'aa', u'aaa', u''] - output_expected = [u'aaa', u'aa', u'a'] - assert (_merge_sequences(input_list) == output_expected) - - -def test_location_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) - with t.location(3, 4): - t.stream.write(u'hi') - expected_output = u''.join( - (unicode_cap('sc'), - unicode_parm('cup', 4, 3), - u'hi', unicode_cap('rc'))) - assert (t.stream.getvalue() == expected_output) - - child_with_styling(all_terms) - - -def test_location_without_styling(): - """Make sure ``location()`` silently passes without styling.""" - @as_subprocess - def child_without_styling(): - """No side effect for location as a context manager without styling.""" - t = TestTerminal(stream=StringIO(), force_styling=None) - - with t.location(3, 4): - t.stream.write(u'hi') - - assert t.stream.getvalue() == u'hi' - - child_without_styling() - - -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) - with t.location(x=5): - pass - expected_output = u''.join( - (unicode_cap('sc'), - unicode_parm('hpa', 5), - unicode_cap('rc'))) - assert (t.stream.getvalue() == expected_output), ( - repr(t.stream.getvalue()), repr(expected_output)) - - # skip 'screen', hpa is proxied (see later tests) - if all_terms != 'screen': - child(all_terms) - - -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) - with t.location(y=5): - pass - expected_output = u''.join( - (unicode_cap('sc'), - unicode_parm('vpa', 5), - unicode_cap('rc'))) - assert (t.stream.getvalue() == expected_output) - - # skip 'screen', vpa is proxied (see later tests) - if all_terms != 'screen': - child(all_terms) - - -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) - COL = 5 - with t.location(x=COL): - pass - expected_output = u''.join( - (unicode_cap('sc'), - u'\x1b[{0}G'.format(COL + 1), - unicode_cap('rc'))) - assert (t.stream.getvalue() == expected_output) - assert (t.move_x(COL) == u'\x1b[{0}G'.format(COL + 1)) - - child('screen') - child('screen-256color') - child('ansi') - - -def test_inject_move_y(): - """Test injection of vpa attribute for screen/ansi (issue #55).""" - @as_subprocess - def child(kind): - t = TestTerminal(kind=kind, stream=StringIO(), force_styling=True) - ROW = 5 - with t.location(y=ROW): - pass - expected_output = u''.join( - (unicode_cap('sc'), - u'\x1b[{0}d'.format(ROW + 1), - unicode_cap('rc'))) - assert (t.stream.getvalue() == expected_output) - assert (t.move_y(ROW) == u'\x1b[{0}d'.format(ROW + 1)) - - child('screen') - child('screen-256color') - child('ansi') - - -def test_inject_civis_and_cnorm_for_ansi(): - """Test injection of cvis attribute for ansi.""" - @as_subprocess - def child(kind): - t = TestTerminal(kind=kind, stream=StringIO(), force_styling=True) - with t.hidden_cursor(): - pass - expected_output = u''.join( - (unicode_cap('sc'), - u'\x1b[?25l\x1b[?25h', - unicode_cap('rc'))) - assert (t.stream.getvalue() == expected_output) - - child('ansi') - - -def test_zero_location(all_terms): - """Make sure ``location()`` pays attention to 0-valued args.""" - @as_subprocess - def child(kind): - t = TestTerminal(kind=kind, stream=StringIO(), force_styling=True) - with t.location(0, 0): - pass - expected_output = u''.join( - (unicode_cap('sc'), - unicode_parm('cup', 0, 0), - unicode_cap('rc'))) - assert (t.stream.getvalue() == expected_output) - - child(all_terms) - - -def test_mnemonic_colors(all_terms): - """Make sure color shortcuts work.""" - @as_subprocess - def child(kind): - def color(t, num): - return t.number_of_colors and unicode_parm('setaf', num) or '' - - def on_color(t, num): - return t.number_of_colors and unicode_parm('setab', num) or '' - - # Avoid testing red, blue, yellow, and cyan, since they might someday - # change depending on terminal type. - t = TestTerminal(kind=kind) - assert (t.white == color(t, 7)) - assert (t.green == color(t, 2)) # Make sure it's different than white. - assert (t.on_black == on_color(t, 0)) - assert (t.on_green == on_color(t, 2)) - assert (t.bright_black == color(t, 8)) - assert (t.bright_green == color(t, 10)) - assert (t.on_bright_black == on_color(t, 8)) - assert (t.on_bright_green == on_color(t, 10)) - - child(all_terms) - - -def test_callable_numeric_colors(all_terms): - """``color(n)`` should return a formatting wrapper.""" - @as_subprocess - def child(kind): - t = TestTerminal(kind=kind) - if t.magenta: - assert t.color(5)('smoo') == t.magenta + 'smoo' + t.normal - else: - assert t.color(5)('smoo') == 'smoo' - - if t.on_magenta: - assert t.on_color(5)('smoo') == t.on_magenta + 'smoo' + t.normal - else: - assert t.color(5)(u'smoo') == 'smoo' - - if t.color(4): - assert t.color(4)(u'smoo') == t.color(4) + u'smoo' + t.normal - else: - assert t.color(4)(u'smoo') == 'smoo' - - if t.on_green: - assert t.on_color(2)('smoo') == t.on_green + u'smoo' + t.normal - else: - assert t.on_color(2)('smoo') == 'smoo' - - if t.on_color(6): - assert t.on_color(6)('smoo') == t.on_color(6) + u'smoo' + t.normal - else: - assert t.on_color(6)('smoo') == 'smoo' - - child(all_terms) - - -def test_null_callable_numeric_colors(all_terms): - """``color(n)`` should be a no-op on null terminals.""" - @as_subprocess - def child(kind): - t = TestTerminal(stream=StringIO(), kind=kind) - assert (t.color(5)('smoo') == 'smoo') - assert (t.on_color(6)('smoo') == 'smoo') - - child(all_terms) - - -def test_naked_color_cap(all_terms): - """``term.color`` should return a stringlike capability.""" - @as_subprocess - def child(kind): - t = TestTerminal(kind=kind) - assert (t.color + '' == t.setaf + '') - - child(all_terms) - - -def test_formatting_functions(all_terms): - """Test simple and compound formatting wrappers.""" - @as_subprocess - def child(kind): - t = TestTerminal(kind=kind) - # test simple sugar, - if t.bold: - expected_output = u''.join((t.bold, u'hi', t.normal)) - else: - expected_output = u'hi' - assert t.bold(u'hi') == expected_output - # Plain strs for Python 2.x - if t.green: - expected_output = u''.join((t.green, 'hi', t.normal)) - else: - expected_output = u'hi' - assert t.green('hi') == expected_output - # Test unicode - if t.underline: - expected_output = u''.join((t.underline, u'boö', t.normal)) - else: - expected_output = u'boö' - assert (t.underline(u'boö') == expected_output) - - if t.subscript: - expected_output = u''.join((t.subscript, u'[1]', t.normal)) - else: - expected_output = u'[1]' - - assert (t.subscript(u'[1]') == expected_output) - - child(all_terms) - - -def test_compound_formatting(all_terms): - """Test simple and compound formatting wrappers.""" - @as_subprocess - def child(kind): - t = TestTerminal(kind=kind) - if any((t.bold, t.green)): - expected_output = u''.join((t.bold, t.green, u'boö', t.normal)) - else: - expected_output = u'boö' - assert t.bold_green(u'boö') == expected_output - - if any((t.on_bright_red, t.bold, t.bright_green, t.underline)): - expected_output = u''.join( - (t.on_bright_red, t.bold, t.bright_green, t.underline, u'meh', - t.normal)) - else: - expected_output = u'meh' - assert (t.on_bright_red_bold_bright_green_underline('meh') - == expected_output) - - child(all_terms) - - -def test_formatting_functions_without_tty(all_terms): - """Test crazy-ass formatting wrappers when there's no tty.""" - @as_subprocess - def child(kind): - t = TestTerminal(kind=kind, stream=StringIO(), force_styling=False) - assert (t.bold(u'hi') == u'hi') - assert (t.green('hi') == u'hi') - # Test non-ASCII chars, no longer really necessary: - assert (t.bold_green(u'boö') == u'boö') - assert (t.bold_underline_green_on_red('loo') == u'loo') - assert (t.on_bright_red_bold_bright_green_underline('meh') == u'meh') - - child(all_terms) - - -def test_nice_formatting_errors(all_terms): - """Make sure you get nice hints if you misspell a formatting wrapper.""" - @as_subprocess - def child(kind): - t = TestTerminal(kind=kind) - try: - t.bold_misspelled('hey') - assert not t.is_a_tty or False, 'Should have thrown exception' - except TypeError: - e = sys.exc_info()[1] - assert 'probably misspelled' in e.args[0] - try: - t.bold_misspelled(u'hey') # unicode - assert not t.is_a_tty or False, 'Should have thrown exception' - except TypeError: - e = sys.exc_info()[1] - assert 'probably misspelled' in e.args[0] - - try: - t.bold_misspelled(None) # an arbitrary non-string - assert not t.is_a_tty or False, 'Should have thrown exception' - except TypeError: - e = sys.exc_info()[1] - assert 'probably misspelled' not in e.args[0] - - if platform.python_implementation() != 'PyPy': - # PyPy fails to toss an exception, Why?! - try: - t.bold_misspelled('a', 'b') # >1 string arg - assert not t.is_a_tty or False, 'Should have thrown exception' - except TypeError: - e = sys.exc_info()[1] - assert 'probably misspelled' in e.args[0], e.args - - child(all_terms) - - -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) - assert (t.clear == '') - assert (t.move(1 == 2) == '') - assert (t.move_x(1) == '') - assert (t.bold() == '') - assert (t.bold('', 'x', 'huh?') == '') - assert (t.bold('', 9876) == '') - assert (t.uhh(9876) == '') - assert (t.clear('x') == 'x') - - child(all_terms) - - -def test_bnc_parameter_emits_warning(): - """A fake capability without target digits emits a warning.""" - import warnings - from blessings.sequences import _build_numeric_capability - - # given, - warnings.filterwarnings("error", category=UserWarning) - term = mock.Mock() - fake_cap = lambda *args: u'NO-DIGIT' - term.fake_cap = fake_cap - - # excersize, - try: - _build_numeric_capability(term, 'fake_cap', base_num=1984) - except UserWarning: - err = sys.exc_info()[1] - assert err.args[0].startswith('Unknown parameter in ') - else: - assert False, 'Previous stmt should have raised exception.' - warnings.resetwarnings() - - -def test_bna_parameter_emits_warning(): - """A fake capability without any digits emits a warning.""" - import warnings - from blessings.sequences import _build_any_numeric_capability - - # given, - warnings.filterwarnings("error", category=UserWarning) - term = mock.Mock() - fake_cap = lambda *args: 'NO-DIGIT' - term.fake_cap = fake_cap - - # excersize, - try: - _build_any_numeric_capability(term, 'fake_cap') - except UserWarning: - err = sys.exc_info()[1] - assert err.args[0].startswith('Missing numerics in ') - else: - assert False, 'Previous stmt should have raised exception.' - warnings.resetwarnings() - - -def test_padd(): - """ Test terminal.padd(seq). """ - @as_subprocess - def child(): - from blessings.sequences import Sequence - from blessings import Terminal - term = Terminal('xterm-256color') - assert Sequence('xyz\b', term).padd() == u'xy' - assert Sequence('xxxx\x1b[3Dzz', term).padd() == u'xzz' - - child() diff --git a/blessed/tests/test_wrap.py b/blessed/tests/test_wrap.py deleted file mode 100644 index e5f7a15..0000000 --- a/blessed/tests/test_wrap.py +++ /dev/null @@ -1,105 +0,0 @@ -import platform -import textwrap -import termios -import struct -import fcntl -import sys - -from .accessories import ( - as_subprocess, - TestTerminal, - many_columns, - all_terms, -) - -import pytest - - -def test_SequenceWrapper_invalid_width(): - """Test exception thrown from invalid width""" - WIDTH = -3 - - @as_subprocess - def child(): - term = TestTerminal() - try: - my_wrapped = term.wrap(u'------- -------------', WIDTH) - except ValueError as err: - assert err.args[0] == ( - "invalid width %r(%s) (must be integer > 0)" % ( - WIDTH, type(WIDTH))) - else: - assert False, 'Previous stmt should have raised exception.' - del my_wrapped # assigned but never used - - child() - - -@pytest.mark.parametrize("kwargs", [ - dict(break_long_words=False, - drop_whitespace=False, - subsequent_indent=''), - dict(break_long_words=False, - drop_whitespace=True, - subsequent_indent=''), - dict(break_long_words=False, - drop_whitespace=False, - subsequent_indent=' '), - dict(break_long_words=False, - drop_whitespace=True, - subsequent_indent=' '), - dict(break_long_words=True, - drop_whitespace=False, - subsequent_indent=''), - dict(break_long_words=True, - drop_whitespace=True, - subsequent_indent=''), - dict(break_long_words=True, - drop_whitespace=False, - subsequent_indent=' '), - dict(break_long_words=True, - drop_whitespace=True, - subsequent_indent=' '), -]) -def test_SequenceWrapper(all_terms, many_columns, kwargs): - """Test that text wrapping matches internal extra options.""" - @as_subprocess - def child(term, width, kwargs): - # build a test paragraph, along with a very colorful version - term = TestTerminal() - pgraph = u' Z! a bc defghij klmnopqrstuvw<<>>xyz012345678900 ' - attributes = ('bright_red', 'on_bright_blue', 'underline', 'reverse', - 'red_reverse', 'red_on_white', 'superscript', - 'subscript', 'on_bright_white') - term.bright_red('x') - term.on_bright_blue('x') - term.underline('x') - term.reverse('x') - term.red_reverse('x') - term.red_on_white('x') - term.superscript('x') - term.subscript('x') - term.on_bright_white('x') - - pgraph_colored = u''.join([ - getattr(term, (attributes[idx % len(attributes)]))(char) - if char != u' ' else u' ' - for idx, char in enumerate(pgraph)]) - - internal_wrapped = textwrap.wrap(pgraph, width=width, **kwargs) - my_wrapped = term.wrap(pgraph, width=width, **kwargs) - my_wrapped_colored = term.wrap(pgraph_colored, width=width, **kwargs) - - # ensure we textwrap ascii the same as python - assert internal_wrapped == my_wrapped - - # ensure content matches for each line, when the sequences are - # stripped back off of each line - for line_no, (left, right) in enumerate( - zip(internal_wrapped, my_wrapped_colored)): - assert left == term.strip_seqs(right) - - # ensure our colored textwrap is the same paragraph length - assert (len(internal_wrapped) == len(my_wrapped_colored)) - - child(all_terms, many_columns, kwargs) diff --git a/blessed/tests/wall.ans b/blessed/tests/wall.ans deleted file mode 100644 index 081b4d2..0000000 --- a/blessed/tests/wall.ans +++ /dev/null @@ -1,7 +0,0 @@ -▄▓▄ - ░░ █▀▀█▀██▀████████▀█▀▀█ █▀▀█▀██▀▀█▀█xz(imp) - ▄▄▄ ▓█████ ▄▄ █▀▀█░ ▀▀▀▀▀ █████▓ ▓█████ ░▓ █▀▀█░ ▓█████ ░▓ █▀▀█░ ░▄ ░░ - ▐▄░▄▀ ▀▀▀▀▀▀ █▓ ▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀ ██ ▀▀▀▀▀ ▀▀▀▀▀▀ ██ ▀▀▀▀▀ █▄▌▌▄▄ - █▓██ ░▓████ ▐ ████░ ░████ ▄█ ████▓░ ░▓████ ██ ████░ ░▓████ ██ ████░ █▐▄▄█░ - ██▓▀ ░▓████ █ ████░ ░████ ▐ ████▓░ ░▓████ ▄█▌████░ ░▓████ ▄█▌████░ ▌█████ - ▀ ░▓████▄█▄▄███ ░ ░ ███▄▄▄▄████▓░ ░▓████▄▄▄▄███ ░ ░▓████▄▄▄▄███ ░ ▀▀▀▓ diff --git a/blessings/__init__.py b/blessings/__init__.py new file mode 100644 index 0000000..3622d0e --- /dev/null +++ b/blessings/__init__.py @@ -0,0 +1,16 @@ +""" +A thin, practical wrapper around terminal capabilities in Python + +http://pypi.python.org/pypi/blessings +""" +import platform as _platform +if ('3', '0', '0') <= _platform.python_version_tuple() < ('3', '2', '2+'): + # Good till 3.2.10 + # Python 3.x < 3.2.3 has a bug in which tparm() erroneously takes a string. + raise ImportError('Blessings needs Python 3.2.3 or greater for Python 3 ' + 'support due to http://bugs.python.org/issue10570.') + + +from .terminal import Terminal + +__all__ = ('Terminal',) diff --git a/blessings/_binterms.py b/blessings/_binterms.py new file mode 100644 index 0000000..6c93ee0 --- /dev/null +++ b/blessings/_binterms.py @@ -0,0 +1,872 @@ +""" 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. +#: +#: This may be generated by exporting TEST_BINTERMS, then analyzing the +#: jUnit result xml written to the project folder. +binary_terminals = u""" +9term +aaa+dec +aaa+rv +aaa+rv-100 +aaa+rv-30 +aaa-rv-unk +abm80 +abm85 +abm85e +abm85h +abm85h-old +act4 +act5 +addrinfo +adds980 +adm+sgr +adm+sgr-100 +adm+sgr-30 +adm11 +adm1178 +adm12 +adm1a +adm2 +adm20 +adm21 +adm22 +adm3 +adm31 +adm31-old +adm3a +adm3a+ +adm42 +adm42-ns +adm5 +aepro +aj510 +aj830 +alto-h19 +altos4 +altos7 +altos7pc +ampex175 +ampex175-b +ampex210 +ampex232 +ampex232w +ampex80 +annarbor4080 +ansi+arrows +ansi+csr +ansi+cup +ansi+enq +ansi+erase +ansi+idc +ansi+idl +ansi+idl1 +ansi+inittabs +ansi+local +ansi+local1 +ansi+pp +ansi+rca +ansi+rep +ansi+sgr +ansi+sgrbold +ansi+sgrdim +ansi+sgrso +ansi+sgrul +ansi+tabs +ansi-color-2-emx +ansi-color-3-emx +ansi-emx +ansi-mini +ansi-mr +ansi-mtabs +ansi-nt +ansi.sys +ansi.sys-old +ansi.sysk +ansi77 +apollo +apple-80 +apple-ae +apple-soroc +apple-uterm +apple-uterm-vb +apple-videx +apple-videx2 +apple-videx3 +apple-vm80 +apple2e +apple2e-p +apple80p +appleII +appleIIgs +atari +att4415+nl +att4420 +att4424m +att5310 +att5310-100 +att5310-30 +att5620-s +avatar +avatar0 +avatar0+ +avt+s +aws +awsc +bantam +basis +beacon +beehive +blit +bq300-8 +bq300-8-pc +bq300-8-pc-rv +bq300-8-pc-w +bq300-8-pc-w-rv +bq300-8rv +bq300-8w +bq300-w-8rv +c100 +c100-rv +c108 +c108-4p +c108-rv +c108-rv-4p +c108-w +ca22851 +cbblit +cbunix +cci +cci-100 +cci-30 +cdc456 +cdc721 +cdc721-esc +cdc721-esc-100 +cdc721-esc-30 +cdc721ll +cdc752 +cdc756 +cit101e +cit101e-132 +cit101e-n +cit101e-n132 +cit80 +citoh +citoh-6lpi +citoh-8lpi +citoh-comp +citoh-elite +citoh-pica +citoh-prop +coco3 +color_xterm +commodore +contel300 +contel301 +cops10 +ct8500 +ctrm +ctrm-100 +ctrm-30 +cyb110 +cyb83 +d132 +d200 +d200-100 +d200-30 +d210-dg +d210-dg-100 +d210-dg-30 +d211-dg +d211-dg-100 +d211-dg-30 +d216-dg +d216-dg-100 +d216-dg-30 +d216-unix +d216-unix-25 +d217-unix +d217-unix-25 +d220 +d220-100 +d220-30 +d220-7b +d220-7b-100 +d220-7b-30 +d220-dg +d220-dg-100 +d220-dg-30 +d230c +d230c-100 +d230c-30 +d230c-dg +d230c-dg-100 +d230c-dg-30 +d400 +d400-100 +d400-30 +d410-dg +d410-dg-100 +d410-dg-30 +d412-dg +d412-dg-100 +d412-dg-30 +d412-unix +d412-unix-25 +d412-unix-s +d412-unix-sr +d412-unix-w +d413-unix +d413-unix-25 +d413-unix-s +d413-unix-sr +d413-unix-w +d414-unix +d414-unix-25 +d414-unix-s +d414-unix-sr +d414-unix-w +d430c-dg +d430c-dg-100 +d430c-dg-30 +d430c-dg-ccc +d430c-dg-ccc-100 +d430c-dg-ccc-30 +d430c-unix +d430c-unix-100 +d430c-unix-25 +d430c-unix-25-100 +d430c-unix-25-30 +d430c-unix-25-ccc +d430c-unix-30 +d430c-unix-ccc +d430c-unix-s +d430c-unix-s-ccc +d430c-unix-sr +d430c-unix-sr-ccc +d430c-unix-w +d430c-unix-w-ccc +d470c +d470c-7b +d470c-dg +d555-dg +d577-dg +d800 +delta +dg+ccc +dg+color +dg+color8 +dg+fixed +dg-generic +dg200 +dg210 +dg211 +dg450 +dg460-ansi +dg6053 +dg6053-old +dgkeys+11 +dgkeys+15 +dgkeys+7b +dgkeys+8b +dgmode+color +dgmode+color8 +dgunix+ccc +dgunix+fixed +diablo1620 +diablo1620-m8 +diablo1640 +diablo1640-lm +diablo1740-lm +digilog +djgpp203 +dm1520 +dm2500 +dm3025 +dm3045 +dmchat +dmterm +dp8242 +dt100 +dt100w +dt110 +dt80-sas +dtc300s +dtc382 +dumb +dw1 +dw2 +dw3 +dw4 +dwk +ecma+color +ecma+sgr +elks +elks-glasstty +elks-vt52 +emu +ep40 +ep48 +esprit +esprit-am +ex155 +f100 +f100-rv +f110 +f110-14 +f110-14w +f110-w +f200 +f200-w +f200vi +f200vi-w +falco +falco-p +fos +fox +gator-52 +gator-52t +glasstty +gnome +gnome+pcfkeys +gnome-2007 +gnome-2008 +gnome-256color +gnome-fc5 +gnome-rh72 +gnome-rh80 +gnome-rh90 +go140 +go140w +gs6300 +gsi +gt40 +gt42 +guru+rv +guru+s +h19 +h19-bs +h19-g +h19-u +h19-us +h19k +ha8675 +ha8686 +hft-old +hmod1 +hp+arrows +hp+color +hp+labels +hp+pfk+arrows +hp+pfk+cr +hp+pfk-cr +hp+printer +hp2 +hp236 +hp262x +hp2641a +hp300h +hp700-wy +hp70092 +hp9837 +hp9845 +hpterm +hpterm-color +hz1000 +hz1420 +hz1500 +hz1510 +hz1520 +hz1520-noesc +hz1552 +hz1552-rv +hz2000 +i100 +i400 +ibcs2 +ibm+16color +ibm+color +ibm-apl +ibm-system1 +ibm3101 +ibm3151 +ibm3161 +ibm3161-C +ibm3162 +ibm3164 +ibm327x +ibm5081-c +ibm8514-c +ibmaed +ibmapa8c +ibmapa8c-c +ibmega +ibmega-c +ibmmono +ibmvga +ibmvga-c +icl6404 +icl6404-w +ifmr +ims-ansi +ims950 +ims950-b +ims950-rv +intertube +intertube2 +intext +intext2 +kaypro +kermit +kermit-am +klone+acs +klone+color +klone+koi8acs +klone+sgr +klone+sgr-dumb +klone+sgr8 +konsole +konsole+pcfkeys +konsole-16color +konsole-256color +konsole-base +konsole-linux +konsole-solaris +konsole-vt100 +konsole-vt420pc +konsole-xf3x +konsole-xf4x +kt7 +kt7ix +ln03 +ln03-w +lpr +luna +megatek +mgterm +microb +mime +mime-fb +mime-hb +mime2a +mime2a-s +mime314 +mime3a +mime3ax +minitel1 +minitel1b +mlterm+pcfkeys +mm340 +modgraph2 +msk227 +msk22714 +msk227am +mt70 +ncr160vppp +ncr160vpwpp +ncr160wy50+pp +ncr160wy50+wpp +ncr160wy60pp +ncr160wy60wpp +ncr260vppp +ncr260vpwpp +ncr260wy325pp +ncr260wy325wpp +ncr260wy350pp +ncr260wy350wpp +ncr260wy50+pp +ncr260wy50+wpp +ncr260wy60pp +ncr260wy60wpp +ncr7900i +ncr7900iv +ncr7901 +ncrvt100an +ncrvt100wan +ndr9500 +ndr9500-25 +ndr9500-25-mc +ndr9500-25-mc-nl +ndr9500-25-nl +ndr9500-mc +ndr9500-mc-nl +ndr9500-nl +nec5520 +newhpkeyboard +nextshell +northstar +nsterm+c +nsterm+c41 +nsterm+s +nwp511 +oblit +oc100 +oldpc3 +origpc3 +osborne +osborne-w +osexec +otek4112 +owl +p19 +pc-coherent +pc-venix +pc6300plus +pcix +pckermit +pckermit120 +pe1251 +pe7000c +pe7000m +pilot +pmcons +prism2 +prism4 +prism5 +pro350 +psterm-fast +psterm-fast-100 +psterm-fast-30 +pt100 +pt210 +pt250 +pty +qansi +qansi-g +qansi-m +qansi-t +qansi-w +qdss +qnx +qnx-100 +qnx-30 +qnxm +qnxm-100 +qnxm-30 +qnxt +qnxt-100 +qnxt-30 +qnxt2 +qnxtmono +qnxtmono-100 +qnxtmono-30 +qnxw +qnxw-100 +qnxw-30 +qume5 +qvt101 +qvt101+ +qvt102 +qvt119+ +qvt119+-25 +qvt119+-25-w +qvt119+-w +rbcomm +rbcomm-nam +rbcomm-w +rca +regent100 +regent20 +regent25 +regent40 +regent40+ +regent60 +rt6221 +rt6221-w +rtpc +rxvt+pcfkeys +scanset +screen+fkeys +screen-16color +screen-16color-bce +screen-16color-bce-s +screen-16color-s +screen-256color +screen-256color-bce +screen-256color-bce-s +screen-256color-s +screen-bce +screen-s +screen-w +screen.linux +screen.rxvt +screen.teraterm +screen.xterm-r6 +screen2 +screen3 +screwpoint +sibo +simterm +soroc120 +soroc140 +st52 +superbee-xsb +superbeeic +superbrain +swtp +synertek +t10 +t1061 +t1061f +t3700 +t3800 +tandem6510 +tandem653 +tandem653-100 +tandem653-30 +tek +tek4013 +tek4014 +tek4014-sm +tek4015 +tek4015-sm +tek4023 +tek4105 +tek4107 +tek4113-nd +tek4205 +tek4205-100 +tek4205-30 +tek4207-s +teraterm +teraterm2.3 +teraterm4.59 +terminet1200 +ti700 +ti931 +trs16 +trs2 +tt +tty33 +tty37 +tty43 +tvi803 +tvi9065 +tvi910 +tvi910+ +tvi912 +tvi912b +tvi912b+2p +tvi912b+dim +tvi912b+dim-100 +tvi912b+dim-30 +tvi912b+mc +tvi912b+mc-100 +tvi912b+mc-30 +tvi912b+printer +tvi912b+vb +tvi912b-2p +tvi912b-2p-mc +tvi912b-2p-mc-100 +tvi912b-2p-mc-30 +tvi912b-2p-p +tvi912b-2p-unk +tvi912b-mc +tvi912b-mc-100 +tvi912b-mc-30 +tvi912b-p +tvi912b-unk +tvi912b-vb +tvi912b-vb-mc +tvi912b-vb-mc-100 +tvi912b-vb-mc-30 +tvi912b-vb-p +tvi912b-vb-unk +tvi920b +tvi920b+fn +tvi920b-2p +tvi920b-2p-mc +tvi920b-2p-mc-100 +tvi920b-2p-mc-30 +tvi920b-2p-p +tvi920b-2p-unk +tvi920b-mc +tvi920b-mc-100 +tvi920b-mc-30 +tvi920b-p +tvi920b-unk +tvi920b-vb +tvi920b-vb-mc +tvi920b-vb-mc-100 +tvi920b-vb-mc-30 +tvi920b-vb-p +tvi920b-vb-unk +tvi921 +tvi924 +tvi925 +tvi925-hi +tvi92B +tvi92D +tvi950 +tvi950-2p +tvi950-4p +tvi950-rv +tvi950-rv-2p +tvi950-rv-4p +tvipt +unknown +vanilla +vc303 +vc404 +vc404-s +vc414 +vc415 +vi200 +vi200-f +vi200-rv +vi50 +vi500 +vi50adm +vi55 +viewpoint +vp3a+ +vp60 +vp90 +vremote +vt100+enq +vt100+fnkeys +vt100+keypad +vt100+pfkeys +vt100-s +vt102+enq +vt200-js +vt220+keypad +vt50h +vt52 +vt61 +wsiris +wy100 +wy100q +wy120 +wy120-25 +wy120-vb +wy160 +wy160-25 +wy160-42 +wy160-43 +wy160-tek +wy160-tek-100 +wy160-tek-30 +wy160-vb +wy30 +wy30-mc +wy30-mc-100 +wy30-mc-30 +wy30-vb +wy325 +wy325-25 +wy325-42 +wy325-43 +wy325-vb +wy350 +wy350-100 +wy350-30 +wy350-vb +wy350-vb-100 +wy350-vb-30 +wy350-w +wy350-w-100 +wy350-w-30 +wy350-wvb +wy350-wvb-100 +wy350-wvb-30 +wy370 +wy370-100 +wy370-105k +wy370-105k-100 +wy370-105k-30 +wy370-30 +wy370-EPC +wy370-EPC-100 +wy370-EPC-30 +wy370-nk +wy370-nk-100 +wy370-nk-30 +wy370-rv +wy370-rv-100 +wy370-rv-30 +wy370-tek +wy370-tek-100 +wy370-tek-30 +wy370-vb +wy370-vb-100 +wy370-vb-30 +wy370-w +wy370-w-100 +wy370-w-30 +wy370-wvb +wy370-wvb-100 +wy370-wvb-30 +wy50 +wy50-mc +wy50-mc-100 +wy50-mc-30 +wy50-vb +wy60 +wy60-25 +wy60-42 +wy60-43 +wy60-vb +wy99-ansi +wy99a-ansi +wy99f +wy99fa +wy99gt +wy99gt-25 +wy99gt-vb +wy99gt-tek +wyse-vp +xerox1720 +xerox820 +xfce +xnuppc+100x37 +xnuppc+112x37 +xnuppc+128x40 +xnuppc+128x48 +xnuppc+144x48 +xnuppc+160x64 +xnuppc+200x64 +xnuppc+200x75 +xnuppc+256x96 +xnuppc+80x25 +xnuppc+80x30 +xnuppc+90x30 +xnuppc+c +xnuppc+c-100 +xnuppc+c-30 +xtalk +xtalk-100 +xtalk-30 +xterm+256color +xterm+256color-100 +xterm+256color-30 +xterm+88color +xterm+88color-100 +xterm+88color-30 +xterm+app +xterm+edit +xterm+noapp +xterm+pc+edit +xterm+pcc0 +xterm+pcc1 +xterm+pcc2 +xterm+pcc3 +xterm+pce2 +xterm+pcf0 +xterm+pcf2 +xterm+pcfkeys +xterm+r6f2 +xterm+vt+edit +xterm-vt52 +z100 +z100bw +z29 +zen30 +zen50 +ztx +""".split() + +__all__ = ('binary_terminals',) diff --git a/blessings/formatters.py b/blessings/formatters.py new file mode 100644 index 0000000..54f090c --- /dev/null +++ b/blessings/formatters.py @@ -0,0 +1,332 @@ +"This sub-module provides formatting functions." +import curses +import sys + +_derivatives = ('on', 'bright', 'on_bright',) + +_colors = set('black red green yellow blue magenta cyan white'.split()) +_compoundables = set('bold underline reverse blink dim italic shadow ' + 'standout subscript superscript'.split()) + +#: Valid colors and their background (on), bright, and bright-bg derivatives. +COLORS = set(['_'.join((derivitive, color)) + for derivitive in _derivatives + for color in _colors]) | _colors + +#: All valid compoundable names. +COMPOUNDABLES = (COLORS | _compoundables) + +if sys.version_info[0] == 3: + text_type = str + basestring = str +else: + text_type = unicode # noqa + + +class ParameterizingString(text_type): + """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') + u'\x1b[91mcolor #9\x1b(B\x1b[m' + """ + + def __new__(cls, *args): + """P.__new__(cls, cap, [normal, [name]]) + + :arg cap: parameterized string suitable for curses.tparm() + :arg normal: terminating sequence for this capability. + :arg name: name of this terminal capability. + """ + assert len(args) and len(args) < 4, args + new = text_type.__new__(cls, args[0]) + new._normal = len(args) > 1 and args[1] or u'' + new._name = len(args) > 2 and args[2] or u'' + return new + + def __call__(self, *args): + """P(*args) -> FormattingString() + + Return evaluated terminal capability (self), receiving arguments + ``*args``, followed by the terminating sequence (self.normal) into + a FormattingString capable of being called. + """ + try: + # Re-encode the cap, because tparm() takes a bytestring in Python + # 3. However, appear to be a plain Unicode string otherwise so + # concats work. + attr = curses.tparm(self.encode('latin1'), *args).decode('latin1') + return FormattingString(attr, self._normal) + except TypeError as err: + # If the first non-int (i.e. incorrect) arg was a string, suggest + # something intelligent: + if len(args) and isinstance(args[0], basestring): + raise TypeError( + "A native or nonexistent capability template, %r received" + " invalid argument %r: %s. You probably misspelled a" + " formatting call like `bright_red'" % ( + self._name, args, err)) + # Somebody passed a non-string; I don't feel confident + # guessing what they were trying to do. + raise + except curses.error as err: + # ignore 'tparm() returned NULL', you won't get any styling, + # even if does_styling is True. This happens on win32 platforms + # with http://www.lfd.uci.edu/~gohlke/pythonlibs/#curses installed + if "tparm() returned NULL" not in text_type(err): + raise + return NullCallableString() + + +class ParameterizingProxyString(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' + """ + + def __new__(cls, *args): + """P.__new__(cls, (fmt, callable), [normal, [name]]) + + :arg fmt: format string suitable for displaying terminal sequences. + :arg callable: receives __call__ arguments for formatting fmt. + :arg normal: terminating sequence for this capability. + :arg name: name of this terminal capability. + """ + assert len(args) and len(args) < 4, args + assert type(args[0]) is tuple, args[0] + assert callable(args[0][1]), args[0][1] + new = text_type.__new__(cls, args[0][0]) + new._fmt_args = args[0][1] + new._normal = len(args) > 1 and args[1] or u'' + new._name = len(args) > 2 and args[2] or u'' + return new + + def __call__(self, *args): + """P(*args) -> FormattingString() + + Return evaluated terminal capability format, (self), using callable + ``self._fmt_args`` receiving arguments ``*args``, followed by the + terminating sequence (self.normal) into a FormattingString capable + of being called. + """ + return FormattingString(self.format(*self._fmt_args(*args)), + self._normal) + + +def get_proxy_string(term, attr): + """ 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. + """ + # normalize 'screen-256color', or 'ansi.sys' to its basic names + term_kind = next(iter(_kind for _kind in ('screen', 'ansi',) + if term.kind.startswith(_kind)), term) + return { + 'screen': { + # proxy move_x/move_y for 'screen' terminal type. + 'hpa': ParameterizingProxyString( + (u'\x1b[{0}G', lambda *arg: (arg[0] + 1,)), term.normal, attr), + 'vpa': ParameterizingProxyString( + (u'\x1b[{0}d', lambda *arg: (arg[0] + 1,)), term.normal, attr), + }, + 'ansi': { + # proxy show/hide cursor for 'ansi' terminal type. + 'civis': ParameterizingProxyString( + (u'\x1b[?25l', lambda *arg: ()), term.normal, attr), + 'cnorm': ParameterizingProxyString( + (u'\x1b[?25h', lambda *arg: ()), term.normal, attr), + 'hpa': ParameterizingProxyString( + (u'\x1b[{0}G', lambda *arg: (arg[0] + 1,)), term.normal, attr), + 'vpa': ParameterizingProxyString( + (u'\x1b[{0}d', lambda *arg: (arg[0] + 1,)), term.normal, attr), + } + }.get(term_kind, {}).get(attr, None) + + +class FormattingString(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' + """ + + def __new__(cls, *args): + """P.__new__(cls, sequence, [normal]) + :arg sequence: terminal attribute sequence. + :arg normal: terminating sequence for this attribute. + """ + assert 1 <= len(args) <= 2, args + new = text_type.__new__(cls, args[0]) + new._normal = len(args) > 1 and args[1] or u'' + return new + + def __call__(self, text): + """P(text) -> unicode + + Return string ``text``, joined by specified video attribute, + (self), and followed by reset attribute sequence (term.normal). + """ + if len(self): + return u''.join((self, text, self._normal)) + return text + + +class NullCallableString(text_type): + """A dummy callable Unicode to stand in for ``FormattingString`` and + ``ParameterizingString`` for terminals that cannot perform styling. + """ + def __new__(cls): + new = 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). + + 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 + terminfo(5) which accept non-int arguments, they are seldom used. + + When called with a non-int as the first arg (no no args at all), return + the first arg, acting in place of ``FormattingString`` without + any attributes. + """ + if len(args) != 1 or isinstance(args[0], int): + # I am acting as a ParameterizingString. + + # tparm can take not only ints but also (at least) strings as its + # 2nd...nth argument. But we don't support callable parameterizing + # capabilities that take non-ints yet, so we can cheap out here. + + # TODO(erikrose): Go through enough of the motions in the + # capability resolvers to determine which of 2 special-purpose + # classes, NullParameterizableString or NullFormattingString, + # to return, and retire this one. + + # As a NullCallableString, even when provided with a parameter, + # such as t.color(5), we must also still be callable, fe: + # + # >>> t.color(5)('shmoo') + # + # is actually simplified result of NullCallable()() on terminals + # without color support, so turtles all the way down: we return + # another instance. + return NullCallableString() + return args[0] + + +def split_compound(compound): + """Split a possibly compound format string into segments. + + >>> split_compound('bold_underline_bright_blue_on_red') + ['bold', 'underline', 'bright_blue', 'on_red'] + + """ + merged_segs = [] + # These occur only as prefixes, so they can always be merged: + mergeable_prefixes = ['on', 'bright', 'on_bright'] + for s in compound.split('_'): + if merged_segs and merged_segs[-1] in mergeable_prefixes: + merged_segs[-1] += '_' + s + else: + merged_segs.append(s) + return merged_segs + + +def resolve_capability(term, attr): + """Return a Unicode string for the terminal capability ``attr``, + or an empty string if not found, or if terminal is without styling + capabilities. + """ + # Decode sequences as latin1, as they are always 8-bit bytes, so when + # b'\xff' is returned, this must be decoded to u'\xff'. + if not term.does_styling: + return u'' + val = curses.tigetstr(term._sugar.get(attr, attr)) + return u'' if val is None else val.decode('latin1') + + +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``. + """ + # NOTE(erikrose): Does curses automatically exchange red and blue and cyan + # and yellow when a terminal supports setf/setb rather than setaf/setab? + # I'll be blasted if I can find any documentation. The following + # assumes it does. + color_cap = (term._background_color if 'on_' in color else + term._foreground_color) + + # curses constants go up to only 7, so add an offset to get at the + # bright colors at 8-15: + offset = 8 if 'bright_' in color else 0 + base_color = color.rsplit('_', 1)[-1] + if term.number_of_colors == 0: + return NullCallableString() + + attr = 'COLOR_%s' % (base_color.upper(),) + fmt_attr = color_cap(getattr(curses, attr) + offset) + return FormattingString(fmt_attr, term.normal) + + +def resolve_attribute(term, attr): + """Resolve a sugary or plain capability name, color, or compound + formatting name into a *callable* unicode string capability, + ``ParameterizingString`` or ``FormattingString``. + """ + # A simple color, such as `red' or `blue'. + if attr in COLORS: + return resolve_color(term, attr) + + # A direct compoundable, such as `bold' or `on_red'. + if attr in COMPOUNDABLES: + sequence = resolve_capability(term, attr) + return FormattingString(sequence, term.normal) + + # Given `bold_on_red', resolve to ('bold', 'on_red'), RECURSIVE + # call for each compounding section, joined and returned as + # a completed completed FormattingString. + formatters = split_compound(attr) + if all(fmt in COMPOUNDABLES for fmt in formatters): + resolution = (resolve_attribute(term, fmt) for fmt in formatters) + return FormattingString(u''.join(resolution), term.normal) + else: + # otherwise, this is our end-game: given a sequence such as 'csr' + # (change scrolling region), return a ParameterizingString instance, + # that when called, performs and returns the final string after curses + # capability lookup is performed. + tparm_capseq = resolve_capability(term, attr) + if not tparm_capseq: + # and, for special terminals, such as 'screen', provide a Proxy + # ParameterizingString for attributes they do not claim to support, + # but actually do! (such as 'hpa' and 'vpa'). + proxy = get_proxy_string(term, term._sugar.get(attr, attr)) + if proxy is not None: + return proxy + return ParameterizingString(tparm_capseq, term.normal, attr) diff --git a/blessings/keyboard.py b/blessings/keyboard.py new file mode 100644 index 0000000..280606d --- /dev/null +++ b/blessings/keyboard.py @@ -0,0 +1,276 @@ +"This sub-module provides 'keyboard awareness'." + +__author__ = 'Jeff Quast ' +__license__ = 'MIT' + +__all__ = ('Keystroke', 'get_keyboard_codes', 'get_keyboard_sequences',) + +import curses.has_key +import collections +import curses +import sys + +if hasattr(collections, 'OrderedDict'): + OrderedDict = collections.OrderedDict +else: + # python 2.6 requires 3rd party library + import ordereddict + OrderedDict = ordereddict.OrderedDict + +get_curses_keycodes = lambda: dict( + ((keyname, getattr(curses, keyname)) + for keyname in dir(curses) + if keyname.startswith('KEY_')) +) + +# override a few curses constants with easier mnemonics, +# there may only be a 1:1 mapping, so for those who desire +# to use 'KEY_DC' from, perhaps, ported code, recommend +# that they simply compare with curses.KEY_DC. +CURSES_KEYCODE_OVERRIDE_MIXIN = ( + ('KEY_DELETE', curses.KEY_DC), + ('KEY_INSERT', curses.KEY_IC), + ('KEY_PGUP', curses.KEY_PPAGE), + ('KEY_PGDOWN', curses.KEY_NPAGE), + ('KEY_ESCAPE', curses.KEY_EXIT), + ('KEY_SUP', curses.KEY_SR), + ('KEY_SDOWN', curses.KEY_SF), + ('KEY_UP_LEFT', curses.KEY_A1), + ('KEY_UP_RIGHT', curses.KEY_A3), + ('KEY_CENTER', curses.KEY_B2), + ('KEY_BEGIN', curses.KEY_BEG), +) + +# Inject KEY_{names} that we think would be useful, there are no curses +# definitions for the keypad keys. We need keys that generate multibyte +# sequences, though it is useful to have some aliases for basic control +# characters such as TAB. +_lastval = max(get_curses_keycodes().values()) +for key in ('TAB', 'KP_MULTIPLY', 'KP_ADD', 'KP_SEPARATOR', 'KP_SUBTRACT', + 'KP_DECIMAL', 'KP_DIVIDE', 'KP_EQUAL', 'KP_0', 'KP_1', 'KP_2', + 'KP_3', 'KP_4', 'KP_5', 'KP_6', 'KP_7', 'KP_8', 'KP_9'): + _lastval += 1 + setattr(curses, 'KEY_{0}'.format(key), _lastval) + +if sys.version_info[0] == 3: + text_type = str + unichr = chr +else: + text_type = unicode # noqa + + +class Keystroke(text_type): + """A unicode-derived class for describing keyboard input returned by + the ``inkey()`` method of ``Terminal``, which may, at times, be a + multibyte sequence, providing properties ``is_sequence`` as ``True`` + when the string is a known sequence, and ``code``, which returns an + integer value that may be compared against the terminal class attributes + such as ``KEY_LEFT``. + """ + def __new__(cls, ucs='', code=None, name=None): + new = text_type.__new__(cls, ucs) + new._name = name + new._code = code + return new + + @property + def is_sequence(self): + "Whether the value represents a multibyte sequence (bool)." + return self._code is not None + + def __repr__(self): + return self._name is None and text_type.__repr__(self) or self._name + __repr__.__doc__ = text_type.__doc__ + + @property + def name(self): + "String-name of key sequence, such as ``'KEY_LEFT'`` (str)." + return self._name + + @property + def code(self): + "Integer keycode value of multibyte sequence (int)." + return self._code + + +def get_keyboard_codes(): + """get_keyboard_codes() -> dict + + Returns dictionary of (code, name) pairs for curses keyboard constant + values and their mnemonic name. Such as key ``260``, with the value of + its identity, ``KEY_LEFT``. These are derived from the attributes by the + same of the curses module, with the following exceptions: + + * ``KEY_DELETE`` in place of ``KEY_DC`` + * ``KEY_INSERT`` in place of ``KEY_IC`` + * ``KEY_PGUP`` in place of ``KEY_PPAGE`` + * ``KEY_PGDOWN`` in place of ``KEY_NPAGE`` + * ``KEY_ESCAPE`` in place of ``KEY_EXIT`` + * ``KEY_SUP`` in place of ``KEY_SR`` + * ``KEY_SDOWN`` in place of ``KEY_SF`` + """ + keycodes = OrderedDict(get_curses_keycodes()) + keycodes.update(CURSES_KEYCODE_OVERRIDE_MIXIN) + + # invert dictionary (key, values) => (values, key), preferring the + # last-most inserted value ('KEY_DELETE' over 'KEY_DC'). + return dict(zip(keycodes.values(), keycodes.keys())) + + +def _alternative_left_right(term): + """_alternative_left_right(T) -> dict + + Return dict of sequences ``term._cuf1``, and ``term._cub1``, + valued as ``KEY_RIGHT``, ``KEY_LEFT`` when appropriate if available. + + some terminals report a different value for *kcuf1* than *cuf1*, but + actually send the value of *cuf1* for right arrow key (which is + non-destructive space). + """ + keymap = dict() + if term._cuf1 and term._cuf1 != u' ': + keymap[term._cuf1] = curses.KEY_RIGHT + if term._cub1 and term._cub1 != u'\b': + keymap[term._cub1] = curses.KEY_LEFT + return keymap + + +def get_keyboard_sequences(term): + """get_keyboard_sequences(T) -> (OrderedDict) + + Initialize and return a keyboard map and sequence lookup table, + (sequence, constant) from blessings Terminal instance ``term``, + where ``sequence`` is a multibyte input sequence, such as u'\x1b[D', + and ``constant`` is a constant, such as term.KEY_LEFT. The return + value is an OrderedDict instance, with their keys sorted longest-first. + """ + # A small gem from curses.has_key that makes this all possible, + # _capability_names: a lookup table of terminal capability names for + # keyboard sequences (fe. kcub1, key_left), keyed by the values of + # constants found beginning with KEY_ in the main curses module + # (such as KEY_LEFT). + # + # latin1 encoding is used so that bytes in 8-bit range of 127-255 + # have equivalent chr() and unichr() values, so that the sequence + # of a kermit or avatar terminal, for example, remains unchanged + # in its byte sequence values even when represented by unicode. + # + capability_names = curses.has_key._capability_names + sequence_map = dict(( + (seq.decode('latin1'), val) + for (seq, val) in ( + (curses.tigetstr(cap), val) + for (val, cap) in capability_names.items() + ) if seq + ) if term.does_styling else ()) + + sequence_map.update(_alternative_left_right(term)) + sequence_map.update(DEFAULT_SEQUENCE_MIXIN) + + # This is for fast lookup matching of sequences, preferring + # full-length sequence such as ('\x1b[D', KEY_LEFT) + # over simple sequences such as ('\x1b', KEY_EXIT). + return OrderedDict(( + (seq, sequence_map[seq]) for seq in sorted( + sequence_map.keys(), key=len, reverse=True))) + + +def resolve_sequence(text, mapper, codes): + """resolve_sequence(text, mapper, codes) -> Keystroke() + + Returns first matching Keystroke() instance for sequences found in + ``mapper`` beginning with input ``text``, where ``mapper`` is an + OrderedDict of unicode multibyte sequences, such as u'\x1b[D' paired by + their integer value (260), and ``codes`` is a dict of integer values (260) + paired by their mnemonic name, 'KEY_LEFT'. + """ + for sequence, code in mapper.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. + +This goes for many: rxvt, putty, iTerm. + +These "mixins" are used for *all* terminals, regardless of their type. + +Furthermore, curses does not provide sequences sent by the keypad, +at least, it does not provide a way to distinguish between keypad 0 +and numeric 0. +""" +DEFAULT_SEQUENCE_MIXIN = ( + # these common control characters (and 127, ctrl+'?') mapped to + # an application key definition. + (unichr(10), curses.KEY_ENTER), + (unichr(13), curses.KEY_ENTER), + (unichr(8), curses.KEY_BACKSPACE), + (unichr(9), curses.KEY_TAB), + (unichr(27), curses.KEY_EXIT), + (unichr(127), curses.KEY_DC), + + (u"\x1b[A", curses.KEY_UP), + (u"\x1b[B", curses.KEY_DOWN), + (u"\x1b[C", curses.KEY_RIGHT), + (u"\x1b[D", curses.KEY_LEFT), + (u"\x1b[F", curses.KEY_END), + (u"\x1b[H", curses.KEY_HOME), + # not sure where these are from .. please report + (u"\x1b[K", curses.KEY_END), + (u"\x1b[U", curses.KEY_NPAGE), + (u"\x1b[V", curses.KEY_PPAGE), + + # keys sent after term.smkx (keypad_xmit) is emitted, source: + # http://www.xfree86.org/current/ctlseqs.html#PC-Style%20Function%20Keys + # http://fossies.org/linux/rxvt/doc/rxvtRef.html#KeyCodes + # + # keypad, numlock on + (u"\x1bOM", curses.KEY_ENTER), # return + (u"\x1bOj", curses.KEY_KP_MULTIPLY), # * + (u"\x1bOk", curses.KEY_KP_ADD), # + + (u"\x1bOl", curses.KEY_KP_SEPARATOR), # , + (u"\x1bOm", curses.KEY_KP_SUBTRACT), # - + (u"\x1bOn", curses.KEY_KP_DECIMAL), # . + (u"\x1bOo", curses.KEY_KP_DIVIDE), # / + (u"\x1bOX", curses.KEY_KP_EQUAL), # = + (u"\x1bOp", curses.KEY_KP_0), # 0 + (u"\x1bOq", curses.KEY_KP_1), # 1 + (u"\x1bOr", curses.KEY_KP_2), # 2 + (u"\x1bOs", curses.KEY_KP_3), # 3 + (u"\x1bOt", curses.KEY_KP_4), # 4 + (u"\x1bOu", curses.KEY_KP_5), # 5 + (u"\x1bOv", curses.KEY_KP_6), # 6 + (u"\x1bOw", curses.KEY_KP_7), # 7 + (u"\x1bOx", curses.KEY_KP_8), # 8 + (u"\x1bOy", curses.KEY_KP_9), # 9 + + # keypad, numlock off + (u"\x1b[1~", curses.KEY_FIND), # find + (u"\x1b[2~", curses.KEY_IC), # insert (0) + (u"\x1b[3~", curses.KEY_DC), # delete (.), "Execute" + (u"\x1b[4~", curses.KEY_SELECT), # select + (u"\x1b[5~", curses.KEY_PPAGE), # pgup (9) + (u"\x1b[6~", curses.KEY_NPAGE), # pgdown (3) + (u"\x1b[7~", curses.KEY_HOME), # home + (u"\x1b[8~", curses.KEY_END), # end + (u"\x1b[OA", curses.KEY_UP), # up (8) + (u"\x1b[OB", curses.KEY_DOWN), # down (2) + (u"\x1b[OC", curses.KEY_RIGHT), # right (6) + (u"\x1b[OD", curses.KEY_LEFT), # left (4) + (u"\x1b[OF", curses.KEY_END), # end (1) + (u"\x1b[OH", curses.KEY_HOME), # home (7) + + # The vt220 placed F1-F4 above the keypad, in place of actual + # F1-F4 were local functions (hold screen, print screen, + # set up, data/talk, break). + (u"\x1bOP", curses.KEY_F1), + (u"\x1bOQ", curses.KEY_F2), + (u"\x1bOR", curses.KEY_F3), + (u"\x1bOS", curses.KEY_F4), +) diff --git a/blessings/sequences.py b/blessings/sequences.py new file mode 100644 index 0000000..80649c3 --- /dev/null +++ b/blessings/sequences.py @@ -0,0 +1,688 @@ +# encoding: utf-8 +" This sub-module provides 'sequence awareness' for blessings." + +__author__ = 'Jeff Quast ' +__license__ = 'MIT' + +__all__ = ('init_sequence_patterns', 'Sequence', 'SequenceTextWrapper',) + +# built-ins +import functools +import textwrap +import warnings +import math +import sys +import re + +# local +from ._binterms import binary_terminals as _BINTERM_UNSUPPORTED + +# 3rd-party +import wcwidth # https://github.com/jquast/wcwidth + +_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.") + +if sys.version_info[0] == 3: + text_type = str +else: + text_type = unicode # noqa + + +def _merge_sequences(inp): + """Merge a list of input sequence patterns for use in a regular expression. + Order by lengthyness (full sequence set precedent over subset), + and exclude any empty (u'') sequences. + """ + return sorted(list(filter(None, inp)), key=len, reverse=True) + + +def _build_numeric_capability(term, cap, optional=False, + base_num=99, nparams=1): + """ Build regexp from capabilities having matching numeric + parameter contained within termcap value: n->(\d+). + """ + _cap = getattr(term, cap) + opt = '?' if optional else '' + if _cap: + args = (base_num,) * nparams + cap_re = re.escape(_cap(*args)) + for num in range(base_num - 1, base_num + 2): + # search for matching ascii, n-1 through n+1 + if str(num) in cap_re: + # modify & return n to matching digit expression + cap_re = cap_re.replace(str(num), r'(\d+)%s' % (opt,)) + return cap_re + warnings.warn('Unknown parameter in %r (%r, %r)' % (cap, _cap, cap_re)) + return None # no such capability + + +def _build_any_numeric_capability(term, cap, num=99, nparams=1): + """ Build regexp from capabilities having *any* digit parameters + (substitute matching \d with pattern \d and return). + """ + _cap = getattr(term, cap) + if _cap: + cap_re = re.escape(_cap(*((num,) * nparams))) + cap_re = re.sub('(\d+)', r'(\d+)', cap_re) + if r'(\d+)' in cap_re: + return cap_re + warnings.warn('Missing numerics in %r, %r' % (cap, cap_re)) + return None # no such capability + + +def get_movement_sequence_patterns(term): + """ Build and return set of regexp for capabilities of ``term`` known + to cause movement. + """ + bnc = functools.partial(_build_numeric_capability, term) + + return set([ + # carriage_return + re.escape(term.cr), + # column_address: Horizontal position, absolute + bnc(cap='hpa'), + # row_address: Vertical position #1 absolute + bnc(cap='vpa'), + # cursor_address: Move to row #1 columns #2 + bnc(cap='cup', nparams=2), + # cursor_down: Down one line + re.escape(term.cud1), + # cursor_home: Home cursor (if no cup) + re.escape(term.home), + # cursor_left: Move left one space + re.escape(term.cub1), + # cursor_right: Non-destructive space (move right one space) + re.escape(term.cuf1), + # cursor_up: Up one line + re.escape(term.cuu1), + # param_down_cursor: Down #1 lines + bnc(cap='cud', optional=True), + # restore_cursor: Restore cursor to position of last save_cursor + re.escape(term.rc), + # clear_screen: clear screen and home cursor + re.escape(term.clear), + # enter/exit_fullscreen: switch to alternate screen buffer + re.escape(term.enter_fullscreen), + re.escape(term.exit_fullscreen), + # forward cursor + term._cuf, + # backward cursor + term._cub, + ]) + + +def get_wontmove_sequence_patterns(term): + """ Build and return set of regexp for capabilities of ``term`` known + not to cause any movement. + """ + bnc = functools.partial(_build_numeric_capability, term) + bna = functools.partial(_build_any_numeric_capability, term) + + return list([ + # print_screen: Print contents of screen + re.escape(term.mc0), + # prtr_off: Turn off printer + re.escape(term.mc4), + # prtr_on: Turn on printer + re.escape(term.mc5), + # save_cursor: Save current cursor position (P) + re.escape(term.sc), + # set_tab: Set a tab in every row, current columns + re.escape(term.hts), + # enter_bold_mode: Turn on bold (extra bright) mode + re.escape(term.bold), + # enter_standout_mode + re.escape(term.standout), + # enter_subscript_mode + re.escape(term.subscript), + # enter_superscript_mode + re.escape(term.superscript), + # enter_underline_mode: Begin underline mode + re.escape(term.underline), + # enter_blink_mode: Turn on blinking + re.escape(term.blink), + # enter_dim_mode: Turn on half-bright mode + re.escape(term.dim), + # cursor_invisible: Make cursor invisible + re.escape(term.civis), + # cursor_visible: Make cursor very visible + re.escape(term.cvvis), + # cursor_normal: Make cursor appear normal (undo civis/cvvis) + re.escape(term.cnorm), + # clear_all_tabs: Clear all tab stops + re.escape(term.tbc), + # change_scroll_region: Change region to line #1 to line #2 + bnc(cap='csr', nparams=2), + # clr_bol: Clear to beginning of line + re.escape(term.el1), + # clr_eol: Clear to end of line + re.escape(term.el), + # clr_eos: Clear to end of screen + re.escape(term.clear_eos), + # delete_character: Delete character + re.escape(term.dch1), + # delete_line: Delete line (P*) + re.escape(term.dl1), + # erase_chars: Erase #1 characters + bnc(cap='ech'), + # insert_line: Insert line (P*) + re.escape(term.il1), + # parm_dch: Delete #1 characters + bnc(cap='dch'), + # parm_delete_line: Delete #1 lines + bnc(cap='dl'), + # exit_alt_charset_mode: End alternate character set (P) + re.escape(term.rmacs), + # exit_am_mode: Turn off automatic margins + re.escape(term.rmam), + # exit_attribute_mode: Turn off all attributes + re.escape(term.sgr0), + # exit_ca_mode: Strings to end programs using cup + re.escape(term.rmcup), + # exit_insert_mode: Exit insert mode + re.escape(term.rmir), + # exit_standout_mode: Exit standout mode + re.escape(term.rmso), + # exit_underline_mode: Exit underline mode + re.escape(term.rmul), + # flash_hook: Flash switch hook + re.escape(term.hook), + # flash_screen: Visible bell (may not move cursor) + re.escape(term.flash), + # keypad_local: Leave 'keyboard_transmit' mode + re.escape(term.rmkx), + # keypad_xmit: Enter 'keyboard_transmit' mode + re.escape(term.smkx), + # meta_off: Turn off meta mode + re.escape(term.rmm), + # meta_on: Turn on meta mode (8th-bit on) + re.escape(term.smm), + # orig_pair: Set default pair to its original value + re.escape(term.op), + # parm_ich: Insert #1 characters + bnc(cap='ich'), + # parm_index: Scroll forward #1 + bnc(cap='indn'), + # parm_insert_line: Insert #1 lines + bnc(cap='il'), + # erase_chars: Erase #1 characters + bnc(cap='ech'), + # parm_rindex: Scroll back #1 lines + bnc(cap='rin'), + # parm_up_cursor: Up #1 lines + bnc(cap='cuu'), + # scroll_forward: Scroll text up (P) + re.escape(term.ind), + # scroll_reverse: Scroll text down (P) + re.escape(term.rev), + # tab: Tab to next 8-space hardware tab stop + re.escape(term.ht), + # set_a_background: Set background color to #1, using ANSI escape + bna(cap='setab', num=1), + bna(cap='setab', num=(term.number_of_colors - 1)), + # set_a_foreground: Set foreground color to #1, using ANSI escape + bna(cap='setaf', num=1), + bna(cap='setaf', num=(term.number_of_colors - 1)), + ] + [ + # set_attributes: Define video attributes #1-#9 (PG9) + # ( not *exactly* legal, being extra forgiving. ) + bna(cap='sgr', nparams=_num) for _num in range(1, 10) + # reset_{1,2,3}string: Reset string + ] + list(map(re.escape, (term.r1, term.r2, term.r3,)))) + + +def init_sequence_patterns(term): + """Given a Terminal instance, ``term``, this function processes + and parses several known terminal capabilities, and builds and + returns a dictionary database of regular expressions, which may + be re-attached to the terminal by attributes of the same key-name: + + ``_re_will_move`` + any sequence matching this pattern will cause the terminal + cursor to move (such as *term.home*). + + ``_re_wont_move`` + any sequence matching this pattern will not cause the cursor + to move (such as *term.bold*). + + ``_re_cuf`` + regular expression that matches term.cuf(N) (move N characters forward), + or None if temrinal is without cuf sequence. + + ``_cuf1`` + *term.cuf1* sequence (cursor forward 1 character) as a static value. + + ``_re_cub`` + regular expression that matches term.cub(N) (move N characters backward), + or None if terminal is without cub sequence. + + ``_cub1`` + *term.cuf1* sequence (cursor backward 1 character) as a static value. + + These attributes make it possible to perform introspection on strings + containing sequences generated by this terminal, to determine the + printable length of a string. + """ + if term.kind in _BINTERM_UNSUPPORTED: + warnings.warn(_BINTERM_UNSUPPORTED_MSG.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)) + + # 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 += [ + # some last-ditch match efforts; well, xterm and aixterm is going + # to throw \x1b(B and other oddities all around, so, when given + # input such as ansi art (see test using wall.ans), and well, + # theres no reason a vt220 terminal shouldn't be able to recognize + # blue_on_red, even if it didn't cause it to be generated. these + # are final "ok, i will match this, anyway" + re.escape(u'\x1b') + r'\[(\d+)m', + re.escape(u'\x1b') + r'\[(\d+)\;(\d+)m', + re.escape(u'\x1b') + r'\[(\d+)\;(\d+)\;(\d+)m', + re.escape(u'\x1b') + r'\[(\d+)\;(\d+)\;(\d+)\;(\d+)m', + re.escape(u'\x1b(B'), + ] + + # compile as regular expressions, OR'd. + _re_will_move = re.compile('(%s)' % ('|'.join(_will_move))) + _re_wont_move = re.compile('(%s)' % ('|'.join(_wont_move))) + + # static pattern matching for horizontal_distance(ucs, term) + bnc = functools.partial(_build_numeric_capability, term) + + # parm_right_cursor: Move #1 characters to the right + _cuf = bnc(cap='cuf', optional=True) + _re_cuf = re.compile(_cuf) if _cuf else None + + # cursor_right: Non-destructive space (move right one space) + _cuf1 = term.cuf1 + + # parm_left_cursor: Move #1 characters to the left + _cub = bnc(cap='cub', optional=True) + _re_cub = re.compile(_cub) if _cub else None + + # cursor_left: Move left one space + _cub1 = term.cub1 + + return {'_re_will_move': _re_will_move, + '_re_wont_move': _re_wont_move, + '_re_cuf': _re_cuf, + '_re_cub': _re_cub, + '_cuf1': _cuf1, + '_cub1': _cub1, } + + +class SequenceTextWrapper(textwrap.TextWrapper): + def __init__(self, width, term, **kwargs): + 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. + """ + lines = [] + if self.width <= 0 or not isinstance(self.width, int): + raise ValueError("invalid width %r(%s) (must be integer > 0)" % ( + self.width, type(self.width))) + term = self.term + drop_whitespace = not hasattr(self, 'drop_whitespace' + ) or self.drop_whitespace + chunks.reverse() + while chunks: + cur_line = [] + cur_len = 0 + if lines: + indent = self.subsequent_indent + else: + indent = self.initial_indent + width = self.width - len(indent) + if drop_whitespace and ( + Sequence(chunks[-1], term).strip() == '' and lines): + del chunks[-1] + while chunks: + chunk_len = Sequence(chunks[-1], term).length() + if cur_len + chunk_len <= width: + cur_line.append(chunks.pop()) + cur_len += chunk_len + else: + break + if chunks and Sequence(chunks[-1], term).length() > width: + self._handle_long_word(chunks, cur_line, cur_len, width) + if drop_whitespace and ( + cur_line and Sequence(cur_line[-1], term).strip() == ''): + del cur_line[-1] + if cur_line: + lines.append(indent + u''.join(cur_line)) + return lines + + def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width): + """_handle_long_word(chunks : [string], + cur_line : [string], + cur_len : int, width : int) + + Handle a chunk of text (most likely a word, not whitespace) that + is too long to fit in any line. + """ + # Figure out when indent is larger than the specified width, and make + # sure at least one character is stripped off on every pass + if width < 1: + space_left = 1 + else: + space_left = width - cur_len + + # If we're allowed to break long words, then do so: put as much + # of the next chunk onto the current line as will fit. + if self.break_long_words: + term = self.term + chunk = reversed_chunks[-1] + nxt = 0 + for idx in range(0, len(chunk)): + if idx == nxt: + # at sequence, point beyond it, + nxt = idx + measure_length(chunk[idx:], term) + if nxt <= idx: + # point beyond next sequence, if any, + # otherwise point to next character + nxt = idx + measure_length(chunk[idx:], term) + 1 + if Sequence(chunk[:nxt], term).length() > space_left: + break + else: + # our text ends with a sequence, such as in text + # u'!\x1b(B\x1b[m', set index at at end (nxt) + idx = nxt + + cur_line.append(chunk[:idx]) + reversed_chunks[-1] = chunk[idx:] + + # Otherwise, we have to preserve the long word intact. Only add + # it to the current line if there's nothing already there -- + # that minimizes how much we violate the width constraint. + elif not cur_line: + cur_line.append(reversed_chunks.pop()) + + # If we're not allowed to break long words, and there's already + # text on the current line, do nothing. Next time through the + # main loop of _wrap_chunks(), we'll wind up here again, but + # cur_len will be zero, so the next line will be entirely + # devoted to the long word that we can't handle right now. + + +SequenceTextWrapper.__doc__ = textwrap.TextWrapper.__doc__ + + +class Sequence(text_type): + """ + This unicode-derived class understands the effect of escape sequences + of printable length, allowing a properly implemented .rjust(), .ljust(), + .center(), and .len() + """ + + def __new__(cls, sequence_text, term): + """Sequence(sequence_text, term) -> unicode object + + :arg sequence_text: A string containing sequences. + :arg term: Terminal instance this string was created with. + """ + new = text_type.__new__(cls, sequence_text) + new._term = term + return new + + def ljust(self, width, fillchar=u' '): + """S.ljust(width, fillchar) -> unicode + + 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))) + return u''.join((self, rightside)) + + def rjust(self, width, fillchar=u' '): + """S.rjust(width, fillchar=u'') -> unicode + + 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))) + return u''.join((leftside, self)) + + def center(self, width, fillchar=u' '): + """S.center(width, fillchar=u'') -> unicode + + Returns string derived from unicode string ``S``, centered + and surrounded with whitespace padding ``fillchar``.""" + split = max(0.0, float(width) - self.length()) / 2 + leftside = fillchar * int((max(0.0, math.floor(split))) + / 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. + + Strings containing ``term.left`` or ``\b`` will cause "overstrike", + but a length less than 0 is not ever returned. So ``_\b+`` is a + length of 1 (``+``), but ``\b`` is simply a length of 0. + + Some characters may consume more than one cell, mainly those CJK + Unified Ideographs (Chinese, Japanese, Korean) defined by Unicode + as half or full-width characters. + + For example: + >>> from 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 + + Return a copy of the string S with terminal sequences removed, and + leading and trailing whitespace removed. + + If chars is given and not None, remove characters in chars instead. + """ + return self.strip_seqs().strip(chars) + + def lstrip(self, chars=None): + """S.lstrip([chars]) -> unicode + + Return a copy of the string S with terminal sequences and leading + whitespace removed. + + If chars is given and not None, remove characters in chars instead. + """ + return self.strip_seqs().lstrip(chars) + + def rstrip(self, chars=None): + """S.rstrip([chars]) -> unicode + + Return a copy of the string S with terminal sequences and trailing + whitespace removed. + + If chars is given and not None, remove characters in chars instead. + """ + return self.strip_seqs().rstrip(chars) + + def strip_seqs(self): + """S.strip_seqs() -> unicode + + Return a string without sequences for a string that contains + sequences for the Terminal with which they were created. + + Where sequence ``move_right(n)`` is detected, it is replaced with + ``n * u' '``, and where ``move_left()`` or ``\\b`` is detected, + those last-most characters are destroyed. + + 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' + """ + # nxt: points to first character beyond current escape sequence. + # width: currently estimated display length. + input = self.padd() + outp = u'' + nxt = 0 + for idx in range(0, len(input)): + if idx == nxt: + # at sequence, point beyond it, + nxt = idx + measure_length(input[idx:], self._term) + if nxt <= idx: + # append non-sequence to outp, + outp += input[idx] + # point beyond next sequence, if any, + # otherwise point to next character + nxt = idx + measure_length(input[idx:], self._term) + 1 + return outp + + def padd(self): + """S.padd() -> unicode + Make non-destructive space or backspace into destructive ones. + + Where sequence ``move_right(n)`` is detected, it is replaced with + ``n * u' '``. Where sequence ``move_left(n)`` or ``\\b`` is + detected, those last-most characters are destroyed. + """ + outp = u'' + nxt = 0 + for idx in range(0, text_type.__len__(self)): + width = horizontal_distance(self[idx:], self._term) + if width != 0: + nxt = idx + measure_length(self[idx:], self._term) + if width > 0: + outp += u' ' * width + elif width < 0: + outp = outp[:width] + if nxt <= idx: + outp += self[idx] + nxt = idx + 1 + return outp + + +def measure_length(ucs, term): + """measure_length(S, term) -> int + + Returns non-zero for string ``S`` that begins with a terminal sequence, + that is: the width of the first unprintable sequence found in S. For use + as a *next* pointer to skip past sequences. If string ``S`` is not a + sequence, 0 is returned. + + A sequence may be a typical terminal sequence beginning with Escape + (``\x1b``), especially a Control Sequence Initiator (``CSI``, ``\x1b[``, + ...), or those of ``\a``, ``\b``, ``\r``, ``\n``, ``\xe0`` (shift in), + ``\x0f`` (shift out). They do not necessarily have to begin with CSI, they + need only match the capabilities of attributes ``_re_will_move`` and + ``_re_wont_move`` of terminal ``term``. + """ + + # simple terminal control characters, + ctrl_seqs = u'\a\b\r\n\x0e\x0f' + + if any([ucs.startswith(_ch) for _ch in ctrl_seqs]): + return 1 + + # known multibyte sequences, + matching_seq = term and ( + term._re_will_move.match(ucs) or + term._re_wont_move.match(ucs) or + term._re_cub and term._re_cub.match(ucs) or + term._re_cuf and term._re_cuf.match(ucs) + ) + + if matching_seq: + start, end = matching_seq.span() + return end + + # none found, must be printable! + return 0 + + +def termcap_distance(ucs, cap, unit, term): + """termcap_distance(S, cap, unit, term) -> int + + Match horizontal distance by simple ``cap`` capability name, ``cub1`` or + ``cuf1``, with string matching the sequences identified by Terminal + instance ``term`` and a distance of ``unit`` *1* or *-1*, for right and + left, respectively. + + Otherwise, by regular expression (using dynamic regular expressions built + using ``cub(n)`` and ``cuf(n)``. Failing that, any of the standard SGR + sequences (``\033[C``, ``\033[D``, ``\033[nC``, ``\033[nD``). + + Returns 0 if unmatched. + """ + assert cap in ('cuf', 'cub') + # match cub1(left), cuf1(right) + one = getattr(term, '_%s1' % (cap,)) + if one and ucs.startswith(one): + return unit + + # match cub(n), cuf(n) using regular expressions + re_pattern = getattr(term, '_re_%s' % (cap,)) + _dist = re_pattern and re_pattern.match(ucs) + if _dist: + return unit * int(_dist.group(1)) + + return 0 + + +def horizontal_distance(ucs, term): + """horizontal_distance(S, term) -> int + + Returns Integer ```` in SGR sequence of form ``[C`` + (T.move_right(n)), or ``-(n)`` in sequence of form ``[D`` + (T.move_left(n)). Returns -1 for backspace (0x08), Otherwise 0. + + Tabstop (``\t``) cannot be correctly calculated, as the relative column + position cannot be determined: 8 is always (and, incorrectly) returned. + """ + + if ucs.startswith('\b'): + return -1 + + elif ucs.startswith('\t'): + # As best as I can prove it, a tabstop is always 8 by default. + # Though, given that blessings is: + # + # 1. unaware of the output device's current cursor position, and + # 2. unaware of the location the callee may chose to output any + # given string, + # + # It is not possible to determine how many cells any particular + # \t would consume on the output device! + return 8 + + return (termcap_distance(ucs, 'cub', -1, term) or + termcap_distance(ucs, 'cuf', 1, term) or + 0) diff --git a/blessings/terminal.py b/blessings/terminal.py new file mode 100644 index 0000000..60fecbe --- /dev/null +++ b/blessings/terminal.py @@ -0,0 +1,783 @@ +"This primary module provides the Terminal class." +# standard modules +import collections +import contextlib +import functools +import warnings +import platform +import codecs +import curses +import locale +import select +import struct +import time +import sys +import os + +try: + import termios + import fcntl + import tty +except ImportError: + tty_methods = ('setraw', 'cbreak', 'kbhit', 'height', 'width') + msg_nosupport = ( + "One or more of the modules: 'termios', 'fcntl', and 'tty' " + "are not found on your platform '{0}'. The following methods " + "of Terminal are dummy/no-op unless a deriving class overrides " + "them: {1}".format(sys.platform.lower(), ', '.join(tty_methods))) + warnings.warn(msg_nosupport) + HAS_TTY = False +else: + HAS_TTY = True + +try: + from io import UnsupportedOperation as IOUnsupportedOperation +except ImportError: + class IOUnsupportedOperation(Exception): + """A dummy exception to take the place of Python 3's + ``io.UnsupportedOperation`` in Python 2.5""" + +try: + _ = InterruptedError + del _ +except NameError: + # alias py2 exception to py3 + InterruptedError = select.error + +# local imports +from .formatters import ( + ParameterizingString, + NullCallableString, + resolve_capability, + resolve_attribute, +) + +from .sequences import ( + init_sequence_patterns, + SequenceTextWrapper, + Sequence, +) + +from .keyboard import ( + get_keyboard_sequences, + get_keyboard_codes, + resolve_sequence, +) + + +class Terminal(object): + """A wrapper for curses and related terminfo(5) terminal capabilities. + + Instance attributes: + + ``stream`` + The stream the terminal outputs to. It's convenient to pass the stream + around with the terminal; it's almost always needed when the terminal + is and saves sticking lots of extra args on client functions in + practice. + """ + + #: Sugary names for commonly-used capabilities + _sugar = dict( + save='sc', + restore='rc', + # 'clear' clears the whole screen. + clear_eol='el', + clear_bol='el1', + clear_eos='ed', + position='cup', # deprecated + enter_fullscreen='smcup', + exit_fullscreen='rmcup', + move='cup', + move_x='hpa', + move_y='vpa', + move_left='cub1', + move_right='cuf1', + move_up='cuu1', + move_down='cud1', + hide_cursor='civis', + normal_cursor='cnorm', + reset_colors='op', # oc doesn't work on my OS X terminal. + normal='sgr0', + reverse='rev', + italic='sitm', + no_italic='ritm', + shadow='sshm', + no_shadow='rshm', + standout='smso', + no_standout='rmso', + subscript='ssubm', + no_subscript='rsubm', + superscript='ssupm', + no_superscript='rsupm', + underline='smul', + no_underline='rmul') + + def __init__(self, kind=None, stream=None, force_styling=False): + """Initialize the terminal. + + If ``stream`` is not a tty, I will default to returning an empty + Unicode string for all capability values, so things like piping your + output to a file won't strew escape sequences all over the place. The + ``ls`` command sets a precedent for this: it defaults to columnar + output when being sent to a tty and one-item-per-line when not. + + :arg kind: A terminal string as taken by ``setupterm()``. Defaults to + the value of the ``TERM`` environment variable. + :arg stream: A file-like object representing the terminal. Defaults to + the original value of stdout, like ``curses.initscr()`` does. + :arg force_styling: Whether to force the emission of capabilities, even + if we don't seem to be in a terminal. This comes in handy if users + are trying to pipe your output through something like ``less -r``, + which supports terminal codes just fine but doesn't appear itself + to be a terminal. Just expose a command-line option, and set + ``force_styling`` based on it. Terminal initialization sequences + will be sent to ``stream`` if it has a file descriptor and to + ``sys.__stdout__`` otherwise. (``setupterm()`` demands to send them + somewhere, and stdout is probably where the output is ultimately + headed. If not, stderr is probably bound to the same terminal.) + + If you want to force styling to not happen, pass + ``force_styling=None``. + + """ + global _CUR_TERM + self.keyboard_fd = None + + # default stream is stdout, keyboard only valid as stdin when + # output stream is stdout and output stream is a tty + if stream is None or stream == sys.__stdout__: + stream = sys.__stdout__ + self.keyboard_fd = sys.__stdin__.fileno() + + try: + stream_fd = (stream.fileno() if hasattr(stream, 'fileno') + and callable(stream.fileno) else None) + except IOUnsupportedOperation: + stream_fd = None + + self._is_a_tty = stream_fd is not None and os.isatty(stream_fd) + self._does_styling = ((self.is_a_tty or force_styling) and + force_styling is not None) + + # keyboard_fd only non-None if both stdin and stdout is a tty. + self.keyboard_fd = (self.keyboard_fd + if self.keyboard_fd is not None and + self.is_a_tty and os.isatty(self.keyboard_fd) + else None) + self._normal = None # cache normal attr, preventing recursive lookups + + # The descriptor to direct terminal initialization sequences to. + # sys.__stdout__ seems to always have a descriptor of 1, even if output + # is redirected. + self._init_descriptor = (stream_fd is None and sys.__stdout__.fileno() + or stream_fd) + self._kind = kind or os.environ.get('TERM', 'unknown') + + if self.does_styling: + # Make things like tigetstr() work. Explicit args make setupterm() + # work even when -s is passed to nosetests. Lean toward sending + # init sequences to the stream if it has a file descriptor, and + # send them to stdout as a fallback, since they have to go + # somewhere. + try: + if (platform.python_implementation() == 'PyPy' and + isinstance(self._kind, unicode)): + # pypy/2.4.0_2/libexec/lib_pypy/_curses.py, line 1131 + # TypeError: initializer for ctype 'char *' must be a str + curses.setupterm(self._kind.encode('ascii'), self._init_descriptor) + else: + curses.setupterm(self._kind, self._init_descriptor) + except curses.error as err: + warnings.warn('Failed to setupterm(kind={0!r}): {1}' + .format(self._kind, err)) + self._kind = None + self._does_styling = False + else: + if _CUR_TERM is None or self._kind == _CUR_TERM: + _CUR_TERM = self._kind + else: + warnings.warn( + 'A terminal of kind "%s" has been requested; due to an' + ' internal python curses bug, terminal capabilities' + ' for a terminal of kind "%s" will continue to be' + ' returned for the remainder of this process.' % ( + self._kind, _CUR_TERM,)) + + for re_name, re_val in init_sequence_patterns(self).items(): + setattr(self, re_name, re_val) + + # build database of int code <=> KEY_NAME + self._keycodes = get_keyboard_codes() + + # store attributes as: self.KEY_NAME = code + for key_code, key_name in self._keycodes.items(): + setattr(self, key_name, key_code) + + # build database of sequence <=> KEY_NAME + self._keymap = get_keyboard_sequences(self) + + self._keyboard_buf = collections.deque() + if self.keyboard_fd is not None: + locale.setlocale(locale.LC_ALL, '') + self._encoding = locale.getpreferredencoding() or 'ascii' + try: + self._keyboard_decoder = codecs.getincrementaldecoder( + self._encoding)() + except LookupError as err: + warnings.warn('%s, fallback to ASCII for keyboard.' % (err,)) + self._encoding = 'ascii' + self._keyboard_decoder = codecs.getincrementaldecoder( + self._encoding)() + + self.stream = stream + + def __getattr__(self, attr): + """Return a terminal capability as Unicode string. + + For example, ``term.bold`` is a unicode string that may be prepended + to text to set the video attribute for bold, which should also be + terminated with the pairing ``term.normal``. + + This capability is also callable, so you can use ``term.bold("hi")`` + which results in the joining of (term.bold, "hi", term.normal). + + Compound formatters may also be used, for example: + ``term.bold_blink_red_on_green("merry x-mas!")``. + + For a parametrized capability such as ``cup`` (cursor_address), pass + the parameters as arguments ``some_term.cup(line, column)``. See + manual page terminfo(5) for a complete list of capabilities. + """ + if not self.does_styling: + return NullCallableString() + val = resolve_attribute(self, attr) + # Cache capability codes. + setattr(self, attr, val) + return val + + @property + def kind(self): + """Name of this terminal type as string.""" + return self._kind + + @property + def does_styling(self): + """Whether this instance will emit terminal sequences (bool).""" + return self._does_styling + + @property + def is_a_tty(self): + """Whether the ``stream`` associated with this instance is a terminal + (bool).""" + return self._is_a_tty + + @property + def height(self): + """T.height -> int + + The height of the terminal in characters. + """ + return self._height_and_width().ws_row + + @property + def width(self): + """T.width -> int + + The width of the terminal in characters. + """ + return self._height_and_width().ws_col + + @staticmethod + def _winsize(fd): + """T._winsize -> WINSZ(ws_row, ws_col, ws_xpixel, ws_ypixel) + + The tty connected by file desriptor fd is queried for its window size, + and returned as a collections.namedtuple instance WINSZ. + + May raise exception IOError. + """ + if HAS_TTY: + data = fcntl.ioctl(fd, termios.TIOCGWINSZ, WINSZ._BUF) + return WINSZ(*struct.unpack(WINSZ._FMT, data)) + return WINSZ(ws_row=24, ws_col=80, ws_xpixel=0, ws_ypixel=0) + + def _height_and_width(self): + """Return a tuple of (terminal height, terminal width). + """ + # TODO(jquast): hey kids, even if stdout is redirected to a file, + # we can still query sys.__stdin__.fileno() for our terminal size. + # -- of course, if both are redirected, we have no use for this fd. + for fd in (self._init_descriptor, sys.__stdout__): + try: + if fd is not None: + return self._winsize(fd) + except IOError: + pass + + return WINSZ(ws_row=int(os.getenv('LINES', '25')), + ws_col=int(os.getenv('COLUMNS', '80')), + ws_xpixel=None, + ws_ypixel=None) + + @contextlib.contextmanager + def location(self, x=None, y=None): + """Return a context manager for temporarily moving the cursor. + + Move the cursor to a certain position on entry, let you print stuff + there, then return the cursor to its original position:: + + term = Terminal() + with term.location(2, 5): + print 'Hello, world!' + for x in xrange(10): + print 'I can do it %i times!' % x + + Specify ``x`` to move to a certain column, ``y`` to move to a certain + row, both, or neither. If you specify neither, only the saving and + restoration of cursor position will happen. This can be useful if you + simply want to restore your place after doing some manual cursor + movement. + + """ + # Save position and move to the requested column, row, or both: + self.stream.write(self.save) + if x is not None and y is not None: + self.stream.write(self.move(y, x)) + elif x is not None: + self.stream.write(self.move_x(x)) + elif y is not None: + self.stream.write(self.move_y(y)) + try: + yield + finally: + # Restore original cursor position: + self.stream.write(self.restore) + + @contextlib.contextmanager + def fullscreen(self): + """Return a context manager that enters fullscreen mode while inside it + and restores normal mode on leaving. + + Fullscreen mode is characterized by instructing the terminal emulator + to store and save the current screen state (all screen output), switch + to "alternate screen". Upon exiting, the previous screen state is + returned. + + This call may not be tested; only one screen state may be saved at a + time. + """ + self.stream.write(self.enter_fullscreen) + try: + yield + finally: + self.stream.write(self.exit_fullscreen) + + @contextlib.contextmanager + def hidden_cursor(self): + """Return a context manager that hides the cursor upon entering, + and makes it visible again upon exiting.""" + self.stream.write(self.hide_cursor) + try: + yield + finally: + self.stream.write(self.normal_cursor) + + @property + def color(self): + """Returns capability that sets the foreground color. + + The capability is unparameterized until called and passed a number + (0-15), at which point it returns another string which represents a + specific color change. This second string can further be called to + color a piece of text and set everything back to normal afterward. + + :arg num: The number, 0-15, of the color + + """ + if not self.does_styling: + return NullCallableString() + return ParameterizingString(self._foreground_color, + self.normal, 'color') + + @property + def on_color(self): + "Returns capability that sets the background color." + if not self.does_styling: + return NullCallableString() + return ParameterizingString(self._background_color, + self.normal, 'on_color') + + @property + def normal(self): + "Returns sequence that resets video attribute." + if self._normal: + return self._normal + self._normal = resolve_capability(self, 'normal') + return self._normal + + @property + def number_of_colors(self): + """Return the number of colors the terminal supports. + + Common values are 0, 8, 16, 88, and 256. Most commonly + this may be used to test color capabilities at all:: + + if term.number_of_colors: + ...""" + # trim value to 0, as tigetnum('colors') returns -1 if no support, + # -2 if no such capability. + return max(0, self.does_styling and curses.tigetnum('colors') or -1) + + @property + def _foreground_color(self): + return self.setaf or self.setf + + @property + def _background_color(self): + return self.setab or self.setb + + def ljust(self, text, width=None, fillchar=u' '): + """T.ljust(text, [width], [fillchar]) -> unicode + + Return string ``text``, left-justified by printable length ``width``. + Padding is done using the specified fill character (default is a + space). Default ``width`` is the attached terminal's width. ``text`` + may contain terminal sequences.""" + if width is None: + width = self.width + return Sequence(text, self).ljust(width, fillchar) + + def rjust(self, text, width=None, fillchar=u' '): + """T.rjust(text, [width], [fillchar]) -> unicode + + Return string ``text``, right-justified by printable length ``width``. + Padding is done using the specified fill character (default is a + space). Default ``width`` is the attached terminal's width. ``text`` + may contain terminal sequences.""" + if width is None: + width = self.width + return Sequence(text, self).rjust(width, fillchar) + + def center(self, text, width=None, fillchar=u' '): + """T.center(text, [width], [fillchar]) -> unicode + + Return string ``text``, centered by printable length ``width``. + Padding is done using the specified fill character (default is a + space). Default ``width`` is the attached terminal's width. ``text`` + may contain terminal sequences.""" + if width is None: + width = self.width + return Sequence(text, self).center(width, fillchar) + + def length(self, text): + """T.length(text) -> int + + Return the printable length of string ``text``, which may contain + terminal sequences. Strings containing sequences such as 'clear', + which repositions the cursor, does not give accurate results, and + their printable length is evaluated *0*.. + """ + return Sequence(text, self).length() + + def strip(self, text, chars=None): + """T.strip(text) -> unicode + + Return string ``text`` with terminal sequences removed, and leading + and trailing whitespace removed. + """ + return Sequence(text, self).strip(chars) + + def rstrip(self, text, chars=None): + """T.rstrip(text) -> unicode + + Return string ``text`` with terminal sequences and trailing whitespace + removed. + """ + return Sequence(text, self).rstrip(chars) + + def lstrip(self, text, chars=None): + """T.lstrip(text) -> unicode + + Return string ``text`` with terminal sequences and leading whitespace + removed. + """ + return Sequence(text, self).lstrip(chars) + + def strip_seqs(self, text): + """T.strip_seqs(text) -> unicode + + Return string ``text`` stripped only of its sequences. + """ + return Sequence(text, self).strip_seqs() + + def wrap(self, text, width=None, **kwargs): + """T.wrap(text, [width=None, **kwargs ..]) -> list[unicode] + + Wrap paragraphs containing escape sequences ``text`` to the full + ``width`` of Terminal instance *T*, unless ``width`` is specified. + Wrapped by the virtual printable length, irregardless of the video + attribute sequences it may contain, allowing text containing colors, + bold, underline, etc. to be wrapped. + + Returns a list of strings that may contain escape sequences. See + ``textwrap.TextWrapper`` for all available additional kwargs to + customize wrapping behavior such as ``subsequent_indent``. + """ + width = self.width if width is None else width + lines = [] + for line in text.splitlines(): + lines.extend( + (_linewrap for _linewrap in SequenceTextWrapper( + width=width, term=self, **kwargs).wrap(text)) + if line.strip() else (u'',)) + + return lines + + def _next_char(self): + """T._next_char() -> unicode + + Read and decode next byte from keyboard stream. May return u'' + if decoding is not yet complete, or completed unicode character. + Should always return bytes when self._char_is_ready() returns True. + + Implementors of input streams other than os.read() on the stdin fd + should derive and override this method. + """ + assert self.keyboard_fd is not None + byte = os.read(self.keyboard_fd, 1) + return self._keyboard_decoder.decode(byte, final=False) + + def _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. + """ + # Special care is taken to handle a custom SIGWINCH handler, which + # causes select() to be interrupted with errno 4 (EAGAIN) -- + # it is ignored, and a new timeout value is derived from the previous, + # unless timeout becomes negative, because signal handler has blocked + # beyond timeout, then False is returned. Otherwise, when timeout is 0, + # we continue to block indefinitely (default). + stime = time.time() + check_w, check_x, ready_r = [], [], [None, ] + check_r = [self.keyboard_fd] if self.keyboard_fd is not None else [] + + while HAS_TTY and True: + try: + ready_r, ready_w, ready_x = select.select( + check_r, check_w, check_x, timeout) + except InterruptedError: + if not interruptable: + return u'' + if timeout is not None: + # subtract time already elapsed, + timeout -= time.time() - stime + if timeout > 0: + continue + # no time remains after handling exception (rare) + ready_r = [] + break + else: + break + + return False if self.keyboard_fd is None else check_r == ready_r + + @contextlib.contextmanager + def 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 + """ + if HAS_TTY and self.keyboard_fd is not None: + # Save current terminal mode: + save_mode = termios.tcgetattr(self.keyboard_fd) + mode_setter = tty.setraw if raw else tty.setcbreak + mode_setter(self.keyboard_fd, termios.TCSANOW) + try: + yield + finally: + # Restore prior mode: + termios.tcsetattr(self.keyboard_fd, + termios.TCSAFLUSH, + save_mode) + else: + yield + + @contextlib.contextmanager + def keypad(self): + """ + Context manager that enables keypad input (*keyboard_transmit* mode). + + This enables the effect of calling the curses function keypad(3x): + display terminfo(5) capability `keypad_xmit` (smkx) upon entering, + and terminfo(5) capability `keypad_local` (rmkx) upon exiting. + + On an IBM-PC keypad of ttype *xterm*, with numlock off, the + lower-left diagonal key transmits sequence ``\\x1b[F``, ``KEY_END``. + + However, upon entering keypad mode, ``\\x1b[OF`` is transmitted, + translating to ``KEY_LL`` (lower-left key), allowing diagonal + direction keys to be determined. + """ + try: + self.stream.write(self.smkx) + yield + finally: + self.stream.write(self.rmkx) + + def 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') ? + + if timeout is None and self.keyboard_fd is None: + raise NoKeyboard( + 'Waiting for a keystroke on a terminal with no keyboard ' + 'attached and no timeout would take a long time. Add a ' + 'timeout and revise your program logic.') + + def time_left(stime, timeout): + """time_left(stime, timeout) -> float + + Returns time-relative time remaining before ``timeout`` + after time elapsed since ``stime``. + """ + if timeout is not None: + if timeout == 0: + return 0 + return max(0, timeout - (time.time() - stime)) + + resolve = functools.partial(resolve_sequence, + mapper=self._keymap, + codes=self._keycodes) + + stime = time.time() + + # re-buffer previously received keystrokes, + ucs = u'' + while self._keyboard_buf: + ucs += self._keyboard_buf.pop() + + # receive all immediately available bytes + while self._char_is_ready(0): + ucs += self._next_char() + + # decode keystroke, if any + ks = resolve(text=ucs) + + # so long as the most immediately received or buffered keystroke is + # incomplete, (which may be a multibyte encoding), block until until + # one is received. + while not ks and self._char_is_ready(time_left(stime, timeout), + interruptable): + ucs += self._next_char() + ks = resolve(text=ucs) + + # handle escape key (KEY_ESCAPE) vs. escape sequence (which begins + # with KEY_ESCAPE, \x1b[, \x1bO, or \x1b?), up to esc_delay when + # received. This is not optimal, but causes least delay when + # (currently unhandled, and rare) "meta sends escape" is used, + # or when an unsupported sequence is sent. + if ks.code == self.KEY_ESCAPE: + esctime = time.time() + while (ks.code == self.KEY_ESCAPE and + self._char_is_ready(time_left(esctime, esc_delay))): + ucs += self._next_char() + ks = resolve(text=ucs) + + # buffer any remaining text received + self._keyboard_buf.extendleft(ucs[len(ks):]) + return ks + + +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 + +WINSZ = collections.namedtuple('WINSZ', ( + 'ws_row', # /* rows, in characters */ + 'ws_col', # /* columns, in characters */ + 'ws_xpixel', # /* horizontal size, pixels */ + 'ws_ypixel', # /* vertical size, pixels */ +)) +#: format of termios structure +WINSZ._FMT = 'hhhh' +#: buffer of termios structure appropriate for ioctl argument +WINSZ._BUF = '\x00' * struct.calcsize(WINSZ._FMT) diff --git a/blessings/tests/__init__.py b/blessings/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/blessings/tests/accessories.py b/blessings/tests/accessories.py new file mode 100644 index 0000000..8cb3f5b --- /dev/null +++ b/blessings/tests/accessories.py @@ -0,0 +1,240 @@ +# -*- coding: utf-8 -*- +"""Accessories for automated py.test runner.""" +# std +from __future__ import with_statement +import contextlib +import subprocess +import functools +import traceback +import termios +import random +import codecs +import curses +import sys +import pty +import os + +# local +from blessings import Terminal + +# 3rd +import pytest + +if sys.version_info[0] == 3: + text_type = str +else: + text_type = unicode # noqa + +TestTerminal = functools.partial(Terminal, kind='xterm-256color') +SEND_SEMAPHORE = SEMAPHORE = b'SEMAPHORE\n' +RECV_SEMAPHORE = b'SEMAPHORE\r\n' +all_xterms_params = ['xterm', 'xterm-256color'] +many_lines_params = [30, 100] +many_columns_params = [1, 10] +from blessings._binterms import binary_terminals +default_all_terms = ['screen', 'vt220', 'rxvt', 'cons25', 'linux', 'ansi'] +if os.environ.get('TEST_ALLTERMS'): + try: + available_terms = [ + _term.split(None, 1)[0] for _term in + subprocess.Popen(('toe', '-a'), + stdout=subprocess.PIPE, + close_fds=True) + .communicate()[0].splitlines()] + except OSError: + all_terms_params = default_all_terms +else: + available_terms = default_all_terms +all_terms_params = list(set(available_terms) - ( + set(binary_terminals) if not os.environ.get('TEST_BINTERMS') + else set())) or default_all_terms + + +class as_subprocess(object): + """This helper executes test cases in a child process, + avoiding a python-internal bug of _curses: setupterm() + may not be called more than once per process. + """ + _CHILD_PID = 0 + encoding = 'utf8' + + def __init__(self, func): + self.func = func + + def __call__(self, *args, **kwargs): + pid, master_fd = pty.fork() + if pid is self._CHILD_PID: + # child process executes function, raises exception + # if failed, causing a non-zero exit code, using the + # protected _exit() function of ``os``; to prevent the + # 'SystemExit' exception from being thrown. + try: + try: + cov = __import__('cov_core_init').init() + except ImportError: + cov = None + self.func(*args, **kwargs) + except Exception: + e_type, e_value, e_tb = sys.exc_info() + o_err = list() + for line in traceback.format_tb(e_tb): + o_err.append(line.rstrip().encode('utf-8')) + o_err.append(('-=' * 20).encode('ascii')) + o_err.extend([_exc.rstrip().encode('utf-8') for _exc in + traceback.format_exception_only( + e_type, e_value)]) + os.write(sys.__stdout__.fileno(), b'\n'.join(o_err)) + os.close(sys.__stdout__.fileno()) + os.close(sys.__stderr__.fileno()) + os.close(sys.__stdin__.fileno()) + if cov is not None: + cov.stop() + cov.save() + os._exit(1) + else: + if cov is not None: + cov.stop() + cov.save() + os._exit(0) + + exc_output = text_type() + decoder = codecs.getincrementaldecoder(self.encoding)() + while True: + try: + _exc = os.read(master_fd, 65534) + except OSError: + # linux EOF + break + if not _exc: + # bsd EOF + break + exc_output += decoder.decode(_exc) + + # parent process asserts exit code is 0, causing test + # to fail if child process raised an exception/assertion + pid, status = os.waitpid(pid, 0) + os.close(master_fd) + + # Display any output written by child process + # (esp. any AssertionError exceptions written to stderr). + exc_output_msg = 'Output in child process:\n%s\n%s\n%s' % ( + u'=' * 40, exc_output, u'=' * 40,) + assert exc_output == '', exc_output_msg + + # Also test exit status is non-zero + assert os.WEXITSTATUS(status) == 0 + + +def read_until_semaphore(fd, semaphore=RECV_SEMAPHORE, + encoding='utf8', timeout=10): + """Read file descriptor ``fd`` until ``semaphore`` is found. + + Used to ensure the child process is awake and ready. For timing + tests; without a semaphore, the time to fork() would be (incorrectly) + included in the duration of the test, which can be very length on + continuous integration servers (such as Travis-CI). + """ + # note that when a child process writes xyz\\n, the parent + # process will read xyz\\r\\n -- this is how pseudo terminals + # behave; a virtual terminal requires both carriage return and + # line feed, it is only for convenience that \\n does both. + outp = text_type() + decoder = codecs.getincrementaldecoder(encoding)() + semaphore = semaphore.decode('ascii') + while not outp.startswith(semaphore): + try: + _exc = os.read(fd, 1) + except OSError: # Linux EOF + break + if not _exc: # BSD EOF + break + outp += decoder.decode(_exc, final=False) + assert outp.startswith(semaphore), ( + 'Semaphore not recv before EOF ' + '(expected: %r, got: %r)' % (semaphore, outp,)) + return outp[len(semaphore):] + + +def read_until_eof(fd, encoding='utf8'): + """Read file descriptor ``fd`` until EOF. Return decoded string.""" + decoder = codecs.getincrementaldecoder(encoding)() + outp = text_type() + while True: + try: + _exc = os.read(fd, 100) + except OSError: # linux EOF + break + if not _exc: # bsd EOF + break + outp += decoder.decode(_exc, final=False) + return outp + + +@contextlib.contextmanager +def echo_off(fd): + """Ensure any bytes written to pty fd are not duplicated as output.""" + try: + attrs = termios.tcgetattr(fd) + attrs[3] = attrs[3] & ~termios.ECHO + termios.tcsetattr(fd, termios.TCSANOW, attrs) + yield + finally: + attrs[3] = attrs[3] | termios.ECHO + termios.tcsetattr(fd, termios.TCSANOW, attrs) + + +def unicode_cap(cap): + """Return the result of ``tigetstr`` except as Unicode.""" + try: + val = curses.tigetstr(cap) + except curses.error: + val = None + if val: + return val.decode('latin1') + return u'' + + +def unicode_parm(cap, *parms): + """Return the result of ``tparm(tigetstr())`` except as Unicode.""" + try: + cap = curses.tigetstr(cap) + except curses.error: + cap = None + if cap: + try: + val = curses.tparm(cap, *parms) + except curses.error: + val = None + if val: + return val.decode('latin1') + return u'' + + +@pytest.fixture(params=binary_terminals) +def unsupported_sequence_terminals(request): + """Terminals that emit warnings for unsupported sequence-awareness.""" + return request.param + + +@pytest.fixture(params=all_xterms_params) +def xterms(request): + """Common kind values for xterm terminals.""" + return request.param + + +@pytest.fixture(params=all_terms_params) +def all_terms(request): + """Common kind values for all kinds of terminals.""" + return request.param + + +@pytest.fixture(params=many_lines_params) +def many_lines(request): + """Various number of lines for screen height.""" + return request.param + + +@pytest.fixture(params=many_columns_params) +def many_columns(request): + """Various number of columns for screen width.""" + return request.param diff --git a/blessings/tests/test_core.py b/blessings/tests/test_core.py new file mode 100644 index 0000000..ce1e838 --- /dev/null +++ b/blessings/tests/test_core.py @@ -0,0 +1,457 @@ +# -*- coding: utf-8 -*- +"Core blessings Terminal() tests." + +# std +try: + from StringIO import StringIO +except ImportError: + from io import StringIO +import collections +import warnings +import platform +import locale +import sys +import imp +import os + +# local +from .accessories import ( + as_subprocess, + TestTerminal, + unicode_cap, + all_terms +) + +# 3rd party +import mock +import pytest + + +def test_export_only_Terminal(): + "Ensure only Terminal instance is exported for import * statements." + import blessings + assert blessings.__all__ == ('Terminal',) + + +def test_null_location(all_terms): + "Make sure ``location()`` with no args just does position restoration." + @as_subprocess + def child(kind): + t = TestTerminal(stream=StringIO(), force_styling=True) + with t.location(): + pass + expected_output = u''.join( + (unicode_cap('sc'), unicode_cap('rc'))) + assert (t.stream.getvalue() == expected_output) + + child(all_terms) + + +def test_flipped_location_move(all_terms): + "``location()`` and ``move()`` receive counter-example arguments." + @as_subprocess + def child(kind): + buf = StringIO() + t = TestTerminal(stream=buf, force_styling=True) + y, x = 10, 20 + with t.location(y, x): + xy_val = t.move(x, y) + yx_val = buf.getvalue()[len(t.sc):] + assert xy_val == yx_val + + child(all_terms) + + +def test_yield_keypad(): + "Ensure ``keypad()`` writes keyboard_xmit and keyboard_local." + @as_subprocess + def child(kind): + # given, + t = TestTerminal(stream=StringIO(), force_styling=True) + expected_output = u''.join((t.smkx, t.rmkx)) + + # exercise, + with t.keypad(): + pass + + # verify. + assert (t.stream.getvalue() == expected_output) + + child(kind='xterm') + + +def test_null_fileno(): + "Make sure ``Terminal`` works when ``fileno`` is ``None``." + @as_subprocess + def child(): + # This simulates piping output to another program. + out = StringIO() + out.fileno = None + t = TestTerminal(stream=out) + assert (t.save == u'') + + child() + + +def test_number_of_colors_without_tty(): + "``number_of_colors`` should return 0 when there's no tty." + @as_subprocess + def child_256_nostyle(): + t = TestTerminal(stream=StringIO()) + assert (t.number_of_colors == 0) + + @as_subprocess + def child_256_forcestyle(): + t = TestTerminal(stream=StringIO(), force_styling=True) + assert (t.number_of_colors == 256) + + @as_subprocess + def child_8_forcestyle(): + t = TestTerminal(kind='ansi', stream=StringIO(), + force_styling=True) + assert (t.number_of_colors == 8) + + @as_subprocess + def child_0_forcestyle(): + t = TestTerminal(kind='vt220', stream=StringIO(), + force_styling=True) + assert (t.number_of_colors == 0) + + child_0_forcestyle() + child_8_forcestyle() + child_256_forcestyle() + child_256_nostyle() + + +def test_number_of_colors_with_tty(): + "test ``number_of_colors`` 0, 8, and 256." + @as_subprocess + def child_256(): + t = TestTerminal() + assert (t.number_of_colors == 256) + + @as_subprocess + def child_8(): + t = TestTerminal(kind='ansi') + assert (t.number_of_colors == 8) + + @as_subprocess + def child_0(): + t = TestTerminal(kind='vt220') + assert (t.number_of_colors == 0) + + child_0() + child_8() + child_256() + + +def test_init_descriptor_always_initted(all_terms): + "Test height and width with non-tty Terminals." + @as_subprocess + def child(kind): + t = TestTerminal(kind=kind, stream=StringIO()) + assert t._init_descriptor == sys.__stdout__.fileno() + assert (isinstance(t.height, int)) + assert (isinstance(t.width, int)) + assert t.height == t._height_and_width()[0] + assert t.width == t._height_and_width()[1] + + child(all_terms) + + +def test_force_styling_none(all_terms): + "If ``force_styling=None`` is used, don't ever do styling." + @as_subprocess + def child(kind): + t = TestTerminal(kind=kind, force_styling=None) + assert (t.save == '') + assert (t.color(9) == '') + assert (t.bold('oi') == 'oi') + + child(all_terms) + + +def test_setupterm_singleton_issue33(): + "A warning is emitted if a new terminal ``kind`` is used per process." + @as_subprocess + def child(): + warnings.filterwarnings("error", category=UserWarning) + + # instantiate first terminal, of type xterm-256color + term = TestTerminal(force_styling=True) + + try: + # a second instantiation raises UserWarning + term = TestTerminal(kind="vt220", force_styling=True) + except UserWarning: + err = sys.exc_info()[1] + assert (err.args[0].startswith( + 'A terminal of kind "vt220" has been requested') + ), err.args[0] + assert ('a terminal of kind "xterm-256color" will ' + 'continue to be returned' in err.args[0]), err.args[0] + else: + # unless term is not a tty and setupterm() is not called + assert not term.is_a_tty or False, 'Should have thrown exception' + warnings.resetwarnings() + + child() + + +def test_setupterm_invalid_issue39(): + "A warning is emitted if TERM is invalid." + # https://bugzilla.mozilla.org/show_bug.cgi?id=878089 + + # if TERM is unset, defaults to 'unknown', which should + # fail to lookup and emit a warning, only. + @as_subprocess + def child(): + warnings.filterwarnings("error", category=UserWarning) + + try: + term = TestTerminal(kind='unknown', force_styling=True) + except UserWarning: + err = sys.exc_info()[1] + assert err.args[0] == ( + "Failed to setupterm(kind='unknown'): " + "setupterm: could not find terminal") + else: + assert not term.is_a_tty and not term.does_styling, ( + 'Should have thrown exception') + warnings.resetwarnings() + + child() + + +def test_setupterm_invalid_has_no_styling(): + "An unknown TERM type does not perform styling." + # https://bugzilla.mozilla.org/show_bug.cgi?id=878089 + + # if TERM is unset, defaults to 'unknown', which should + # fail to lookup and emit a warning, only. + @as_subprocess + def child(): + warnings.filterwarnings("ignore", category=UserWarning) + + term = TestTerminal(kind='unknown', force_styling=True) + assert term.kind is None + assert term.does_styling is False + assert term.number_of_colors == 0 + warnings.resetwarnings() + + child() + + +@pytest.mark.skipif(platform.python_implementation() == 'PyPy', + reason='PyPy freezes') +def test_missing_ordereddict_uses_module(monkeypatch): + "ordereddict module is imported when without collections.OrderedDict." + import blessings.keyboard + + if hasattr(collections, 'OrderedDict'): + monkeypatch.delattr('collections.OrderedDict') + + try: + imp.reload(blessings.keyboard) + except ImportError as err: + assert err.args[0] in ("No module named ordereddict", # py2 + "No module named 'ordereddict'") # py3 + sys.modules['ordereddict'] = mock.Mock() + sys.modules['ordereddict'].OrderedDict = -1 + imp.reload(blessings.keyboard) + assert blessings.keyboard.OrderedDict == -1 + del sys.modules['ordereddict'] + monkeypatch.undo() + imp.reload(blessings.keyboard) + else: + assert platform.python_version_tuple() < ('2', '7') # reached by py2.6 + + +@pytest.mark.skipif(platform.python_implementation() == 'PyPy', + reason='PyPy freezes') +def test_python3_2_raises_exception(monkeypatch): + "Test python version 3.0 through 3.2 raises an exception." + import blessings + + monkeypatch.setattr('platform.python_version_tuple', + lambda: ('3', '2', '2')) + + try: + imp.reload(blessings) + except ImportError as err: + assert err.args[0] == ( + 'Blessings needs Python 3.2.3 or greater for Python 3 ' + 'support due to http://bugs.python.org/issue10570.') + monkeypatch.undo() + imp.reload(blessings) + else: + 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 + assert '_' not in dir(blessings.terminal) + + +def test_IOUnsupportedOperation(): + "Ensure stream that throws IOUnsupportedOperation results in non-tty." + @as_subprocess + def child(): + import blessings.terminal + + def side_effect(): + raise blessings.terminal.IOUnsupportedOperation + + mock_stream = mock.Mock() + mock_stream.fileno = side_effect + + term = TestTerminal(stream=mock_stream) + assert term.stream == mock_stream + assert term.does_styling is False + assert term.is_a_tty is False + assert term.number_of_colors is 0 + + child() + + +def test_winsize_IOError_returns_environ(): + """When _winsize raises IOError, defaults from os.environ given.""" + @as_subprocess + def child(): + def side_effect(fd): + raise IOError + + term = TestTerminal() + term._winsize = side_effect + os.environ['COLUMNS'] = '1984' + os.environ['LINES'] = '1888' + assert term._height_and_width() == (1888, 1984, None, None) + + child() + + +def test_yield_fullscreen(all_terms): + "Ensure ``fullscreen()`` writes enter_fullscreen and exit_fullscreen." + @as_subprocess + def child(kind): + t = TestTerminal(stream=StringIO(), force_styling=True) + t.enter_fullscreen = u'BEGIN' + t.exit_fullscreen = u'END' + with t.fullscreen(): + pass + expected_output = u''.join((t.enter_fullscreen, t.exit_fullscreen)) + assert (t.stream.getvalue() == expected_output) + + child(all_terms) + + +def test_yield_hidden_cursor(all_terms): + "Ensure ``hidden_cursor()`` writes hide_cursor and normal_cursor." + @as_subprocess + def child(kind): + t = TestTerminal(stream=StringIO(), force_styling=True) + t.hide_cursor = u'BEGIN' + t.normal_cursor = u'END' + with t.hidden_cursor(): + pass + expected_output = u''.join((t.hide_cursor, t.normal_cursor)) + assert (t.stream.getvalue() == expected_output) + + child(all_terms) + + +def test_no_preferredencoding_fallback_ascii(): + "Ensure empty preferredencoding value defaults to ascii." + @as_subprocess + def child(): + with mock.patch('locale.getpreferredencoding') as get_enc: + get_enc.return_value = u'' + t = TestTerminal() + assert t._encoding == 'ascii' + + child() + + +def test_unknown_preferredencoding_warned_and_fallback_ascii(): + "Ensure a locale without a codecs incrementaldecoder emits a warning." + @as_subprocess + def child(): + with mock.patch('locale.getpreferredencoding') as get_enc: + with warnings.catch_warnings(record=True) as warned: + get_enc.return_value = '---unknown--encoding---' + t = TestTerminal() + assert t._encoding == 'ascii' + assert len(warned) == 1 + assert issubclass(warned[-1].category, UserWarning) + assert "fallback to ASCII" in str(warned[-1].message) + + child() + + +def test_win32_missing_tty_modules(monkeypatch): + "Ensure dummy exception is used when io is without UnsupportedOperation." + @as_subprocess + def child(): + OLD_STYLE = False + try: + original_import = getattr(__builtins__, '__import__') + OLD_STYLE = True + except AttributeError: + original_import = __builtins__['__import__'] + + tty_modules = ('termios', 'fcntl', 'tty') + + def __import__(name, *args, **kwargs): + if name in tty_modules: + raise ImportError + return original_import(name, *args, **kwargs) + + for module in tty_modules: + sys.modules.pop(module, None) + + warnings.filterwarnings("error", category=UserWarning) + try: + if OLD_STYLE: + __builtins__.__import__ = __import__ + else: + __builtins__['__import__'] = __import__ + try: + import blessings.terminal + imp.reload(blessings.terminal) + except UserWarning: + err = sys.exc_info()[1] + assert err.args[0] == blessings.terminal.msg_nosupport + + warnings.filterwarnings("ignore", category=UserWarning) + import blessings.terminal + imp.reload(blessings.terminal) + assert blessings.terminal.HAS_TTY is False + term = blessings.terminal.Terminal('ansi') + assert term.height == 24 + assert term.width == 80 + + finally: + if OLD_STYLE: + setattr(__builtins__, '__import__', original_import) + else: + __builtins__['__import__'] = original_import + warnings.resetwarnings() + import blessings.terminal + imp.reload(blessings.terminal) + + child() diff --git a/blessings/tests/test_formatters.py b/blessings/tests/test_formatters.py new file mode 100644 index 0000000..3184140 --- /dev/null +++ b/blessings/tests/test_formatters.py @@ -0,0 +1,403 @@ +# -*- coding: utf-8 -*- +"""Tests string formatting functions.""" +import curses +import mock + + +def test_parameterizing_string_args_unspecified(monkeypatch): + """Test default args of formatters.ParameterizingString.""" + from blessings.formatters import ParameterizingString, FormattingString + # first argument to tparm() is the sequence name, returned as-is; + # subsequent arguments are usually Integers. + tparm = lambda *args: u'~'.join( + arg.decode('latin1') if not num else '%s' % (arg,) + for num, arg in enumerate(args)).encode('latin1') + + monkeypatch.setattr(curses, 'tparm', tparm) + + # given, + pstr = ParameterizingString(u'') + + # excersize __new__ + assert str(pstr) == u'' + assert pstr._normal == u'' + assert pstr._name == u'' + + # excersize __call__ + zero = pstr(0) + assert type(zero) is FormattingString + assert zero == u'~0' + assert zero('text') == u'~0text' + + # excersize __call__ with multiple args + onetwo = pstr(1, 2) + assert type(onetwo) is FormattingString + assert onetwo == u'~1~2' + assert onetwo('text') == u'~1~2text' + + +def test_parameterizing_string_args(monkeypatch): + """Test basic formatters.ParameterizingString.""" + from blessings.formatters import ParameterizingString, FormattingString + + # first argument to tparm() is the sequence name, returned as-is; + # subsequent arguments are usually Integers. + tparm = lambda *args: u'~'.join( + arg.decode('latin1') if not num else '%s' % (arg,) + for num, arg in enumerate(args)).encode('latin1') + + monkeypatch.setattr(curses, 'tparm', tparm) + + # given, + pstr = ParameterizingString(u'cap', u'norm', u'seq-name') + + # excersize __new__ + assert str(pstr) == u'cap' + assert pstr._normal == u'norm' + assert pstr._name == u'seq-name' + + # excersize __call__ + zero = pstr(0) + assert type(zero) is FormattingString + assert zero == u'cap~0' + assert zero('text') == u'cap~0textnorm' + + # excersize __call__ with multiple args + onetwo = pstr(1, 2) + assert type(onetwo) is FormattingString + assert onetwo == u'cap~1~2' + assert onetwo('text') == u'cap~1~2textnorm' + + +def test_parameterizing_string_type_error(monkeypatch): + """Test formatters.ParameterizingString raising TypeError""" + from blessings.formatters import ParameterizingString + + def tparm_raises_TypeError(*args): + raise TypeError('custom_err') + + monkeypatch.setattr(curses, 'tparm', tparm_raises_TypeError) + + # given, + pstr = ParameterizingString(u'cap', u'norm', u'cap-name') + + # ensure TypeError when given a string raises custom exception + try: + pstr('XYZ') + assert False, "previous call should have raised TypeError" + except TypeError as err: + assert (err.args[0] == ( # py3x + "A native or nonexistent capability template, " + "'cap-name' received invalid argument ('XYZ',): " + "custom_err. You probably misspelled a " + "formatting call like `bright_red'") or + err.args[0] == ( + "A native or nonexistent capability template, " + "u'cap-name' received invalid argument ('XYZ',): " + "custom_err. You probably misspelled a " + "formatting call like `bright_red'")) + + # ensure TypeError when given an integer raises its natural exception + try: + pstr(0) + assert False, "previous call should have raised TypeError" + except TypeError as err: + assert err.args[0] == "custom_err" + + +def test_formattingstring(monkeypatch): + """Test formatters.FormattingString""" + from blessings.formatters import FormattingString + + # given, with arg + pstr = FormattingString(u'attr', u'norm') + + # excersize __call__, + assert pstr._normal == u'norm' + assert str(pstr) == u'attr' + assert pstr('text') == u'attrtextnorm' + + # given, without arg + pstr = FormattingString(u'', u'norm') + assert pstr('text') == u'text' + + +def test_nullcallablestring(monkeypatch): + """Test formatters.NullCallableString""" + from blessings.formatters import (NullCallableString) + + # given, with arg + pstr = NullCallableString() + + # excersize __call__, + assert str(pstr) == u'' + assert pstr('text') == u'text' + assert pstr('text', 1) == u'' + assert pstr('text', 'moretext') == u'' + assert pstr(99, 1) == u'' + assert pstr() == u'' + assert pstr(0) == u'' + + +def test_split_compound(): + """Test formatters.split_compound.""" + from blessings.formatters import split_compound + + assert split_compound(u'') == [u''] + assert split_compound(u'a_b_c') == [u'a', u'b', u'c'] + assert split_compound(u'a_on_b_c') == [u'a', u'on_b', u'c'] + assert split_compound(u'a_bright_b_c') == [u'a', u'bright_b', u'c'] + assert split_compound(u'a_on_bright_b_c') == [u'a', u'on_bright_b', u'c'] + + +def test_resolve_capability(monkeypatch): + """Test formatters.resolve_capability and term sugaring """ + from blessings.formatters import resolve_capability + + # given, always returns a b'seq' + tigetstr = lambda attr: ('seq-%s' % (attr,)).encode('latin1') + monkeypatch.setattr(curses, 'tigetstr', tigetstr) + term = mock.Mock() + term._sugar = dict(mnemonic='xyz') + + # excersize + assert resolve_capability(term, 'mnemonic') == u'seq-xyz' + assert resolve_capability(term, 'natural') == u'seq-natural' + + # given, where tigetstr returns None + tigetstr_none = lambda attr: None + monkeypatch.setattr(curses, 'tigetstr', tigetstr_none) + + # excersize, + assert resolve_capability(term, 'natural') == u'' + + # given, where does_styling is False + def raises_exception(*args): + assert False, "Should not be called" + term.does_styling = False + monkeypatch.setattr(curses, 'tigetstr', raises_exception) + + # excersize, + assert resolve_capability(term, 'natural') == u'' + + +def test_resolve_color(monkeypatch): + """Test formatters.resolve_color.""" + from blessings.formatters import (resolve_color, + FormattingString, + NullCallableString) + + color_cap = lambda digit: 'seq-%s' % (digit,) + monkeypatch.setattr(curses, 'COLOR_RED', 1984) + + # given, terminal with color capabilities + term = mock.Mock() + term._background_color = color_cap + term._foreground_color = color_cap + term.number_of_colors = -1 + term.normal = 'seq-normal' + + # excersize, + red = resolve_color(term, 'red') + assert type(red) == FormattingString + assert red == u'seq-1984' + assert red('text') == u'seq-1984textseq-normal' + + # excersize bold, +8 + bright_red = resolve_color(term, 'bright_red') + assert type(bright_red) == FormattingString + assert bright_red == u'seq-1992' + assert bright_red('text') == u'seq-1992textseq-normal' + + # given, terminal without color + term.number_of_colors = 0 + + # excersize, + red = resolve_color(term, 'red') + assert type(red) == NullCallableString + assert red == u'' + assert red('text') == u'text' + + # excesize bold, + bright_red = resolve_color(term, 'bright_red') + assert type(bright_red) == NullCallableString + assert bright_red == u'' + assert bright_red('text') == u'text' + + +def test_resolve_attribute_as_color(monkeypatch): + """ Test simple resolve_attribte() given color name. """ + import blessings + from blessings.formatters import resolve_attribute + + resolve_color = lambda term, digit: 'seq-%s' % (digit,) + COLORS = set(['COLORX', 'COLORY']) + COMPOUNDABLES = set(['JOINT', 'COMPOUND']) + monkeypatch.setattr(blessings.formatters, 'resolve_color', resolve_color) + monkeypatch.setattr(blessings.formatters, 'COLORS', COLORS) + monkeypatch.setattr(blessings.formatters, 'COMPOUNDABLES', COMPOUNDABLES) + term = mock.Mock() + assert resolve_attribute(term, 'COLORX') == u'seq-COLORX' + + +def test_resolve_attribute_as_compoundable(monkeypatch): + """ Test simple resolve_attribte() given a compoundable. """ + import blessings + from blessings.formatters import resolve_attribute, FormattingString + + resolve_cap = lambda term, digit: 'seq-%s' % (digit,) + COMPOUNDABLES = set(['JOINT', 'COMPOUND']) + monkeypatch.setattr(blessings.formatters, + 'resolve_capability', + resolve_cap) + monkeypatch.setattr(blessings.formatters, 'COMPOUNDABLES', COMPOUNDABLES) + term = mock.Mock() + term.normal = 'seq-normal' + + compound = resolve_attribute(term, 'JOINT') + assert type(compound) is FormattingString + assert str(compound) == u'seq-JOINT' + assert compound('text') == u'seq-JOINTtextseq-normal' + + +def test_resolve_attribute_non_compoundables(monkeypatch): + """ Test recursive compounding of resolve_attribute(). """ + import blessings + from blessings.formatters import resolve_attribute, ParameterizingString + uncompoundables = lambda attr: ['split', 'compound'] + resolve_cap = lambda term, digit: 'seq-%s' % (digit,) + monkeypatch.setattr(blessings.formatters, + 'split_compound', + uncompoundables) + monkeypatch.setattr(blessings.formatters, + 'resolve_capability', + resolve_cap) + tparm = lambda *args: u'~'.join( + arg.decode('latin1') if not num else '%s' % (arg,) + for num, arg in enumerate(args)).encode('latin1') + monkeypatch.setattr(curses, 'tparm', tparm) + + term = mock.Mock() + term.normal = 'seq-normal' + + # given + pstr = resolve_attribute(term, 'not-a-compoundable') + assert type(pstr) == ParameterizingString + assert str(pstr) == u'seq-not-a-compoundable' + # this is like calling term.move_x(3) + assert pstr(3) == u'seq-not-a-compoundable~3' + # this is like calling term.move_x(3)('text') + assert pstr(3)('text') == u'seq-not-a-compoundable~3textseq-normal' + + +def test_resolve_attribute_recursive_compoundables(monkeypatch): + """ Test recursive compounding of resolve_attribute(). """ + import blessings + from blessings.formatters import resolve_attribute, FormattingString + + # patch, + resolve_cap = lambda term, digit: 'seq-%s' % (digit,) + monkeypatch.setattr(blessings.formatters, + 'resolve_capability', + resolve_cap) + tparm = lambda *args: u'~'.join( + arg.decode('latin1') if not num else '%s' % (arg,) + for num, arg in enumerate(args)).encode('latin1') + monkeypatch.setattr(curses, 'tparm', tparm) + monkeypatch.setattr(curses, 'COLOR_RED', 6502) + monkeypatch.setattr(curses, 'COLOR_BLUE', 6800) + + color_cap = lambda digit: 'seq-%s' % (digit,) + term = mock.Mock() + term._background_color = color_cap + term._foreground_color = color_cap + term.normal = 'seq-normal' + + # given, + pstr = resolve_attribute(term, 'bright_blue_on_red') + + # excersize, + assert type(pstr) == FormattingString + assert str(pstr) == 'seq-6808seq-6502' + assert pstr('text') == 'seq-6808seq-6502textseq-normal' + + +def test_pickled_parameterizing_string(monkeypatch): + """Test pickle-ability of a formatters.ParameterizingString.""" + from blessings.formatters import ParameterizingString, FormattingString + + # simply send()/recv() over multiprocessing Pipe, a simple + # pickle.loads(dumps(...)) did not reproduce this issue, + from multiprocessing import Pipe + import pickle + + # first argument to tparm() is the sequence name, returned as-is; + # subsequent arguments are usually Integers. + tparm = lambda *args: u'~'.join( + arg.decode('latin1') if not num else '%s' % (arg,) + for num, arg in enumerate(args)).encode('latin1') + + monkeypatch.setattr(curses, 'tparm', tparm) + + # given, + pstr = ParameterizingString(u'seqname', u'norm', u'cap-name') + + # multiprocessing Pipe implicitly pickles. + r, w = Pipe() + + # excersize picklability of ParameterizingString + for proto_num in range(pickle.HIGHEST_PROTOCOL): + assert pstr == pickle.loads(pickle.dumps(pstr, protocol=proto_num)) + w.send(pstr) + r.recv() == pstr + + # excersize picklability of FormattingString + # -- the return value of calling ParameterizingString + zero = pstr(0) + for proto_num in range(pickle.HIGHEST_PROTOCOL): + assert zero == pickle.loads(pickle.dumps(zero, protocol=proto_num)) + w.send(zero) + r.recv() == zero + + +def test_tparm_returns_null(monkeypatch): + """ Test 'tparm() returned NULL' is caught (win32 PDCurses systems). """ + # on win32, any calls to tparm raises curses.error with message, + # "tparm() returned NULL", function PyCurses_tparm of _cursesmodule.c + from blessings.formatters import ParameterizingString, NullCallableString + + def tparm(*args): + raise curses.error("tparm() returned NULL") + + monkeypatch.setattr(curses, 'tparm', tparm) + + term = mock.Mock() + term.normal = 'seq-normal' + + pstr = ParameterizingString(u'cap', u'norm', u'seq-name') + + value = pstr(u'x') + assert type(value) is NullCallableString + + +def test_tparm_other_exception(monkeypatch): + """ Test 'tparm() returned NULL' is caught (win32 PDCurses systems). """ + # on win32, any calls to tparm raises curses.error with message, + # "tparm() returned NULL", function PyCurses_tparm of _cursesmodule.c + from blessings.formatters import ParameterizingString, NullCallableString + + def tparm(*args): + raise curses.error("unexpected error in tparm()") + + monkeypatch.setattr(curses, 'tparm', tparm) + + term = mock.Mock() + term.normal = 'seq-normal' + + pstr = ParameterizingString(u'cap', u'norm', u'seq-name') + + try: + pstr(u'x') + assert False, "previous call should have raised curses.error" + except curses.error: + pass diff --git a/blessings/tests/test_keyboard.py b/blessings/tests/test_keyboard.py new file mode 100644 index 0000000..393b248 --- /dev/null +++ b/blessings/tests/test_keyboard.py @@ -0,0 +1,902 @@ +# -*- coding: utf-8 -*- +"Tests for keyboard support." +# std imports +import functools +import tempfile +try: + from StringIO import StringIO +except ImportError: + import io + StringIO = io.StringIO +import platform +import signal +import curses +import time +import math +import tty # NOQA +import pty +import sys +import os + +# local +from .accessories import ( + read_until_eof, + read_until_semaphore, + SEND_SEMAPHORE, + RECV_SEMAPHORE, + as_subprocess, + TestTerminal, + SEMAPHORE, + all_terms, + echo_off, + xterms, +) + +# 3rd-party +import pytest +import mock + +if sys.version_info[0] == 3: + unichr = chr + + +def test_char_is_ready_interrupted(): + "_char_is_ready() should not be interrupted with a signal handler." + pid, master_fd = pty.fork() + if pid is 0: + try: + cov = __import__('cov_core_init').init() + except ImportError: + cov = None + + # child pauses, writes semaphore and begins awaiting input + global got_sigwinch + got_sigwinch = False + + def on_resize(sig, action): + global got_sigwinch + got_sigwinch = True + + term = TestTerminal() + signal.signal(signal.SIGWINCH, on_resize) + read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.keystroke_input(raw=True): + assert term.keystroke(timeout=1.05) == u'' + os.write(sys.__stdout__.fileno(), b'complete') + assert got_sigwinch is True + if cov is not None: + cov.stop() + cov.save() + os._exit(0) + + with echo_off(master_fd): + os.write(master_fd, SEND_SEMAPHORE) + read_until_semaphore(master_fd) + stime = time.time() + os.kill(pid, signal.SIGWINCH) + output = read_until_eof(master_fd) + + pid, status = os.waitpid(pid, 0) + assert output == u'complete' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 1.0 + + +def test_char_is_ready_interrupted_nonetype(): + "_char_is_ready() should also allow interruption with timeout of None." + pid, master_fd = pty.fork() + if pid is 0: + try: + cov = __import__('cov_core_init').init() + except ImportError: + cov = None + + # child pauses, writes semaphore and begins awaiting input + global got_sigwinch + got_sigwinch = False + + def on_resize(sig, action): + global got_sigwinch + got_sigwinch = True + + term = TestTerminal() + signal.signal(signal.SIGWINCH, on_resize) + read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.keystroke_input(raw=True): + term.keystroke(timeout=1) + os.write(sys.__stdout__.fileno(), b'complete') + assert got_sigwinch is True + if cov is not None: + cov.stop() + cov.save() + os._exit(0) + + with echo_off(master_fd): + os.write(master_fd, SEND_SEMAPHORE) + read_until_semaphore(master_fd) + stime = time.time() + time.sleep(0.05) + os.kill(pid, signal.SIGWINCH) + output = read_until_eof(master_fd) + + pid, status = os.waitpid(pid, 0) + assert output == u'complete' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 1.0 + + +def test_char_is_ready_interrupted_interruptable(): + "_char_is_ready() may be interrupted when interruptable=False." + pid, master_fd = pty.fork() + if pid is 0: + try: + cov = __import__('cov_core_init').init() + except ImportError: + cov = None + + # child pauses, writes semaphore and begins awaiting input + global got_sigwinch + got_sigwinch = False + + def on_resize(sig, action): + global got_sigwinch + got_sigwinch = True + + term = TestTerminal() + signal.signal(signal.SIGWINCH, on_resize) + read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.keystroke_input(raw=True): + term.keystroke(timeout=1.05, interruptable=False) + os.write(sys.__stdout__.fileno(), b'complete') + assert got_sigwinch is True + if cov is not None: + cov.stop() + cov.save() + os._exit(0) + + with echo_off(master_fd): + os.write(master_fd, SEND_SEMAPHORE) + read_until_semaphore(master_fd) + stime = time.time() + time.sleep(0.05) + os.kill(pid, signal.SIGWINCH) + output = read_until_eof(master_fd) + + pid, status = os.waitpid(pid, 0) + assert output == u'complete' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 0.0 + + +def test_char_is_ready_interrupted_nonetype_interruptable(): + """_char_is_ready() may be interrupted when interruptable=False with + timeout None.""" + pid, master_fd = pty.fork() + if pid is 0: + try: + cov = __import__('cov_core_init').init() + except ImportError: + cov = None + + # child pauses, writes semaphore and begins awaiting input + global got_sigwinch + got_sigwinch = False + + def on_resize(sig, action): + global got_sigwinch + got_sigwinch = True + + term = TestTerminal() + signal.signal(signal.SIGWINCH, on_resize) + read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.keystroke_input(raw=True): + term.keystroke(timeout=None, interruptable=False) + os.write(sys.__stdout__.fileno(), b'complete') + assert got_sigwinch is True + if cov is not None: + cov.stop() + cov.save() + os._exit(0) + + with echo_off(master_fd): + os.write(master_fd, SEND_SEMAPHORE) + read_until_semaphore(master_fd) + stime = time.time() + time.sleep(0.05) + os.kill(pid, signal.SIGWINCH) + os.write(master_fd, b'X') + output = read_until_eof(master_fd) + + pid, status = os.waitpid(pid, 0) + assert output == u'complete' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 0.0 + + +def test_keystroke_input_no_kb(): + "keystroke_input() should not call tty.setcbreak() without keyboard." + @as_subprocess + def child(): + with tempfile.NamedTemporaryFile() as stream: + term = TestTerminal(stream=stream) + with mock.patch("tty.setcbreak") as mock_setcbreak: + with term.keystroke_input(): + assert not mock_setcbreak.called + assert term.keyboard_fd is None + child() + + +def test_notty_kb_is_None(): + "keyboard_fd should be None when os.isatty returns False." + # in this scenerio, stream is sys.__stdout__, + # but os.isatty(0) is False, + # such as when piping output to less(1) + @as_subprocess + def child(): + with mock.patch("os.isatty") as mock_isatty: + mock_isatty.return_value = False + term = TestTerminal() + assert term.keyboard_fd is None + child() + + +def test_raw_input_no_kb(): + "keystroke_input(raw=True) should not call tty.setraw() without keyboard." + @as_subprocess + def child(): + with tempfile.NamedTemporaryFile() as stream: + term = TestTerminal(stream=stream) + with mock.patch("tty.setraw") as mock_setraw: + with term.keystroke_input(raw=True): + assert not mock_setraw.called + assert term.keyboard_fd is None + child() + + +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()) + stime = time.time() + assert term.keyboard_fd is None + assert term._char_is_ready(timeout=1.1) is False + assert (math.floor(time.time() - stime) == 1.0) + child() + + +def test_keystroke_0s_keystroke_input_noinput(): + "0-second keystroke without input; '' should be returned." + @as_subprocess + def child(): + term = TestTerminal() + with term.keystroke_input(): + stime = time.time() + inp = term.keystroke(timeout=0) + assert (inp == u'') + assert (math.floor(time.time() - stime) == 0.0) + child() + + +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()) + with term.keystroke_input(): + stime = time.time() + inp = term.keystroke(timeout=0) + assert (inp == u'') + assert (math.floor(time.time() - stime) == 0.0) + child() + + +def test_keystroke_1s_keystroke_input_noinput(): + "1-second keystroke without input; '' should be returned after ~1 second." + @as_subprocess + def child(): + term = TestTerminal() + with term.keystroke_input(): + stime = time.time() + inp = term.keystroke(timeout=1) + assert (inp == u'') + assert (math.floor(time.time() - stime) == 1.0) + child() + + +def test_keystroke_1s_keystroke_input_noinput_nokb(): + "1-second keystroke without input or keyboard." + @as_subprocess + def child(): + term = TestTerminal(stream=StringIO()) + with term.keystroke_input(): + stime = time.time() + inp = term.keystroke(timeout=1) + assert (inp == u'') + assert (math.floor(time.time() - stime) == 1.0) + child() + + +def test_keystroke_0s_keystroke_input_with_input(): + "0-second keystroke with input; Keypress should be immediately returned." + pid, master_fd = pty.fork() + if pid is 0: + try: + cov = __import__('cov_core_init').init() + except ImportError: + cov = None + # child pauses, writes semaphore and begins awaiting input + term = TestTerminal() + read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.keystroke_input(): + inp = term.keystroke(timeout=0) + os.write(sys.__stdout__.fileno(), inp.encode('utf-8')) + if cov is not None: + cov.stop() + cov.save() + os._exit(0) + + with echo_off(master_fd): + os.write(master_fd, SEND_SEMAPHORE) + os.write(master_fd, u'x'.encode('ascii')) + read_until_semaphore(master_fd) + stime = time.time() + output = read_until_eof(master_fd) + + pid, status = os.waitpid(pid, 0) + assert output == u'x' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 0.0 + + +def test_keystroke_keystroke_input_with_input_slowly(): + "0-second keystroke with input; Keypress should be immediately returned." + pid, master_fd = pty.fork() + if pid is 0: + try: + cov = __import__('cov_core_init').init() + except ImportError: + cov = None + # child pauses, writes semaphore and begins awaiting input + term = TestTerminal() + read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.keystroke_input(): + while True: + inp = term.keystroke(timeout=0.5) + os.write(sys.__stdout__.fileno(), inp.encode('utf-8')) + if inp == 'X': + break + if cov is not None: + cov.stop() + cov.save() + os._exit(0) + + with echo_off(master_fd): + os.write(master_fd, SEND_SEMAPHORE) + os.write(master_fd, u'a'.encode('ascii')) + time.sleep(0.1) + os.write(master_fd, u'b'.encode('ascii')) + time.sleep(0.1) + os.write(master_fd, u'cdefgh'.encode('ascii')) + time.sleep(0.1) + os.write(master_fd, u'X'.encode('ascii')) + read_until_semaphore(master_fd) + stime = time.time() + output = read_until_eof(master_fd) + + pid, status = os.waitpid(pid, 0) + assert output == u'abcdefghX' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 0.0 + + +def test_keystroke_0s_keystroke_input_multibyte_utf8(): + "0-second keystroke with multibyte utf-8 input; should decode immediately." + # utf-8 bytes represent "latin capital letter upsilon". + pid, master_fd = pty.fork() + if pid is 0: # child + try: + cov = __import__('cov_core_init').init() + except ImportError: + cov = None + term = TestTerminal() + read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.keystroke_input(): + inp = term.keystroke(timeout=0) + os.write(sys.__stdout__.fileno(), inp.encode('utf-8')) + if cov is not None: + cov.stop() + cov.save() + os._exit(0) + + with echo_off(master_fd): + os.write(master_fd, SEND_SEMAPHORE) + os.write(master_fd, u'\u01b1'.encode('utf-8')) + read_until_semaphore(master_fd) + stime = time.time() + output = read_until_eof(master_fd) + pid, status = os.waitpid(pid, 0) + assert output == u'Ʊ' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 0.0 + + +@pytest.mark.skipif(os.environ.get('TRAVIS', None) is not None or + platform.python_implementation() == 'PyPy', + reason="travis-ci nor pypy handle ^C very well.") +def test_keystroke_0s_raw_input_ctrl_c(): + "0-second keystroke with raw allows receiving ^C." + pid, master_fd = pty.fork() + if pid is 0: # child + try: + cov = __import__('cov_core_init').init() + except ImportError: + cov = None + term = TestTerminal() + read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE) + with term.keystroke_input(raw=True): + os.write(sys.__stdout__.fileno(), RECV_SEMAPHORE) + inp = term.keystroke(timeout=0) + os.write(sys.__stdout__.fileno(), inp.encode('latin1')) + if cov is not None: + cov.stop() + cov.save() + os._exit(0) + + with echo_off(master_fd): + os.write(master_fd, SEND_SEMAPHORE) + # ensure child is in raw mode before sending ^C, + read_until_semaphore(master_fd) + os.write(master_fd, u'\x03'.encode('latin1')) + stime = time.time() + output = read_until_eof(master_fd) + pid, status = os.waitpid(pid, 0) + assert (output == u'\x03' or + output == u'' and not os.isatty(0)) + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 0.0 + + +def test_keystroke_0s_keystroke_input_sequence(): + "0-second keystroke with multibyte sequence; should decode immediately." + pid, master_fd = pty.fork() + if pid is 0: # child + try: + cov = __import__('cov_core_init').init() + except ImportError: + cov = None + term = TestTerminal() + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.keystroke_input(): + inp = term.keystroke(timeout=0) + os.write(sys.__stdout__.fileno(), inp.name.encode('ascii')) + sys.stdout.flush() + if cov is not None: + cov.stop() + cov.save() + os._exit(0) + + with echo_off(master_fd): + os.write(master_fd, u'\x1b[D'.encode('ascii')) + read_until_semaphore(master_fd) + stime = time.time() + output = read_until_eof(master_fd) + pid, status = os.waitpid(pid, 0) + assert output == u'KEY_LEFT' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 0.0 + + +def test_keystroke_1s_keystroke_input_with_input(): + "1-second keystroke w/multibyte sequence; should return after ~1 second." + pid, master_fd = pty.fork() + if pid is 0: # child + try: + cov = __import__('cov_core_init').init() + except ImportError: + cov = None + term = TestTerminal() + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.keystroke_input(): + inp = term.keystroke(timeout=3) + os.write(sys.__stdout__.fileno(), inp.name.encode('utf-8')) + sys.stdout.flush() + if cov is not None: + cov.stop() + cov.save() + os._exit(0) + + with echo_off(master_fd): + read_until_semaphore(master_fd) + stime = time.time() + time.sleep(1) + os.write(master_fd, u'\x1b[C'.encode('ascii')) + output = read_until_eof(master_fd) + + pid, status = os.waitpid(pid, 0) + assert output == u'KEY_RIGHT' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 1.0 + + +def test_esc_delay_keystroke_input_035(): + "esc_delay will cause a single ESC (\\x1b) to delay for 0.35." + pid, master_fd = pty.fork() + if pid is 0: # child + try: + cov = __import__('cov_core_init').init() + except ImportError: + cov = None + term = TestTerminal() + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.keystroke_input(): + stime = time.time() + inp = term.keystroke(timeout=5) + measured_time = (time.time() - stime) * 100 + os.write(sys.__stdout__.fileno(), ( + '%s %i' % (inp.name, measured_time,)).encode('ascii')) + sys.stdout.flush() + if cov is not None: + cov.stop() + cov.save() + os._exit(0) + + with echo_off(master_fd): + read_until_semaphore(master_fd) + stime = time.time() + os.write(master_fd, u'\x1b'.encode('ascii')) + key_name, duration_ms = read_until_eof(master_fd).split() + + pid, status = os.waitpid(pid, 0) + assert key_name == u'KEY_ESCAPE' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 0.0 + assert 34 <= int(duration_ms) <= 45, duration_ms + + +def test_esc_delay_keystroke_input_135(): + "esc_delay=1.35 will cause a single ESC (\\x1b) to delay for 1.35." + pid, master_fd = pty.fork() + if pid is 0: # child + try: + cov = __import__('cov_core_init').init() + except ImportError: + cov = None + term = TestTerminal() + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.keystroke_input(): + stime = time.time() + inp = term.keystroke(timeout=5, esc_delay=1.35) + measured_time = (time.time() - stime) * 100 + os.write(sys.__stdout__.fileno(), ( + '%s %i' % (inp.name, measured_time,)).encode('ascii')) + sys.stdout.flush() + if cov is not None: + cov.stop() + cov.save() + os._exit(0) + + with echo_off(master_fd): + read_until_semaphore(master_fd) + stime = time.time() + os.write(master_fd, u'\x1b'.encode('ascii')) + key_name, duration_ms = read_until_eof(master_fd).split() + + pid, status = os.waitpid(pid, 0) + assert key_name == u'KEY_ESCAPE' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 1.0 + assert 134 <= int(duration_ms) <= 145, int(duration_ms) + + +def test_esc_delay_keystroke_input_timout_0(): + """esc_delay still in effect with timeout of 0 ("nonblocking").""" + pid, master_fd = pty.fork() + if pid is 0: # child + try: + cov = __import__('cov_core_init').init() + except ImportError: + cov = None + term = TestTerminal() + os.write(sys.__stdout__.fileno(), SEMAPHORE) + with term.keystroke_input(): + stime = time.time() + inp = term.keystroke(timeout=0) + measured_time = (time.time() - stime) * 100 + os.write(sys.__stdout__.fileno(), ( + '%s %i' % (inp.name, measured_time,)).encode('ascii')) + sys.stdout.flush() + if cov is not None: + cov.stop() + cov.save() + os._exit(0) + + with echo_off(master_fd): + os.write(master_fd, u'\x1b'.encode('ascii')) + read_until_semaphore(master_fd) + stime = time.time() + key_name, duration_ms = read_until_eof(master_fd).split() + + pid, status = os.waitpid(pid, 0) + assert key_name == u'KEY_ESCAPE' + assert os.WEXITSTATUS(status) == 0 + assert math.floor(time.time() - stime) == 0.0 + assert 34 <= int(duration_ms) <= 45, int(duration_ms) + + +def test_keystroke_default_args(): + "Test keyboard.Keystroke constructor with default arguments." + from blessings.keyboard import Keystroke + ks = Keystroke() + assert ks._name is None + assert ks.name == ks._name + assert ks._code is None + assert ks.code == ks._code + assert u'x' == u'x' + ks + assert ks.is_sequence is False + assert repr(ks) in ("u''", # py26, 27 + "''",) # py33 + + +def test_a_keystroke(): + "Test keyboard.Keystroke constructor with set arguments." + from blessings.keyboard import Keystroke + ks = Keystroke(ucs=u'x', code=1, name=u'the X') + assert ks._name is u'the X' + assert ks.name == ks._name + assert ks._code is 1 + assert ks.code == ks._code + assert u'xx' == u'x' + ks + assert ks.is_sequence is True + assert repr(ks) == "the X" + + +def test_get_keyboard_codes(): + "Test all values returned by get_keyboard_codes are from curses." + from blessings.keyboard import ( + get_keyboard_codes, + CURSES_KEYCODE_OVERRIDE_MIXIN, + ) + exemptions = dict(CURSES_KEYCODE_OVERRIDE_MIXIN) + for value, keycode in get_keyboard_codes().items(): + if keycode in exemptions: + assert value == exemptions[keycode] + continue + assert hasattr(curses, keycode) + assert getattr(curses, keycode) == value + + +def test_alternative_left_right(): + "Test _alternative_left_right behavior for space/backspace." + from blessings.keyboard import _alternative_left_right + term = mock.Mock() + term._cuf1 = u'' + term._cub1 = u'' + assert not bool(_alternative_left_right(term)) + term._cuf1 = u' ' + term._cub1 = u'\b' + assert not bool(_alternative_left_right(term)) + term._cuf1 = u'seq-right' + term._cub1 = u'seq-left' + assert (_alternative_left_right(term) == { + u'seq-right': curses.KEY_RIGHT, + u'seq-left': curses.KEY_LEFT}) + + +def test_cuf1_and_cub1_as_RIGHT_LEFT(all_terms): + "Test that cuf1 and cub1 are assigned KEY_RIGHT and KEY_LEFT." + from blessings.keyboard import get_keyboard_sequences + + @as_subprocess + def child(kind): + term = TestTerminal(kind=kind, force_styling=True) + keymap = get_keyboard_sequences(term) + if term._cuf1: + assert term._cuf1 in keymap + assert keymap[term._cuf1] == term.KEY_RIGHT + if term._cub1: + assert term._cub1 in keymap + if term._cub1 == '\b': + assert keymap[term._cub1] == term.KEY_BACKSPACE + else: + assert keymap[term._cub1] == term.KEY_LEFT + + child(all_terms) + + +def test_get_keyboard_sequences_sort_order(xterms): + "ordereddict ensures sequences are ordered longest-first." + @as_subprocess + def child(): + term = TestTerminal(force_styling=True) + maxlen = None + for sequence, code in term._keymap.items(): + if maxlen is not None: + assert len(sequence) <= maxlen + assert sequence + maxlen = len(sequence) + child() + + +def test_get_keyboard_sequence(monkeypatch): + "Test keyboard.get_keyboard_sequence. " + import curses.has_key + import blessings.keyboard + + (KEY_SMALL, KEY_LARGE, KEY_MIXIN) = range(3) + (CAP_SMALL, CAP_LARGE) = 'cap-small cap-large'.split() + (SEQ_SMALL, SEQ_LARGE, SEQ_MIXIN, SEQ_ALT_CUF1, SEQ_ALT_CUB1) = ( + b'seq-small-a', + b'seq-large-abcdefg', + b'seq-mixin', + b'seq-alt-cuf1', + b'seq-alt-cub1_') + + # patch curses functions + monkeypatch.setattr(curses, 'tigetstr', + lambda cap: {CAP_SMALL: SEQ_SMALL, + CAP_LARGE: SEQ_LARGE}[cap]) + + monkeypatch.setattr(curses.has_key, '_capability_names', + dict(((KEY_SMALL, CAP_SMALL,), + (KEY_LARGE, CAP_LARGE,)))) + + # patch global sequence mix-in + monkeypatch.setattr(blessings.keyboard, + 'DEFAULT_SEQUENCE_MIXIN', ( + (SEQ_MIXIN.decode('latin1'), KEY_MIXIN),)) + + # patch for _alternative_left_right + term = mock.Mock() + term._cuf1 = SEQ_ALT_CUF1.decode('latin1') + term._cub1 = SEQ_ALT_CUB1.decode('latin1') + keymap = blessings.keyboard.get_keyboard_sequences(term) + + assert list(keymap.items()) == [ + (SEQ_LARGE.decode('latin1'), KEY_LARGE), + (SEQ_ALT_CUB1.decode('latin1'), curses.KEY_LEFT), + (SEQ_ALT_CUF1.decode('latin1'), curses.KEY_RIGHT), + (SEQ_SMALL.decode('latin1'), KEY_SMALL), + (SEQ_MIXIN.decode('latin1'), KEY_MIXIN)] + + +def test_resolve_sequence(): + "Test resolve_sequence for order-dependent mapping." + from blessings.keyboard import resolve_sequence, OrderedDict + mapper = OrderedDict(((u'SEQ1', 1), + (u'SEQ2', 2), + # takes precedence over LONGSEQ, first-match + (u'KEY_LONGSEQ_longest', 3), + (u'LONGSEQ', 4), + # wont match, LONGSEQ is first-match in this order + (u'LONGSEQ_longer', 5), + # falls through for L{anything_else} + (u'L', 6))) + codes = {1: u'KEY_SEQ1', + 2: u'KEY_SEQ2', + 3: u'KEY_LONGSEQ_longest', + 4: u'KEY_LONGSEQ', + 5: u'KEY_LONGSEQ_longer', + 6: u'KEY_L'} + ks = resolve_sequence(u'', mapper, codes) + assert ks == u'' + assert ks.name is None + assert ks.code is None + assert ks.is_sequence is False + assert repr(ks) in ("u''", # py26, 27 + "''",) # py33 + + ks = resolve_sequence(u'notfound', mapper=mapper, codes=codes) + assert ks == u'n' + assert ks.name is None + assert ks.code is None + assert ks.is_sequence is False + assert repr(ks) in (u"u'n'", "'n'",) + + ks = resolve_sequence(u'SEQ1', mapper, codes) + assert ks == u'SEQ1' + assert ks.name == u'KEY_SEQ1' + assert ks.code is 1 + assert ks.is_sequence is True + assert repr(ks) in (u"KEY_SEQ1", "KEY_SEQ1") + + ks = resolve_sequence(u'LONGSEQ_longer', mapper, codes) + assert ks == u'LONGSEQ' + assert ks.name == u'KEY_LONGSEQ' + assert ks.code is 4 + assert ks.is_sequence is True + assert repr(ks) in (u"KEY_LONGSEQ", "KEY_LONGSEQ") + + ks = resolve_sequence(u'LONGSEQ', mapper, codes) + assert ks == u'LONGSEQ' + assert ks.name == u'KEY_LONGSEQ' + assert ks.code is 4 + assert ks.is_sequence is True + assert repr(ks) in (u"KEY_LONGSEQ", "KEY_LONGSEQ") + + ks = resolve_sequence(u'Lxxxxx', mapper, codes) + assert ks == u'L' + assert ks.name == u'KEY_L' + assert ks.code is 6 + assert ks.is_sequence is True + assert repr(ks) in (u"KEY_L", "KEY_L") + + +def test_keypad_mixins_and_aliases(): + """ Test PC-Style function key translations when in ``keypad`` mode.""" + # Key plain app modified + # Up ^[[A ^[OA ^[[1;mA + # Down ^[[B ^[OB ^[[1;mB + # Right ^[[C ^[OC ^[[1;mC + # Left ^[[D ^[OD ^[[1;mD + # End ^[[F ^[OF ^[[1;mF + # Home ^[[H ^[OH ^[[1;mH + @as_subprocess + def child(kind): + term = TestTerminal(kind=kind, force_styling=True) + from blessings.keyboard import resolve_sequence + + resolve = functools.partial(resolve_sequence, + mapper=term._keymap, + codes=term._keycodes) + + assert resolve(unichr(10)).name == "KEY_ENTER" + assert resolve(unichr(13)).name == "KEY_ENTER" + assert resolve(unichr(8)).name == "KEY_BACKSPACE" + assert resolve(unichr(9)).name == "KEY_TAB" + assert resolve(unichr(27)).name == "KEY_ESCAPE" + assert resolve(unichr(127)).name == "KEY_DELETE" + assert resolve(u"\x1b[A").name == "KEY_UP" + assert resolve(u"\x1b[B").name == "KEY_DOWN" + assert resolve(u"\x1b[C").name == "KEY_RIGHT" + assert resolve(u"\x1b[D").name == "KEY_LEFT" + assert resolve(u"\x1b[U").name == "KEY_PGDOWN" + assert resolve(u"\x1b[V").name == "KEY_PGUP" + assert resolve(u"\x1b[H").name == "KEY_HOME" + assert resolve(u"\x1b[F").name == "KEY_END" + assert resolve(u"\x1b[K").name == "KEY_END" + assert resolve(u"\x1bOM").name == "KEY_ENTER" + assert resolve(u"\x1bOj").name == "KEY_KP_MULTIPLY" + assert resolve(u"\x1bOk").name == "KEY_KP_ADD" + assert resolve(u"\x1bOl").name == "KEY_KP_SEPARATOR" + assert resolve(u"\x1bOm").name == "KEY_KP_SUBTRACT" + assert resolve(u"\x1bOn").name == "KEY_KP_DECIMAL" + assert resolve(u"\x1bOo").name == "KEY_KP_DIVIDE" + assert resolve(u"\x1bOX").name == "KEY_KP_EQUAL" + assert resolve(u"\x1bOp").name == "KEY_KP_0" + assert resolve(u"\x1bOq").name == "KEY_KP_1" + assert resolve(u"\x1bOr").name == "KEY_KP_2" + assert resolve(u"\x1bOs").name == "KEY_KP_3" + assert resolve(u"\x1bOt").name == "KEY_KP_4" + assert resolve(u"\x1bOu").name == "KEY_KP_5" + assert resolve(u"\x1bOv").name == "KEY_KP_6" + assert resolve(u"\x1bOw").name == "KEY_KP_7" + assert resolve(u"\x1bOx").name == "KEY_KP_8" + assert resolve(u"\x1bOy").name == "KEY_KP_9" + assert resolve(u"\x1b[1~").name == "KEY_FIND" + assert resolve(u"\x1b[2~").name == "KEY_INSERT" + assert resolve(u"\x1b[3~").name == "KEY_DELETE" + assert resolve(u"\x1b[4~").name == "KEY_SELECT" + assert resolve(u"\x1b[5~").name == "KEY_PGUP" + assert resolve(u"\x1b[6~").name == "KEY_PGDOWN" + assert resolve(u"\x1b[7~").name == "KEY_HOME" + assert resolve(u"\x1b[8~").name == "KEY_END" + assert resolve(u"\x1b[OA").name == "KEY_UP" + assert resolve(u"\x1b[OB").name == "KEY_DOWN" + assert resolve(u"\x1b[OC").name == "KEY_RIGHT" + assert resolve(u"\x1b[OD").name == "KEY_LEFT" + assert resolve(u"\x1b[OF").name == "KEY_END" + assert resolve(u"\x1b[OH").name == "KEY_HOME" + assert resolve(u"\x1bOP").name == "KEY_F1" + assert resolve(u"\x1bOQ").name == "KEY_F2" + assert resolve(u"\x1bOR").name == "KEY_F3" + assert resolve(u"\x1bOS").name == "KEY_F4" + + child('xterm') diff --git a/blessings/tests/test_length_sequence.py b/blessings/tests/test_length_sequence.py new file mode 100644 index 0000000..0eac165 --- /dev/null +++ b/blessings/tests/test_length_sequence.py @@ -0,0 +1,342 @@ +# encoding: utf-8 +import itertools +import platform +import termios +import struct +import fcntl +import sys +import os +try: + from StringIO import StringIO +except ImportError: + from io import StringIO + +from .accessories import ( + all_terms, + as_subprocess, + TestTerminal, + many_columns, + many_lines, +) + +import pytest + + +def test_length_cjk(): + @as_subprocess + def child(): + term = TestTerminal(kind='xterm-256color') + + # given, + given = term.bold_red(u'コンニチハ, セカイ!') + expected = sum((2, 2, 2, 2, 2, 1, 1, 2, 2, 2, 1,)) + + # exercise, + assert term.length(given) == expected + + child() + + +def test_length_ansiart(): + @as_subprocess + def child(): + import codecs + from blessings.sequences import Sequence + term = TestTerminal(kind='xterm-256color') + # this 'ansi' art contributed by xzip!impure for another project, + # unlike most CP-437 DOS ansi art, this is actually utf-8 encoded. + fname = os.path.join(os.path.dirname(__file__), 'wall.ans') + lines = codecs.open(fname, 'r', 'utf-8').readlines() + assert term.length(lines[0]) == 67 # ^[[64C^[[34m▄▓▄ + assert term.length(lines[1]) == 75 + assert term.length(lines[2]) == 78 + assert term.length(lines[3]) == 78 + assert term.length(lines[4]) == 78 + assert term.length(lines[5]) == 78 + assert term.length(lines[6]) == 77 + child() + + +def test_sequence_length(all_terms): + """Ensure T.length(string containing sequence) is correct.""" + @as_subprocess + def child(kind): + t = TestTerminal(kind=kind) + # Create a list of ascii characters, to be separated + # by word, to be zipped up with a cycling list of + # terminal sequences. Then, compare the length of + # each, the basic plain_text.__len__ vs. the Terminal + # method length. They should be equal. + plain_text = (u'The softest things of the world ' + u'Override the hardest things of the world ' + u'That which has no substance ' + u'Enters into that which has no openings') + if t.bold: + assert (t.length(t.bold) == 0) + assert (t.length(t.bold(u'x')) == 1) + assert (t.length(t.bold_red) == 0) + assert (t.length(t.bold_red(u'x')) == 1) + assert (t.strip(t.bold) == u'') + assert (t.rstrip(t.bold) == u'') + assert (t.lstrip(t.bold) == u'') + assert (t.strip(t.bold(u' x ')) == u'x') + assert (t.strip(t.bold(u'z x q'), 'zq') == u' x ') + assert (t.rstrip(t.bold(u' x ')) == u' x') + assert (t.lstrip(t.bold(u' x ')) == u'x ') + assert (t.strip(t.bold_red) == u'') + assert (t.rstrip(t.bold_red) == u'') + assert (t.lstrip(t.bold_red) == u'') + assert (t.strip(t.bold_red(u' x ')) == u'x') + assert (t.rstrip(t.bold_red(u' x ')) == u' x') + assert (t.lstrip(t.bold_red(u' x ')) == u'x ') + assert (t.strip_seqs(t.bold) == u'') + assert (t.strip_seqs(t.bold(u' x ')) == u' x ') + assert (t.strip_seqs(t.bold_red) == u'') + assert (t.strip_seqs(t.bold_red(u' x ')) == u' x ') + + if t.underline: + assert (t.length(t.underline) == 0) + assert (t.length(t.underline(u'x')) == 1) + assert (t.length(t.underline_red) == 0) + assert (t.length(t.underline_red(u'x')) == 1) + assert (t.strip(t.underline) == u'') + assert (t.strip(t.underline(u' x ')) == u'x') + assert (t.strip(t.underline_red) == u'') + assert (t.strip(t.underline_red(u' x ')) == u'x') + assert (t.rstrip(t.underline_red(u' x ')) == u' x') + assert (t.lstrip(t.underline_red(u' x ')) == u'x ') + assert (t.strip_seqs(t.underline) == u'') + assert (t.strip_seqs(t.underline(u' x ')) == u' x ') + assert (t.strip_seqs(t.underline_red) == u'') + assert (t.strip_seqs(t.underline_red(u' x ')) == u' x ') + + if t.reverse: + assert (t.length(t.reverse) == 0) + assert (t.length(t.reverse(u'x')) == 1) + assert (t.length(t.reverse_red) == 0) + assert (t.length(t.reverse_red(u'x')) == 1) + assert (t.strip(t.reverse) == u'') + assert (t.strip(t.reverse(u' x ')) == u'x') + assert (t.strip(t.reverse_red) == u'') + assert (t.strip(t.reverse_red(u' x ')) == u'x') + assert (t.rstrip(t.reverse_red(u' x ')) == u' x') + assert (t.lstrip(t.reverse_red(u' x ')) == u'x ') + assert (t.strip_seqs(t.reverse) == u'') + assert (t.strip_seqs(t.reverse(u' x ')) == u' x ') + assert (t.strip_seqs(t.reverse_red) == u'') + assert (t.strip_seqs(t.reverse_red(u' x ')) == u' x ') + + if t.blink: + assert (t.length(t.blink) == 0) + assert (t.length(t.blink(u'x')) == 1) + assert (t.length(t.blink_red) == 0) + assert (t.length(t.blink_red(u'x')) == 1) + assert (t.strip(t.blink) == u'') + assert (t.strip(t.blink(u' x ')) == u'x') + assert (t.strip(t.blink(u'z x q'), u'zq') == u' x ') + assert (t.strip(t.blink_red) == u'') + assert (t.strip(t.blink_red(u' x ')) == u'x') + assert (t.strip_seqs(t.blink) == u'') + assert (t.strip_seqs(t.blink(u' x ')) == u' x ') + assert (t.strip_seqs(t.blink_red) == u'') + assert (t.strip_seqs(t.blink_red(u' x ')) == u' x ') + + if t.home: + assert (t.length(t.home) == 0) + assert (t.strip(t.home) == u'') + if t.clear_eol: + assert (t.length(t.clear_eol) == 0) + assert (t.strip(t.clear_eol) == u'') + if t.enter_fullscreen: + assert (t.length(t.enter_fullscreen) == 0) + assert (t.strip(t.enter_fullscreen) == u'') + if t.exit_fullscreen: + assert (t.length(t.exit_fullscreen) == 0) + assert (t.strip(t.exit_fullscreen) == u'') + + # horizontally, we decide move_down and move_up are 0, + assert (t.length(t.move_down) == 0) + assert (t.length(t.move_down(2)) == 0) + assert (t.length(t.move_up) == 0) + assert (t.length(t.move_up(2)) == 0) + + # other things aren't so simple, somewhat edge cases, + # moving backwards and forwards horizontally must be + # accounted for as a "length", as + # will result in a printed column length of 12 (even + # though columns 2-11 are non-destructive space + assert (t.length(u'x\b') == 0) + assert (t.strip(u'x\b') == u'') + + # XXX why are some terminals width of 9 here ?? + assert (t.length(u'\t') in (8, 9)) + assert (t.strip(u'\t') == u'') + assert (t.length(u'_' + t.move_left) == 0) + + if t.cub: + assert (t.length((u'_' * 10) + t.cub(10)) == 0) + + assert (t.length(t.move_right) == 1) + + if t.cuf: + assert (t.length(t.cuf(10)) == 10) + + # vertical spacing is unaccounted as a 'length' + assert (t.length(t.move_up) == 0) + assert (t.length(t.cuu(10)) == 0) + assert (t.length(t.move_down) == 0) + assert (t.length(t.cud(10)) == 0) + + # this is how manpages perform underlining, this is done + # with the 'overstrike' capability of teletypes, and aparently + # less(1), '123' -> '1\b_2\b_3\b_' + text_wseqs = u''.join(itertools.chain( + *zip(plain_text, itertools.cycle(['\b_'])))) + assert (t.length(text_wseqs) == len(plain_text)) + + child(all_terms) + + +def test_env_winsize(): + """Test height and width is appropriately queried in a pty.""" + @as_subprocess + def child(): + # set the pty's virtual window size + os.environ['COLUMNS'] = '99' + os.environ['LINES'] = '11' + t = TestTerminal(stream=StringIO()) + save_init = t._init_descriptor + save_stdout = sys.__stdout__ + try: + t._init_descriptor = None + sys.__stdout__ = None + winsize = t._height_and_width() + width = t.width + height = t.height + finally: + t._init_descriptor = save_init + sys.__stdout__ = save_stdout + assert winsize.ws_col == width == 99 + assert winsize.ws_row == height == 11 + + child() + + +@pytest.mark.skipif(platform.python_implementation() == 'PyPy', + reason='PyPy fails TIOCSWINSZ') +def test_winsize(many_lines, many_columns): + """Test height and width is appropriately queried in a pty.""" + @as_subprocess + def child(lines=25, cols=80): + # set the pty's virtual window size + val = struct.pack('HHHH', lines, cols, 0, 0) + fcntl.ioctl(sys.__stdout__.fileno(), termios.TIOCSWINSZ, val) + t = TestTerminal() + winsize = t._height_and_width() + assert t.width == cols + assert t.height == lines + assert winsize.ws_col == cols + assert winsize.ws_row == lines + + child(lines=many_lines, cols=many_columns) + + +@pytest.mark.skipif(platform.python_implementation() == 'PyPy', + reason='PyPy fails TIOCSWINSZ') +def test_Sequence_alignment(all_terms): + """Tests methods related to Sequence class, namely ljust, rjust, center.""" + @as_subprocess + def child(kind, lines=25, cols=80): + # set the pty's virtual window size + val = struct.pack('HHHH', lines, cols, 0, 0) + fcntl.ioctl(sys.__stdout__.fileno(), termios.TIOCSWINSZ, val) + t = TestTerminal(kind=kind) + + pony_msg = 'pony express, all aboard, choo, choo!' + pony_len = len(pony_msg) + pony_colored = u''.join( + ['%s%s' % (t.color(n % 7), ch,) + for n, ch in enumerate(pony_msg)]) + pony_colored += t.normal + ladjusted = t.ljust(pony_colored) + radjusted = t.rjust(pony_colored) + centered = t.center(pony_colored) + assert (t.length(pony_colored) == pony_len) + assert (t.length(centered.strip()) == pony_len) + assert (t.length(centered) == len(pony_msg.center(t.width))) + assert (t.length(ladjusted.strip()) == pony_len) + assert (t.length(ladjusted) == len(pony_msg.ljust(t.width))) + assert (t.length(radjusted.strip()) == pony_len) + assert (t.length(radjusted) == len(pony_msg.rjust(t.width))) + + child(kind=all_terms) + + +def test_sequence_is_movement_false(all_terms): + """Test parser about sequences that do not move the cursor.""" + @as_subprocess + def child_mnemonics_wontmove(kind): + from blessings.sequences import measure_length + t = TestTerminal(kind=kind) + assert (0 == measure_length(u'', t)) + # not even a mbs + assert (0 == measure_length(u'xyzzy', t)) + # negative numbers, though printable as %d, do not result + # in movement; just garbage. Also not a valid sequence. + assert (0 == measure_length(t.cuf(-333), t)) + assert (len(t.clear_eol) == measure_length(t.clear_eol, t)) + # various erases don't *move* + assert (len(t.clear_bol) == measure_length(t.clear_bol, t)) + assert (len(t.clear_eos) == measure_length(t.clear_eos, t)) + assert (len(t.bold) == measure_length(t.bold, t)) + # various paints don't move + assert (len(t.red) == measure_length(t.red, t)) + assert (len(t.civis) == measure_length(t.civis, t)) + if t.cvvis: + assert (len(t.cvvis) == measure_length(t.cvvis, t)) + assert (len(t.underline) == measure_length(t.underline, t)) + assert (len(t.reverse) == measure_length(t.reverse, t)) + for _num in range(t.number_of_colors): + assert (len(t.color(_num)) == measure_length(t.color(_num), t)) + assert (len(t.normal) == measure_length(t.normal, t)) + assert (len(t.normal_cursor) == measure_length(t.normal_cursor, t)) + assert (len(t.hide_cursor) == measure_length(t.hide_cursor, t)) + assert (len(t.save) == measure_length(t.save, t)) + assert (len(t.italic) == measure_length(t.italic, t)) + assert (len(t.standout) == measure_length(t.standout, t) + ), (t.standout, t._wont_move) + + child_mnemonics_wontmove(all_terms) + + +def test_sequence_is_movement_true(all_terms): + """Test parsers about sequences that move the cursor.""" + @as_subprocess + def child_mnemonics_willmove(kind): + from blessings.sequences import measure_length + t = TestTerminal(kind=kind) + # movements + assert (len(t.move(98, 76)) == + measure_length(t.move(98, 76), t)) + assert (len(t.move(54)) == + measure_length(t.move(54), t)) + assert not t.cud1 or (len(t.cud1) == + measure_length(t.cud1, t)) + assert not t.cub1 or (len(t.cub1) == + measure_length(t.cub1, t)) + assert not t.cuf1 or (len(t.cuf1) == + measure_length(t.cuf1, t)) + assert not t.cuu1 or (len(t.cuu1) == + measure_length(t.cuu1, t)) + assert not t.cub or (len(t.cub(333)) == + measure_length(t.cub(333), t)) + assert not t.cuf or (len(t.cuf(333)) == + measure_length(t.cuf(333), t)) + assert not t.home or (len(t.home) == + measure_length(t.home, t)) + assert not t.restore or (len(t.restore) == + measure_length(t.restore, t)) + assert not t.clear or (len(t.clear) == + measure_length(t.clear, t)) + + child_mnemonics_willmove(all_terms) diff --git a/blessings/tests/test_sequences.py b/blessings/tests/test_sequences.py new file mode 100644 index 0000000..f42a52c --- /dev/null +++ b/blessings/tests/test_sequences.py @@ -0,0 +1,549 @@ +# -*- coding: utf-8 -*- +"""Tests for Terminal() sequences and sequence-awareness.""" +# std imports +try: + from StringIO import StringIO +except ImportError: + from io import StringIO +import platform +import random +import sys +import os + +# local +from .accessories import ( + unsupported_sequence_terminals, + all_terms, + as_subprocess, + TestTerminal, + unicode_parm, + many_columns, + unicode_cap, +) + +# 3rd-party +import pytest +import mock + + +def test_capability(): + """Check that capability lookup works.""" + @as_subprocess + def child(): + # Also test that Terminal grabs a reasonable default stream. This test + # assumes it will be run from a tty. + t = TestTerminal() + sc = unicode_cap('sc') + assert t.save == sc + assert t.save == sc # Make sure caching doesn't screw it up. + + child() + + +def test_capability_without_tty(): + """Assert capability templates are '' when stream is not a tty.""" + @as_subprocess + def child(): + t = TestTerminal(stream=StringIO()) + assert t.save == u'' + assert t.red == u'' + + child() + + +def test_capability_with_forced_tty(): + """force styling should return sequences even for non-ttys.""" + @as_subprocess + def child(): + t = TestTerminal(stream=StringIO(), force_styling=True) + assert t.save == unicode_cap('sc') + + child() + + +def test_parametrization(): + """Test parameterizing a capability.""" + @as_subprocess + def child(): + assert TestTerminal().cup(3, 4) == unicode_parm('cup', 3, 4) + + child() + + +def test_height_and_width(): + """Assert that ``height_and_width()`` returns full integers.""" + @as_subprocess + def child(): + t = TestTerminal() # kind shouldn't matter. + assert isinstance(t.height, int) + assert isinstance(t.width, int) + + child() + + +def test_stream_attr(): + """Make sure Terminal ``stream`` is stdout by default.""" + @as_subprocess + def child(): + assert TestTerminal().stream == sys.__stdout__ + + child() + + +@pytest.mark.skipif(os.environ.get('TRAVIS', None) is not None, + reason="travis-ci does not have binary-packed terminals.") +def test_emit_warnings_about_binpacked(): + """Test known binary-packed terminals (kermit, avatar) emit a warning.""" + from blessings.sequences import _BINTERM_UNSUPPORTED_MSG + from blessings._binterms import binary_terminals + + @as_subprocess + def child(kind): + import warnings + warnings.filterwarnings("error", category=RuntimeWarning) + warnings.filterwarnings("error", category=UserWarning) + + try: + TestTerminal(kind=kind, force_styling=True) + except UserWarning: + err = sys.exc_info()[1] + assert (err.args[0] == _BINTERM_UNSUPPORTED_MSG.format(kind) or + err.args[0].startswith('Unknown parameter in ') or + err.args[0].startswith('Failed to setupterm(') + ), err + else: + assert 'warnings should have been emitted.' + warnings.resetwarnings() + + # any binary terminal should do. + child(binary_terminals[random.randrange(len(binary_terminals))]) + + +def test_unit_binpacked_unittest(): + """Unit Test known binary-packed terminals emit a warning (travis-safe).""" + import warnings + from blessings._binterms import binary_terminals + from blessings.sequences import (_BINTERM_UNSUPPORTED_MSG, + init_sequence_patterns) + warnings.filterwarnings("error", category=UserWarning) + term = mock.Mock() + term.kind = binary_terminals[random.randrange(len(binary_terminals))] + + try: + init_sequence_patterns(term) + except UserWarning: + err = sys.exc_info()[1] + assert err.args[0] == _BINTERM_UNSUPPORTED_MSG.format(term.kind) + else: + assert False, 'Previous stmt should have raised exception.' + warnings.resetwarnings() + + +def test_merge_sequences(): + """Test sequences are filtered and ordered longest-first.""" + from blessings.sequences import _merge_sequences + input_list = [u'a', u'aa', u'aaa', u''] + output_expected = [u'aaa', u'aa', u'a'] + assert (_merge_sequences(input_list) == output_expected) + + +def test_location_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) + with t.location(3, 4): + t.stream.write(u'hi') + expected_output = u''.join( + (unicode_cap('sc'), + unicode_parm('cup', 4, 3), + u'hi', unicode_cap('rc'))) + assert (t.stream.getvalue() == expected_output) + + child_with_styling(all_terms) + + +def test_location_without_styling(): + """Make sure ``location()`` silently passes without styling.""" + @as_subprocess + def child_without_styling(): + """No side effect for location as a context manager without styling.""" + t = TestTerminal(stream=StringIO(), force_styling=None) + + with t.location(3, 4): + t.stream.write(u'hi') + + assert t.stream.getvalue() == u'hi' + + child_without_styling() + + +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) + with t.location(x=5): + pass + expected_output = u''.join( + (unicode_cap('sc'), + unicode_parm('hpa', 5), + unicode_cap('rc'))) + assert (t.stream.getvalue() == expected_output), ( + repr(t.stream.getvalue()), repr(expected_output)) + + # skip 'screen', hpa is proxied (see later tests) + if all_terms != 'screen': + child(all_terms) + + +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) + with t.location(y=5): + pass + expected_output = u''.join( + (unicode_cap('sc'), + unicode_parm('vpa', 5), + unicode_cap('rc'))) + assert (t.stream.getvalue() == expected_output) + + # skip 'screen', vpa is proxied (see later tests) + if all_terms != 'screen': + child(all_terms) + + +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) + COL = 5 + with t.location(x=COL): + pass + expected_output = u''.join( + (unicode_cap('sc'), + u'\x1b[{0}G'.format(COL + 1), + unicode_cap('rc'))) + assert (t.stream.getvalue() == expected_output) + assert (t.move_x(COL) == u'\x1b[{0}G'.format(COL + 1)) + + child('screen') + child('screen-256color') + child('ansi') + + +def test_inject_move_y(): + """Test injection of vpa attribute for screen/ansi (issue #55).""" + @as_subprocess + def child(kind): + t = TestTerminal(kind=kind, stream=StringIO(), force_styling=True) + ROW = 5 + with t.location(y=ROW): + pass + expected_output = u''.join( + (unicode_cap('sc'), + u'\x1b[{0}d'.format(ROW + 1), + unicode_cap('rc'))) + assert (t.stream.getvalue() == expected_output) + assert (t.move_y(ROW) == u'\x1b[{0}d'.format(ROW + 1)) + + child('screen') + child('screen-256color') + child('ansi') + + +def test_inject_civis_and_cnorm_for_ansi(): + """Test injection of cvis attribute for ansi.""" + @as_subprocess + def child(kind): + t = TestTerminal(kind=kind, stream=StringIO(), force_styling=True) + with t.hidden_cursor(): + pass + expected_output = u''.join( + (unicode_cap('sc'), + u'\x1b[?25l\x1b[?25h', + unicode_cap('rc'))) + assert (t.stream.getvalue() == expected_output) + + child('ansi') + + +def test_zero_location(all_terms): + """Make sure ``location()`` pays attention to 0-valued args.""" + @as_subprocess + def child(kind): + t = TestTerminal(kind=kind, stream=StringIO(), force_styling=True) + with t.location(0, 0): + pass + expected_output = u''.join( + (unicode_cap('sc'), + unicode_parm('cup', 0, 0), + unicode_cap('rc'))) + assert (t.stream.getvalue() == expected_output) + + child(all_terms) + + +def test_mnemonic_colors(all_terms): + """Make sure color shortcuts work.""" + @as_subprocess + def child(kind): + def color(t, num): + return t.number_of_colors and unicode_parm('setaf', num) or '' + + def on_color(t, num): + return t.number_of_colors and unicode_parm('setab', num) or '' + + # Avoid testing red, blue, yellow, and cyan, since they might someday + # change depending on terminal type. + t = TestTerminal(kind=kind) + assert (t.white == color(t, 7)) + assert (t.green == color(t, 2)) # Make sure it's different than white. + assert (t.on_black == on_color(t, 0)) + assert (t.on_green == on_color(t, 2)) + assert (t.bright_black == color(t, 8)) + assert (t.bright_green == color(t, 10)) + assert (t.on_bright_black == on_color(t, 8)) + assert (t.on_bright_green == on_color(t, 10)) + + child(all_terms) + + +def test_callable_numeric_colors(all_terms): + """``color(n)`` should return a formatting wrapper.""" + @as_subprocess + def child(kind): + t = TestTerminal(kind=kind) + if t.magenta: + assert t.color(5)('smoo') == t.magenta + 'smoo' + t.normal + else: + assert t.color(5)('smoo') == 'smoo' + + if t.on_magenta: + assert t.on_color(5)('smoo') == t.on_magenta + 'smoo' + t.normal + else: + assert t.color(5)(u'smoo') == 'smoo' + + if t.color(4): + assert t.color(4)(u'smoo') == t.color(4) + u'smoo' + t.normal + else: + assert t.color(4)(u'smoo') == 'smoo' + + if t.on_green: + assert t.on_color(2)('smoo') == t.on_green + u'smoo' + t.normal + else: + assert t.on_color(2)('smoo') == 'smoo' + + if t.on_color(6): + assert t.on_color(6)('smoo') == t.on_color(6) + u'smoo' + t.normal + else: + assert t.on_color(6)('smoo') == 'smoo' + + child(all_terms) + + +def test_null_callable_numeric_colors(all_terms): + """``color(n)`` should be a no-op on null terminals.""" + @as_subprocess + def child(kind): + t = TestTerminal(stream=StringIO(), kind=kind) + assert (t.color(5)('smoo') == 'smoo') + assert (t.on_color(6)('smoo') == 'smoo') + + child(all_terms) + + +def test_naked_color_cap(all_terms): + """``term.color`` should return a stringlike capability.""" + @as_subprocess + def child(kind): + t = TestTerminal(kind=kind) + assert (t.color + '' == t.setaf + '') + + child(all_terms) + + +def test_formatting_functions(all_terms): + """Test simple and compound formatting wrappers.""" + @as_subprocess + def child(kind): + t = TestTerminal(kind=kind) + # test simple sugar, + if t.bold: + expected_output = u''.join((t.bold, u'hi', t.normal)) + else: + expected_output = u'hi' + assert t.bold(u'hi') == expected_output + # Plain strs for Python 2.x + if t.green: + expected_output = u''.join((t.green, 'hi', t.normal)) + else: + expected_output = u'hi' + assert t.green('hi') == expected_output + # Test unicode + if t.underline: + expected_output = u''.join((t.underline, u'boö', t.normal)) + else: + expected_output = u'boö' + assert (t.underline(u'boö') == expected_output) + + if t.subscript: + expected_output = u''.join((t.subscript, u'[1]', t.normal)) + else: + expected_output = u'[1]' + + assert (t.subscript(u'[1]') == expected_output) + + child(all_terms) + + +def test_compound_formatting(all_terms): + """Test simple and compound formatting wrappers.""" + @as_subprocess + def child(kind): + t = TestTerminal(kind=kind) + if any((t.bold, t.green)): + expected_output = u''.join((t.bold, t.green, u'boö', t.normal)) + else: + expected_output = u'boö' + assert t.bold_green(u'boö') == expected_output + + if any((t.on_bright_red, t.bold, t.bright_green, t.underline)): + expected_output = u''.join( + (t.on_bright_red, t.bold, t.bright_green, t.underline, u'meh', + t.normal)) + else: + expected_output = u'meh' + assert (t.on_bright_red_bold_bright_green_underline('meh') + == expected_output) + + child(all_terms) + + +def test_formatting_functions_without_tty(all_terms): + """Test crazy-ass formatting wrappers when there's no tty.""" + @as_subprocess + def child(kind): + t = TestTerminal(kind=kind, stream=StringIO(), force_styling=False) + assert (t.bold(u'hi') == u'hi') + assert (t.green('hi') == u'hi') + # Test non-ASCII chars, no longer really necessary: + assert (t.bold_green(u'boö') == u'boö') + assert (t.bold_underline_green_on_red('loo') == u'loo') + assert (t.on_bright_red_bold_bright_green_underline('meh') == u'meh') + + child(all_terms) + + +def test_nice_formatting_errors(all_terms): + """Make sure you get nice hints if you misspell a formatting wrapper.""" + @as_subprocess + def child(kind): + t = TestTerminal(kind=kind) + try: + t.bold_misspelled('hey') + assert not t.is_a_tty or False, 'Should have thrown exception' + except TypeError: + e = sys.exc_info()[1] + assert 'probably misspelled' in e.args[0] + try: + t.bold_misspelled(u'hey') # unicode + assert not t.is_a_tty or False, 'Should have thrown exception' + except TypeError: + e = sys.exc_info()[1] + assert 'probably misspelled' in e.args[0] + + try: + t.bold_misspelled(None) # an arbitrary non-string + assert not t.is_a_tty or False, 'Should have thrown exception' + except TypeError: + e = sys.exc_info()[1] + assert 'probably misspelled' not in e.args[0] + + if platform.python_implementation() != 'PyPy': + # PyPy fails to toss an exception, Why?! + try: + t.bold_misspelled('a', 'b') # >1 string arg + assert not t.is_a_tty or False, 'Should have thrown exception' + except TypeError: + e = sys.exc_info()[1] + assert 'probably misspelled' in e.args[0], e.args + + child(all_terms) + + +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) + assert (t.clear == '') + assert (t.move(1 == 2) == '') + assert (t.move_x(1) == '') + assert (t.bold() == '') + assert (t.bold('', 'x', 'huh?') == '') + assert (t.bold('', 9876) == '') + assert (t.uhh(9876) == '') + assert (t.clear('x') == 'x') + + child(all_terms) + + +def test_bnc_parameter_emits_warning(): + """A fake capability without target digits emits a warning.""" + import warnings + from blessings.sequences import _build_numeric_capability + + # given, + warnings.filterwarnings("error", category=UserWarning) + term = mock.Mock() + fake_cap = lambda *args: u'NO-DIGIT' + term.fake_cap = fake_cap + + # excersize, + try: + _build_numeric_capability(term, 'fake_cap', base_num=1984) + except UserWarning: + err = sys.exc_info()[1] + assert err.args[0].startswith('Unknown parameter in ') + else: + assert False, 'Previous stmt should have raised exception.' + warnings.resetwarnings() + + +def test_bna_parameter_emits_warning(): + """A fake capability without any digits emits a warning.""" + import warnings + from blessings.sequences import _build_any_numeric_capability + + # given, + warnings.filterwarnings("error", category=UserWarning) + term = mock.Mock() + fake_cap = lambda *args: 'NO-DIGIT' + term.fake_cap = fake_cap + + # excersize, + try: + _build_any_numeric_capability(term, 'fake_cap') + except UserWarning: + err = sys.exc_info()[1] + assert err.args[0].startswith('Missing numerics in ') + else: + assert False, 'Previous stmt should have raised exception.' + warnings.resetwarnings() + + +def test_padd(): + """ Test terminal.padd(seq). """ + @as_subprocess + def child(): + from blessings.sequences import Sequence + from blessings import Terminal + term = Terminal('xterm-256color') + assert Sequence('xyz\b', term).padd() == u'xy' + assert Sequence('xxxx\x1b[3Dzz', term).padd() == u'xzz' + + child() diff --git a/blessings/tests/test_wrap.py b/blessings/tests/test_wrap.py new file mode 100644 index 0000000..e5f7a15 --- /dev/null +++ b/blessings/tests/test_wrap.py @@ -0,0 +1,105 @@ +import platform +import textwrap +import termios +import struct +import fcntl +import sys + +from .accessories import ( + as_subprocess, + TestTerminal, + many_columns, + all_terms, +) + +import pytest + + +def test_SequenceWrapper_invalid_width(): + """Test exception thrown from invalid width""" + WIDTH = -3 + + @as_subprocess + def child(): + term = TestTerminal() + try: + my_wrapped = term.wrap(u'------- -------------', WIDTH) + except ValueError as err: + assert err.args[0] == ( + "invalid width %r(%s) (must be integer > 0)" % ( + WIDTH, type(WIDTH))) + else: + assert False, 'Previous stmt should have raised exception.' + del my_wrapped # assigned but never used + + child() + + +@pytest.mark.parametrize("kwargs", [ + dict(break_long_words=False, + drop_whitespace=False, + subsequent_indent=''), + dict(break_long_words=False, + drop_whitespace=True, + subsequent_indent=''), + dict(break_long_words=False, + drop_whitespace=False, + subsequent_indent=' '), + dict(break_long_words=False, + drop_whitespace=True, + subsequent_indent=' '), + dict(break_long_words=True, + drop_whitespace=False, + subsequent_indent=''), + dict(break_long_words=True, + drop_whitespace=True, + subsequent_indent=''), + dict(break_long_words=True, + drop_whitespace=False, + subsequent_indent=' '), + dict(break_long_words=True, + drop_whitespace=True, + subsequent_indent=' '), +]) +def test_SequenceWrapper(all_terms, many_columns, kwargs): + """Test that text wrapping matches internal extra options.""" + @as_subprocess + def child(term, width, kwargs): + # build a test paragraph, along with a very colorful version + term = TestTerminal() + pgraph = u' Z! a bc defghij klmnopqrstuvw<<>>xyz012345678900 ' + attributes = ('bright_red', 'on_bright_blue', 'underline', 'reverse', + 'red_reverse', 'red_on_white', 'superscript', + 'subscript', 'on_bright_white') + term.bright_red('x') + term.on_bright_blue('x') + term.underline('x') + term.reverse('x') + term.red_reverse('x') + term.red_on_white('x') + term.superscript('x') + term.subscript('x') + term.on_bright_white('x') + + pgraph_colored = u''.join([ + getattr(term, (attributes[idx % len(attributes)]))(char) + if char != u' ' else u' ' + for idx, char in enumerate(pgraph)]) + + internal_wrapped = textwrap.wrap(pgraph, width=width, **kwargs) + my_wrapped = term.wrap(pgraph, width=width, **kwargs) + my_wrapped_colored = term.wrap(pgraph_colored, width=width, **kwargs) + + # ensure we textwrap ascii the same as python + assert internal_wrapped == my_wrapped + + # ensure content matches for each line, when the sequences are + # stripped back off of each line + for line_no, (left, right) in enumerate( + zip(internal_wrapped, my_wrapped_colored)): + assert left == term.strip_seqs(right) + + # ensure our colored textwrap is the same paragraph length + assert (len(internal_wrapped) == len(my_wrapped_colored)) + + child(all_terms, many_columns, kwargs) diff --git a/blessings/tests/wall.ans b/blessings/tests/wall.ans new file mode 100644 index 0000000..081b4d2 --- /dev/null +++ b/blessings/tests/wall.ans @@ -0,0 +1,7 @@ +▄▓▄ + ░░ █▀▀█▀██▀████████▀█▀▀█ █▀▀█▀██▀▀█▀█xz(imp) + ▄▄▄ ▓█████ ▄▄ █▀▀█░ ▀▀▀▀▀ █████▓ ▓█████ ░▓ █▀▀█░ ▓█████ ░▓ █▀▀█░ ░▄ ░░ + ▐▄░▄▀ ▀▀▀▀▀▀ █▓ ▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀ ██ ▀▀▀▀▀ ▀▀▀▀▀▀ ██ ▀▀▀▀▀ █▄▌▌▄▄ + █▓██ ░▓████ ▐ ████░ ░████ ▄█ ████▓░ ░▓████ ██ ████░ ░▓████ ██ ████░ █▐▄▄█░ + ██▓▀ ░▓████ █ ████░ ░████ ▐ ████▓░ ░▓████ ▄█▌████░ ░▓████ ▄█▌████░ ▌█████ + ▀ ░▓████▄█▄▄███ ░ ░ ███▄▄▄▄████▓░ ░▓████▄▄▄▄███ ░ ░▓████▄▄▄▄███ ░ ▀▀▀▓ -- cgit v1.2.1