diff --git a/.travis.yml b/.travis.yml index ac386ee6..92e90f54 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ matrix: env: TOXENV=py37,codecov TEST_QUICK=1 COVERAGE_ID=travis-ci - python: 3.8 env: TOXENV=py38,codecov COVERAGE_ID=travis-ci TEST_RAW=yes - - python: 3.9-dev + - python: 3.9 env: TOXENV=py39,codecov TEST_QUICK=1 COVERAGE_ID=travis-ci TOXPYTHON=3.9 - python: 3.10-dev env: TOXENV=py310,codecov TEST_QUICK=1 COVERAGE_ID=travis-ci TOXPYTHON=3.10 @@ -38,7 +38,6 @@ matrix: env: PATH=/c/Python38:/c/Python38/Scripts:$PATH TOXENV=py38,codecov COVERAGE_ID=travis-ci TEST_KEYBOARD=no allow_failures: - - python: 3.9-dev - python: 3.10-dev install: diff --git a/blessed/__init__.py b/blessed/__init__.py index fb72bb76..cb372d7a 100644 --- a/blessed/__init__.py +++ b/blessed/__init__.py @@ -19,4 +19,4 @@ 'support due to http://bugs.python.org/issue10570.') __all__ = ('Terminal',) -__version__ = '1.18.0' +__version__ = "1.18.1" diff --git a/blessed/_capabilities.py b/blessed/_capabilities.py index 0b2b92fd..c4df54bc 100644 --- a/blessed/_capabilities.py +++ b/blessed/_capabilities.py @@ -3,7 +3,6 @@ import re from collections import OrderedDict - __all__ = ( 'CAPABILITY_DATABASE', 'CAPABILITIES_RAW_MIXIN', diff --git a/blessed/terminal.py b/blessed/terminal.py index 192e89e6..1f8425fb 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -1100,20 +1100,33 @@ def strip_seqs(self, text): """ return Sequence(text, self).strip_seqs() - def split_seqs(self, text, **kwds): + def split_seqs(self, text, maxsplit=0): r""" Return ``text`` split by individual character elements and sequences. :arg str text: String containing sequences - :arg kwds: remaining keyword arguments for :func:`re.split`. + :arg int maxsplit: When maxsplit is nonzero, at most maxsplit splits + occur, and the remainder of the string is returned as the final element + of the list (same meaning is argument for :func:`re.split`). :rtype: list[str] :returns: List of sequences and individual characters >>> term.split_seqs(term.underline(u'xyz')) ['\x1b[4m', 'x', 'y', 'z', '\x1b(B', '\x1b[m'] + + >>> term.split_seqs(term.underline(u'xyz'), 1) + ['\x1b[4m', r'xyz\x1b(B\x1b[m'] """ pattern = self._caps_unnamed_any - return list(filter(None, re.split(pattern, text, **kwds))) + result = [] + for idx, match in enumerate(re.finditer(pattern, text)): + result.append(match.group()) + if maxsplit and idx == maxsplit: + remaining = text[match.end():] + if remaining: + result[-1] += remaining + break + return result def wrap(self, text, width=None, **kwargs): """ diff --git a/blessed/terminal.pyi b/blessed/terminal.pyi index 4ceb6445..d528d419 100644 --- a/blessed/terminal.pyi +++ b/blessed/terminal.pyi @@ -88,7 +88,7 @@ class Terminal: def rstrip(self, text: str, chars: Optional[str] = ...) -> str: ... def lstrip(self, text: str, chars: Optional[str] = ...) -> str: ... def strip_seqs(self, text: str) -> str: ... - def split_seqs(self, text: str, **kwds: Any) -> List[str]: ... + def split_seqs(self, text: str, maxsplit: int) -> List[str]: ... def wrap( self, text: str, width: Optional[int] = ..., **kwargs: Any ) -> List[str]: ... diff --git a/docs/history.rst b/docs/history.rst index 4018bd6e..fc4543b6 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -1,6 +1,8 @@ Version History =============== 1.18 + * bugfix: :meth:`~Terminal.split_seqs` for some sequences + like ``term.move_left(3)``, :ghissue:`197`. * introduced: type annotations, :ghissue:`192` by :ghuser:`dlax`. * bugfix: do not fail when ``sys.stdin`` is unset, :ghissue:`195` by :ghuser:`Olen` diff --git a/run_codecov.py b/run_codecov.py deleted file mode 100644 index 49e7d7ff..00000000 --- a/run_codecov.py +++ /dev/null @@ -1,47 +0,0 @@ -""" -Workaround for https://github.com/codecov/codecov-python/issues/158 -""" - -# std imports -import sys -import time - -# local -import codecov - -RETRIES = 5 -TIMEOUT = 2 - - -def main(): - """ - Run codecov up to RETRIES times - On the final attempt, let it exit normally - """ - - # Make a copy of argv and make sure --required is in it - args = sys.argv[1:] - if '--required' not in args: - args.append('--required') - - for num in range(1, RETRIES + 1): - - print('Running codecov attempt %d: ' % num) - # On the last, let codecov handle the exit - if num == RETRIES: - codecov.main() - - try: - codecov.main(*args) - except SystemExit as err: - # If there's no exit code, it was successful - if err.code: - time.sleep(TIMEOUT) - else: - sys.exit(err.code) - else: - break - - -if __name__ == '__main__': - main() diff --git a/tests/accessories.py b/tests/accessories.py index 91e864a3..f8928ee9 100644 --- a/tests/accessories.py +++ b/tests/accessories.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- """Accessories for automated py.test runner.""" -# standard imports from __future__ import print_function, with_statement # std imports @@ -11,11 +10,9 @@ import functools import traceback import contextlib -import subprocess # 3rd party import six -import pytest # local from blessed import Terminal @@ -40,31 +37,6 @@ TestTerminal = functools.partial(Terminal, kind=test_kind) # type: Callable[..., Terminal] SEND_SEMAPHORE = SEMAPHORE = b'SEMAPHORE\n' RECV_SEMAPHORE = b'SEMAPHORE\r\n' -many_lines_params = [40, 80] -# we must test a '1' column for conditional in _handle_long_word -many_columns_params = [1, 10] - -if os.environ.get('TEST_QUICK'): - many_lines_params = [80, ] - many_columns_params = [25, ] - -all_terms_params = 'xterm screen ansi vt220 rxvt cons25 linux'.split() - -if os.environ.get('TEST_FULL'): - try: - all_terms_params = [ - # use all values of the first column of data in output of 'toe -a' - _term.split(None, 1)[0] for _term in - subprocess.Popen(('toe', '-a'), - stdout=subprocess.PIPE, - close_fds=True) - .communicate()[0].splitlines()] - except OSError: - pass -elif platform.system() == 'Windows': - all_terms_params = ['vtwin10', ] -elif os.environ.get('TEST_QUICK'): - all_terms_params = 'xterm screen ansi linux'.split() def init_subproc_coverage(run_note): @@ -164,8 +136,7 @@ def __call__(self, *args, **kwargs): assert os.WEXITSTATUS(status) == 0 -def read_until_semaphore(fd, semaphore=RECV_SEMAPHORE, - encoding='utf8', timeout=10): +def read_until_semaphore(fd, semaphore=RECV_SEMAPHORE, encoding='utf8'): """ Read file descriptor ``fd`` until ``semaphore`` is found. @@ -254,21 +225,3 @@ def unicode_parm(cap, *parms): if val: return val.decode('latin1') return u'' - - -@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/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..4fe3602d --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,51 @@ +# std imports +import os +import platform +import subprocess + +# 3rd party +import pytest + +all_terms_params = 'xterm screen ansi vt220 rxvt cons25 linux'.split() +many_lines_params = [40, 80] +# we must test a '1' column for conditional in _handle_long_word +many_columns_params = [1, 10] + +if os.environ.get('TEST_FULL'): + try: + all_terms_params = [ + # use all values of the first column of data in output of 'toe -a' + _term.split(None, 1)[0] for _term in + subprocess.Popen(('toe', '-a'), + stdout=subprocess.PIPE, + close_fds=True) + .communicate()[0].splitlines()] + except OSError: + pass +elif platform.system() == 'Windows': + all_terms_params = ['vtwin10', ] +elif os.environ.get('TEST_QUICK'): + all_terms_params = 'xterm screen ansi linux'.split() + + +if os.environ.get('TEST_QUICK'): + many_lines_params = [80, ] + many_columns_params = [25, ] + + +@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/tests/test_core.py b/tests/test_core.py index 609e8f33..f4492749 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -18,7 +18,7 @@ from six.moves import reload_module # local -from .accessories import TestTerminal, all_terms, unicode_cap, as_subprocess +from .accessories import TestTerminal, unicode_cap, as_subprocess def test_export_only_Terminal(): diff --git a/tests/test_keyboard.py b/tests/test_keyboard.py index b604293a..9233c916 100644 --- a/tests/test_keyboard.py +++ b/tests/test_keyboard.py @@ -11,7 +11,7 @@ import pytest # local -from .accessories import TestTerminal, all_terms, as_subprocess +from .accessories import TestTerminal, as_subprocess if platform.system() != 'Windows': import curses diff --git a/tests/test_length_sequence.py b/tests/test_length_sequence.py index 214f51e0..d911fbc5 100644 --- a/tests/test_length_sequence.py +++ b/tests/test_length_sequence.py @@ -10,8 +10,8 @@ import six import pytest -from .accessories import ( # isort:skip - TestTerminal, as_subprocess, all_terms, many_lines, many_columns) +# local +from .accessories import TestTerminal, as_subprocess if platform.system() != 'Windows': import fcntl diff --git a/tests/test_sequences.py b/tests/test_sequences.py index 01881238..5e65ae9b 100644 --- a/tests/test_sequences.py +++ b/tests/test_sequences.py @@ -9,7 +9,7 @@ import pytest # local -from .accessories import TestTerminal, all_terms, unicode_cap, unicode_parm, as_subprocess +from .accessories import TestTerminal, unicode_cap, unicode_parm, as_subprocess @pytest.mark.skipif(platform.system() == 'Windows', reason="requires real tty") @@ -543,9 +543,57 @@ def child(kind): result = list(term.split_seqs(given_text)) assert result == expected + child(all_terms) + + +def test_split_seqs_maxsplit1(all_terms): + """Test Terminal.split_seqs with maxsplit=1.""" + @as_subprocess + def child(kind): + from blessed import Terminal + term = Terminal(kind) + if term.bold: given_text = term.bold + 'bbq' - expected = [term.bold, 'b', 'b', 'q'] + expected = [term.bold, 'bbq'] + result = list(term.split_seqs(given_text, 1)) + assert result == expected + + child(all_terms) + + +def test_split_seqs_term_right(all_terms): + """Test Terminal.split_seqs with parameterized sequence""" + @as_subprocess + def child(kind): + from blessed import Terminal + term = Terminal(kind) + + if term.move_up: + given_text = 'XY' + term.move_right + 'VK' + expected = ['X', 'Y', term.move_right, 'V', 'K'] + result = list(term.split_seqs(given_text)) + assert result == expected + + child(all_terms) + + +def test_split_seqs_maxsplit3_and_term_right(all_terms): + """Test Terminal.split_seqs with parameterized sequence.""" + @as_subprocess + def child(kind): + from blessed import Terminal + term = Terminal(kind) + + if term.move_right(32): + given_text = 'PQ' + term.move_right(32) + 'RS' + expected = ['P', 'Q', term.move_right(32), 'RS'] + result = list(term.split_seqs(given_text, 3)) + assert result == expected + + if term.move_up(45): + given_text = 'XY' + term.move_up(45) + 'VK' + expected = ['X', 'Y', term.move_up(45), 'V', 'K'] result = list(term.split_seqs(given_text)) assert result == expected diff --git a/tests/test_wrap.py b/tests/test_wrap.py index 29537596..89f2e1e3 100644 --- a/tests/test_wrap.py +++ b/tests/test_wrap.py @@ -6,7 +6,7 @@ import pytest # local -from .accessories import TestTerminal, many_columns, as_subprocess +from .accessories import TestTerminal, as_subprocess TEXTWRAP_KEYWORD_COMBINATIONS = [ dict(break_long_words=False, diff --git a/tox.ini b/tox.ini index 1deb809c..afe0e66f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,6 @@ [tox] envlist = about + stamp autopep8 docformatter isort @@ -138,6 +139,9 @@ commands = python {toxinidir}/bin/display-sighandlers.py python {toxinidir}/bin/display-terminalinfo.py python {toxinidir}/bin/display-fpathconf.py +[testenv:stamp] +commands = python {toxinidir}/version.py + [testenv:autopep8] deps = autopep8==1.4.4 commands = @@ -208,10 +212,8 @@ commands = {envbindir}/sphinx-build -v -W -d {toxinidir}/docs/_build/doctrees -b [testenv:codecov] basepython = python{env:TOXPYTHON:{env:TRAVIS_PYTHON_VERSION:3.8}} passenv = TOXENV CI TRAVIS TRAVIS_* CODECOV_* -deps = codecov>=1.4.0 -# commands = codecov -e TOXENV -# Workaround for https://github.com/codecov/codecov-python/issues/158 -commands = {envpython} run_codecov.py -e TOXENV +deps = codecov>=2.1 +commands = codecov -e TOXENV [testenv:publish_static] # Synchronize the artifacts in docs/_static/ with https://dxtz6bzwq9sxx.cloudfront.net/ diff --git a/version.json b/version.json index 617b2b36..01b85dcd 100644 --- a/version.json +++ b/version.json @@ -1 +1 @@ -{"version": "1.18.0"} +{"version": "1.18.1"} diff --git a/version.py b/version.py new file mode 100644 index 00000000..5587aa1f --- /dev/null +++ b/version.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +# std imports +import os +import re +import json + + +def main(): + # I don't know why, we maintain __version__ in blessed, because that's + # how it was done a long time ago before pip, anyway we do basic + # code generation, version.json -> __init__.py + fpath_json = os.path.join(os.path.dirname(__file__), 'version.json') + version = json.load(open(fpath_json, 'r'))['version'] + fpath_py = os.path.join(os.path.dirname(__file__), 'blessed', '__init__.py') + prev_text = open(fpath_py, 'r').read() + next_text = re.sub(r"(__version__ = )(.*)$", r'\1"{0}"'.format(version), + prev_text, flags=re.MULTILINE) + if prev_text != next_text: + print('Updating blessed.__version__ to {}'.format(version)) + open(fpath_py, 'w').write(next_text) + + +if __name__ == '__main__': + main()