From 0224922110ade5af0fb01136bac17482c8429796 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 6 Mar 2021 11:21:38 -0500 Subject: [PATCH 1/6] Bugfix for term.split_seqs('term.right(3)') In version 1.18.0 and earlier, >> term.split_seqs(term.move_right(333) + 'xyz', maxsplit=1) ['\x1b[333C', '333', 'xyz'] This is a bug, it duplicates the matched parameter, this is now corrected: ['\x1b[3C', 'xyz'] Previously, we documented "same arguments as "re.split", so we must also implement maxsplit and flags. Also, - fix flake8 linting of tests by moving fixtures to conftest.py, fixes "unused" or "re-definition from import" errors. - version stamp blessed/__init__.py like a codegen step i guess - remove run_codecov.py, its been fixed upstream https://github.com/codecov/codecov-python/issues/158#issuecomment-737017579 --- blessed/__init__.py | 2 +- blessed/terminal.py | 22 +++++++++++++-- blessed/terminal.pyi | 2 +- docs/history.rst | 2 ++ run_codecov.py | 47 ------------------------------- tests/accessories.py | 31 +-------------------- tests/conftest.py | 32 +++++++++++++++++++++ tests/test_core.py | 2 +- tests/test_keyboard.py | 2 +- tests/test_length_sequence.py | 4 +-- tests/test_sequences.py | 52 +++++++++++++++++++++++++++++++++-- tests/test_wrap.py | 2 +- tox.ini | 10 ++++--- version.json | 2 +- version.py | 24 ++++++++++++++++ 15 files changed, 142 insertions(+), 94 deletions(-) delete mode 100644 run_codecov.py create mode 100644 tests/conftest.py create mode 100644 version.py 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/terminal.py b/blessed/terminal.py index a4bde9c5..d5fbaed2 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -1106,20 +1106,36 @@ def strip_seqs(self, text): """ return Sequence(text, self).strip_seqs() - def split_seqs(self, text, **kwds): + def split_seqs(self, text, maxsplit=0, flags=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`). + :arg int flags: regex flags, combined integer flag attributes documented + beginning with flag attribute :attr:`re.A` (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, flags)): + 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..66048671 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, flags: 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..99c5a96a 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 @@ -40,15 +39,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: @@ -164,8 +154,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 +243,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..70dcc387 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,32 @@ +# std imports +import os + +# 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_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 b94fd544..6a25053f 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 6bb361bf..7e817701 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,6 @@ [tox] envlist = about + stamp autopep8 docformatter isort @@ -152,6 +153,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 = @@ -222,10 +226,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() From 0965de5854e95ae017913ca18a85c5825ca3cad8 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 6 Mar 2021 12:17:47 -0500 Subject: [PATCH 2/6] rmove 3.9 from travis-ci "allowed failures" --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index ac386ee6..4272b2e6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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: From 26d286580e8879d42b8769aacdd9ebf1f0001c4c Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 6 Mar 2021 12:46:03 -0500 Subject: [PATCH 3/6] move remaining missing text fixtures code --- blessed/_capabilities.py | 1 - tests/accessories.py | 17 ----------------- tests/conftest.py | 19 +++++++++++++++++++ 3 files changed, 19 insertions(+), 18 deletions(-) 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/tests/accessories.py b/tests/accessories.py index 99c5a96a..db0269d6 100644 --- a/tests/accessories.py +++ b/tests/accessories.py @@ -40,23 +40,6 @@ SEND_SEMAPHORE = SEMAPHORE = b'SEMAPHORE\n' RECV_SEMAPHORE = b'SEMAPHORE\r\n' -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): try: import coverage diff --git a/tests/conftest.py b/tests/conftest.py index 70dcc387..4fe3602d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,7 @@ # std imports import os +import platform +import subprocess # 3rd party import pytest @@ -9,6 +11,23 @@ # 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, ] From 705928bd5768590dbcb8e1e1d324660af83f5a3d Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 6 Mar 2021 12:47:17 -0500 Subject: [PATCH 4/6] remove flags, thanks @avylove --- blessed/terminal.py | 7 ++----- blessed/terminal.pyi | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/blessed/terminal.py b/blessed/terminal.py index dee1a286..1f8425fb 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -1100,7 +1100,7 @@ def strip_seqs(self, text): """ return Sequence(text, self).strip_seqs() - def split_seqs(self, text, maxsplit=0, flags=0): + def split_seqs(self, text, maxsplit=0): r""" Return ``text`` split by individual character elements and sequences. @@ -1108,9 +1108,6 @@ def split_seqs(self, text, maxsplit=0, flags=0): :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`). - :arg int flags: regex flags, combined integer flag attributes documented - beginning with flag attribute :attr:`re.A` (same meaning is argument - for :func:`re.split`). :rtype: list[str] :returns: List of sequences and individual characters @@ -1122,7 +1119,7 @@ def split_seqs(self, text, maxsplit=0, flags=0): """ pattern = self._caps_unnamed_any result = [] - for idx, match in enumerate(re.finditer(pattern, text, flags)): + for idx, match in enumerate(re.finditer(pattern, text)): result.append(match.group()) if maxsplit and idx == maxsplit: remaining = text[match.end():] diff --git a/blessed/terminal.pyi b/blessed/terminal.pyi index 66048671..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, maxsplit: int, flags: int) -> List[str]: ... + def split_seqs(self, text: str, maxsplit: int) -> List[str]: ... def wrap( self, text: str, width: Optional[int] = ..., **kwargs: Any ) -> List[str]: ... From 04002c10a6987f1e8588dcd04287b301a2bb9e8b Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 6 Mar 2021 12:47:49 -0500 Subject: [PATCH 5/6] 3.9 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 4272b2e6..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 From 19171d55c3f97947ec063c66bf996608d0a5c88d Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 6 Mar 2021 16:18:57 -0500 Subject: [PATCH 6/6] lint --- tests/accessories.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/accessories.py b/tests/accessories.py index db0269d6..f8928ee9 100644 --- a/tests/accessories.py +++ b/tests/accessories.py @@ -10,11 +10,9 @@ import functools import traceback import contextlib -import subprocess # 3rd party import six -import pytest # local from blessed import Terminal @@ -40,6 +38,7 @@ SEND_SEMAPHORE = SEMAPHORE = b'SEMAPHORE\n' RECV_SEMAPHORE = b'SEMAPHORE\r\n' + def init_subproc_coverage(run_note): try: import coverage