From 03552d1632b6da01a63a1a541b7a98adc4c2f665 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Sun, 5 Jun 2016 23:10:19 -0400 Subject: [PATCH 001/302] add python 3.5 to testing matrix --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index efb5212..b0502fb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,7 @@ python: - "2.7" - "3.3" - "3.4" + - "3.5" - "pypy" - "pypy3" From 9ad9f3453e8e44652a1891c758d38b852b193683 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Fri, 29 Jul 2016 15:00:13 -0400 Subject: [PATCH 002/302] add codes for F1-F4 --- curtsies/curtsieskeys.py | 7 +++++++ curtsies/events.py | 7 +++++++ tests/test_events.py | 1 + 3 files changed, 15 insertions(+) diff --git a/curtsies/curtsieskeys.py b/curtsies/curtsieskeys.py index 8039de6..9de5267 100644 --- a/curtsies/curtsieskeys.py +++ b/curtsies/curtsieskeys.py @@ -45,6 +45,13 @@ (b'\x1bOQ', u''), (b'\x1bOR', u''), (b'\x1bOS', u''), + + # see bpython #626 + (b'\x1b[11~', u''), + (b'\x1b[12~', u''), + (b'\x1b[13~', u''), + (b'\x1b[14~', u''), + (b'\x1b[15~', u''), (b'\x1b[17~', u''), (b'\x1b[18~', u''), diff --git a/curtsies/events.py b/curtsies/events.py index c582c68..43d780e 100644 --- a/curtsies/events.py +++ b/curtsies/events.py @@ -41,6 +41,13 @@ CURSES_NAMES[b'\x1b[21~'] = u'KEY_F(10)' CURSES_NAMES[b'\x1b[23~'] = u'KEY_F(11)' CURSES_NAMES[b'\x1b[24~'] = u'KEY_F(12)' + +# see bpython #626 +CURSES_NAMES[b'\x1b[11~'] = u'KEY_F(1)' +CURSES_NAMES[b'\x1b[12~'] = u'KEY_F(2)' +CURSES_NAMES[b'\x1b[13~'] = u'KEY_F(3)' +CURSES_NAMES[b'\x1b[14~'] = u'KEY_F(4)' + CURSES_NAMES[b'\x1b[A'] = u'KEY_UP' CURSES_NAMES[b'\x1b[B'] = u'KEY_DOWN' CURSES_NAMES[b'\x1b[C'] = u'KEY_RIGHT' diff --git a/tests/test_events.py b/tests/test_events.py index cff333f..d90cf32 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -65,6 +65,7 @@ def test_sequences_without_names(self): self.assertEqual(get_utf([b'\xc3'], full=True, keynames='curses'), 'xC3') def test_key_names(self): + "Every key sequence with a Curses name should have a Curtsies name too." self.assertTrue(set(events.CURTSIES_NAMES).issuperset(set(events.CURSES_NAMES)), set(events.CURSES_NAMES) - set(events.CURTSIES_NAMES)) class TestGetKeyAscii(unittest.TestCase): From 732a5e3b90ce2068b31d3a0089f32e895a55a647 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Fri, 29 Jul 2016 15:15:10 -0400 Subject: [PATCH 003/302] add changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..dbdb72b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## [Unreleased] +### Added +- alternate codes for F1-F4 (fixes bpython #626) From b200efe0480e2f8479a599fdb390534e23d2e584 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Fri, 29 Jul 2016 15:26:49 -0400 Subject: [PATCH 004/302] fix link in readme --- readme.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 6924e62..650b008 100644 --- a/readme.md +++ b/readme.md @@ -86,4 +86,5 @@ About * `#bpython` on irc is a good place to talk about Curtsies, but feel free to open an issue if you're having a problem! * Thanks to the many contributors! -* If all you need are colored strings, consider one of these [other libraries](http://curtsies.readthedocs.org/en/latest/FmtStr.html#rationale)! +* If all you need are colored strings, consider one of these [other + libraries](http://curtsies.readthedocs.io/en/latest/FmtStr.html#fmtstr-rationale)! From 91bf877ee4a65664a79845ece7794ba3eb4209ea Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Fri, 29 Jul 2016 15:25:14 -0400 Subject: [PATCH 005/302] release 0.2.7 --- CHANGELOG.md | 2 ++ curtsies/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbdb72b..e52ceb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog ## [Unreleased] + +## [0.2.7] - 2016-08-29 ### Added - alternate codes for F1-F4 (fixes bpython #626) diff --git a/curtsies/__init__.py b/curtsies/__init__.py index 6ae98b2..da89628 100644 --- a/curtsies/__init__.py +++ b/curtsies/__init__.py @@ -1,5 +1,5 @@ """Terminal-formatted strings""" -__version__='0.2.6' +__version__='0.2.7' from .window import FullscreenWindow, CursorAwareWindow from .input import Input From f9fe134c377dc6a23b77b7f1505f3c844750af17 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Fri, 29 Jul 2016 16:49:28 -0400 Subject: [PATCH 006/302] fix #90 again, test this time --- curtsies/escseqparse.py | 2 +- curtsies/formatstring.py | 5 +++++ tests/test_fmtstr.py | 23 +++++++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/curtsies/escseqparse.py b/curtsies/escseqparse.py index 3468100..d4dc9dc 100644 --- a/curtsies/escseqparse.py +++ b/curtsies/escseqparse.py @@ -75,7 +75,7 @@ def peel_off_esc_code(s): d = m.groupdict() del d['front'] del d['rest'] - if 'numbers' in d and d['numbers'].split(';'): + if 'numbers' in d and all(d['numbers'].split(';')): d['numbers'] = [int(x) for x in d['numbers'].split(';')] return m.groupdict()['front'], d, m.groupdict()['rest'] diff --git a/curtsies/formatstring.py b/curtsies/formatstring.py index eddb8f9..f7152d9 100644 --- a/curtsies/formatstring.py +++ b/curtsies/formatstring.py @@ -146,6 +146,11 @@ def __init__(self, *components): @classmethod def from_str(cls, s): r""" + Return a FmtStr representing input. + + The str() of a FmtStr is guaranteed to produced the same FmtStr. + Other input with escape sequences may not be preserved. + >>> fmtstr("|"+fmtstr("hey", fg='red', bg='blue')+"|") '|'+on_blue(red('hey'))+'|' >>> fmtstr('|\x1b[31m\x1b[44mhey\x1b[49m\x1b[39m|') diff --git a/tests/test_fmtstr.py b/tests/test_fmtstr.py index 4ab868d..d5da7ff 100644 --- a/tests/test_fmtstr.py +++ b/tests/test_fmtstr.py @@ -47,6 +47,29 @@ def test_actual_init(self): FmtStr() +class TestFmtStrParsing(unittest.TestCase): + def test_no_escapes(self): + self.assertEqual(str(fmtstr('abc')), 'abc') + + def test_simple_escapes(self): + self.assertEqual(str(fmtstr('\x1b[33mhello\x1b[0m')), '\x1b[33mhello\x1b[39m') + self.assertEqual(str(fmtstr('\x1b[33mhello\x1b[39m')), '\x1b[33mhello\x1b[39m') + self.assertEqual(str(fmtstr('\x1b[33mhello')), '\x1b[33mhello\x1b[39m') + self.assertEqual(str(fmtstr('\x1b[43mhello\x1b[49m')), '\x1b[43mhello\x1b[49m') + self.assertEqual(str(fmtstr('\x1b[43mhello\x1b[0m')), '\x1b[43mhello\x1b[49m') + self.assertEqual(str(fmtstr('\x1b[43mhello')), '\x1b[43mhello\x1b[49m') + self.assertEqual(str(fmtstr('\x1b[33m\x1b[43mhello\x1b[0m')), + '\x1b[33m\x1b[43mhello\x1b[49m\x1b[39m') + + def test_out_of_order(self): + self.assertEqual(str(fmtstr('\x1b[33m\x1b[43mhello\x1b[39m\x1b[49m')), + '\x1b[33m\x1b[43mhello\x1b[49m\x1b[39m') + + def test_noncurtsies_output(self): + fmtstr('\x1b[35mx\x1b[m') + #fmtstr('\x1b[1m\x1b[31m-\x1b[m') + #fmtstr('\x1b[41mERROR\x1b[m') + class TestImmutability(unittest.TestCase): def test_fmt_strings_remain_unchanged_when_used_to_construct_other_ones(self): From 6f6ba212fd3185bcbb0dd1728274fa34fa154925 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Fri, 29 Jul 2016 17:15:29 -0400 Subject: [PATCH 007/302] add parsing fallback of stripping ansi seqs --- curtsies/escseqparse.py | 11 ++++++++++- curtsies/formatstring.py | 32 +++++++++++++++++++------------- tests/test_fmtstr.py | 6 ++++-- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/curtsies/escseqparse.py b/curtsies/escseqparse.py index d4dc9dc..00b5ba4 100644 --- a/curtsies/escseqparse.py +++ b/curtsies/escseqparse.py @@ -9,14 +9,23 @@ True """ +import re + from .termformatconstants import (FG_NUMBER_TO_COLOR, BG_NUMBER_TO_COLOR, NUMBER_TO_STYLE, RESET_ALL, RESET_FG, RESET_BG, STYLES) -import re + + +def remove_ansi(s): + return re.sub(r'(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]', '', s) def parse(s): r""" + Returns a list of strings or format dictionaries to describe the strings. + + May raise a ValueError if it can't be parsed. + >>> parse(">>> []") ['>>> []'] >>> #parse("\x1b[33m[\x1b[39m\x1b[33m]\x1b[39m\x1b[33m[\x1b[39m\x1b[33m]\x1b[39m\x1b[33m[\x1b[39m\x1b[33m]\x1b[39m\x1b[33m[\x1b[39m") diff --git a/curtsies/formatstring.py b/curtsies/formatstring.py index f7152d9..6ecf31d 100644 --- a/curtsies/formatstring.py +++ b/curtsies/formatstring.py @@ -26,7 +26,7 @@ import sys import wcwidth -from .escseqparse import parse +from .escseqparse import parse, remove_ansi from .termformatconstants import (FG_COLORS, BG_COLORS, STYLES, FG_NUMBER_TO_COLOR, BG_NUMBER_TO_COLOR, RESET_ALL, RESET_BG, RESET_FG, @@ -158,18 +158,24 @@ def from_str(cls, s): """ if '\x1b[' in s: - tokens_and_strings = parse(s) - bases = [] - cur_fmt = {} - for x in tokens_and_strings: - if isinstance(x, dict): - cur_fmt.update(x) - elif isinstance(x, (bytes, unicode)): - atts = parse_args('', dict((k, v) for k,v in cur_fmt.items() if v is not None)) - bases.append(Chunk(x, atts=atts)) - else: - raise Exception("logic error") - return FmtStr(*bases) + try: + tokens_and_strings = parse(s) + except ValueError: + return FmtStr(Chunk(remove_ansi(s))) + else: + bases = [] + cur_fmt = {} + for x in tokens_and_strings: + if isinstance(x, dict): + cur_fmt.update(x) + elif isinstance(x, (bytes, unicode)): + atts = parse_args('', dict((k, v) + for k, v in cur_fmt.items() + if v is not None)) + bases.append(Chunk(x, atts=atts)) + else: + raise Exception("logic error") + return FmtStr(*bases) else: return FmtStr(Chunk(s)) diff --git a/tests/test_fmtstr.py b/tests/test_fmtstr.py index d5da7ff..a3ee9f7 100644 --- a/tests/test_fmtstr.py +++ b/tests/test_fmtstr.py @@ -67,8 +67,10 @@ def test_out_of_order(self): def test_noncurtsies_output(self): fmtstr('\x1b[35mx\x1b[m') - #fmtstr('\x1b[1m\x1b[31m-\x1b[m') - #fmtstr('\x1b[41mERROR\x1b[m') + self.assertEqual(fmtstr('\x1b[Ahello'), 'hello') + self.assertEqual(fmtstr('\x1b[20Ahello'), 'hello') + self.assertEqual(fmtstr('\x1b[20mhello'), 'hello') + class TestImmutability(unittest.TestCase): From 0b0f9b123045447fbe0f092309fdb9f52b23c498 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Fri, 29 Jul 2016 18:02:11 -0400 Subject: [PATCH 008/302] update changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e52ceb4..b9cdb88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ # Changelog ## [Unreleased] +- fix #90 again +- strip ansi escape sequences if parsing fmtstr input fails ## [0.2.7] - 2016-08-29 -### Added - alternate codes for F1-F4 (fixes bpython #626) From cf94eca0987a0f28d2ce943f99454ab1af39bdcc Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Fri, 5 Aug 2016 19:42:30 -0400 Subject: [PATCH 009/302] Change bad arrow key binding --- curtsies/curtsieskeys.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/curtsies/curtsieskeys.py b/curtsies/curtsieskeys.py index 9de5267..ebe2370 100644 --- a/curtsies/curtsieskeys.py +++ b/curtsies/curtsieskeys.py @@ -16,10 +16,10 @@ (b'\x1b[B', u''), (b'\x1b[C', u''), (b'\x1b[D', u''), - (b'\x1bOA', u''), - (b'\x1bOB', u''), - (b'\x1bOC', u''), - (b'\x1bOD', u''), + (b'\x1bOA', u''), # in issue 92 its shown these should be normal arrows, + (b'\x1bOB', u''), # not ctrl-arrows as we previously had them. + (b'\x1bOC', u''), + (b'\x1bOD', u''), (b'\x1b[1;5A', u''), (b'\x1b[1;5B', u''), From 04bd612bb4b7932644cae97e0a896b2770910563 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Fri, 5 Aug 2016 20:19:46 -0400 Subject: [PATCH 010/302] Prevent negative cursor row. fixes bpython #607 --- curtsies/window.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/curtsies/window.py b/curtsies/window.py index 80c93e0..713ba57 100644 --- a/curtsies/window.py +++ b/curtsies/window.py @@ -448,8 +448,8 @@ def render_to_terminal(self, array, cursor_pos=(0, 0)): logger.debug( 'lines in current lines by row: %r' % current_lines_by_row.keys() ) - self._last_cursor_row = ( - cursor_pos[0] - offscreen_scrolls + self.top_usable_row + self._last_cursor_row = max( + 0, cursor_pos[0] - offscreen_scrolls + self.top_usable_row ) self._last_cursor_column = cursor_pos[1] self.write( From aafa0d0f47361824e5188e36f617b8b87d114eb4 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Sat, 6 Aug 2016 22:51:04 -0400 Subject: [PATCH 011/302] update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9cdb88..f17d766 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## [Unreleased] - fix #90 again - strip ansi escape sequences if parsing fmtstr input fails +- prevent invalid negative cursor positions in CursorAwareWindow (fixes bpython #607) +- '\x1bOA' changed from ctrl-arrow key to arrow key (fixes bpython #621) ## [0.2.7] - 2016-08-29 - alternate codes for F1-F4 (fixes bpython #626) From 88c78a21f83abcf9521d7e6bcaa1281e247c6d36 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Wed, 7 Sep 2016 21:12:34 -0400 Subject: [PATCH 012/302] bump version for 0.2.8 --- CHANGELOG.md | 4 +--- curtsies/__init__.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f17d766..f453b1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,8 @@ # Changelog -## [Unreleased] +## [0.2.8] - 2016-09-07 - fix #90 again - strip ansi escape sequences if parsing fmtstr input fails - prevent invalid negative cursor positions in CursorAwareWindow (fixes bpython #607) - '\x1bOA' changed from ctrl-arrow key to arrow key (fixes bpython #621) - -## [0.2.7] - 2016-08-29 - alternate codes for F1-F4 (fixes bpython #626) diff --git a/curtsies/__init__.py b/curtsies/__init__.py index da89628..e1f5fa5 100644 --- a/curtsies/__init__.py +++ b/curtsies/__init__.py @@ -1,5 +1,5 @@ """Terminal-formatted strings""" -__version__='0.2.7' +__version__='0.2.8' from .window import FullscreenWindow, CursorAwareWindow from .input import Input From dddb181850d8f61084e3fdfdf834f44b59bca4ba Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Wed, 7 Sep 2016 21:45:27 -0400 Subject: [PATCH 013/302] improve docs on pypi --- readme.md | 8 +++++--- setup.py | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/readme.md b/readme.md index 650b008..e6a7aa5 100644 --- a/readme.md +++ b/readme.md @@ -45,6 +45,7 @@ Primer colors and styles displayable in a terminal with [ANSI escape sequences](http://en.wikipedia.org/wiki/ANSI_escape_code>`_). (the import statement shown below is outdated) + ![fmtstr example screenshot](http://i.imgur.com/7lFaxsz.png) [FSArray](http://curtsies.readthedocs.org/en/latest/FSArray.html) objects contain multiple such strings @@ -53,6 +54,7 @@ objects can be superimposed on each other to build complex grids of colored and styled characters through composition. (the import statement shown below is outdated) + ![fsarray example screenshot](http://i.imgur.com/rvTRPv1.png) Such grids of characters can be rendered to the terminal in alternate screen mode @@ -66,15 +68,15 @@ Examples * [Tic-Tac-Toe](/examples/tictactoeexample.py) -![screenshot](http://i.imgur.com/AucB55B.png) +![](http://i.imgur.com/AucB55B.png) * [Avoid the X's game](/examples/gameexample.py) -![screenshot](http://i.imgur.com/nv1RQd3.png) +![](http://i.imgur.com/nv1RQd3.png) * [Bpython-curtsies uses curtsies](http://ballingt.com/2013/12/21/bpython-curtsies.html) -[![ScreenShot](http://i.imgur.com/r7rZiBS.png)](http://www.youtube.com/watch?v=lwbpC4IJlyA) +[![](http://i.imgur.com/r7rZiBS.png)](http://www.youtube.com/watch?v=lwbpC4IJlyA) * [More examples](/examples) diff --git a/setup.py b/setup.py index 1d5c5a2..f0c496d 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ from setuptools import setup import ast import os +import io def version(): """Return version string.""" @@ -9,9 +10,29 @@ def version(): if line.startswith('__version__'): return ast.parse(line).body[0].value.s +def get_long_description(): + with io.open('README.md', encoding="utf-8") as f: + long_description = f.read() + + try: + import pypandoc + except ImportError: + print('pypandoc not installed, using file contents.') + return long_description + + try: + long_description = pypandoc.convert('README.md', 'rst') + except OSError: + print("Pandoc not found. Long_description conversion failure.") + return long_description + else: + long_description = long_description.replace("\r", "") + return long_description + setup(name='curtsies', version=version(), description='Curses-like terminal wrapper, with colored strings!', + long_description=get_long_description(), url='https://github.com/thomasballinger/curtsies', author='Thomas Ballinger', author_email='thomasballinger@gmail.com', From 4f5f6b9d80952aeecb989324ca20f60276d783f5 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Wed, 7 Sep 2016 21:47:41 -0400 Subject: [PATCH 014/302] remove uninformative alt-text --- readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index e6a7aa5..e83b56d 100644 --- a/readme.md +++ b/readme.md @@ -46,7 +46,7 @@ colors and styles displayable in a terminal with [ANSI escape sequences](http:// (the import statement shown below is outdated) -![fmtstr example screenshot](http://i.imgur.com/7lFaxsz.png) +![](http://i.imgur.com/7lFaxsz.png) [FSArray](http://curtsies.readthedocs.org/en/latest/FSArray.html) objects contain multiple such strings with each formatted string on its own row, and FSArray @@ -55,7 +55,7 @@ to build complex grids of colored and styled characters through composition. (the import statement shown below is outdated) -![fsarray example screenshot](http://i.imgur.com/rvTRPv1.png) +![](http://i.imgur.com/rvTRPv1.png) Such grids of characters can be rendered to the terminal in alternate screen mode (no history, like `Vim`, `top` etc.) by [FullscreenWindow](http://curtsies.readthedocs.org/en/latest/window.html#curtsies.window.FullscreenWindow) objects From 07fec7fa290faf9efbce6e34d3473c976e73744c Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Thu, 8 Sep 2016 00:49:52 -0400 Subject: [PATCH 015/302] fix build on case-sensitive file systems stupid mac --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index f0c496d..6975992 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ def version(): return ast.parse(line).body[0].value.s def get_long_description(): - with io.open('README.md', encoding="utf-8") as f: + with io.open('readme.md', encoding="utf-8") as f: long_description = f.read() try: @@ -21,7 +21,7 @@ def get_long_description(): return long_description try: - long_description = pypandoc.convert('README.md', 'rst') + long_description = pypandoc.convert('readme.md', 'rst') except OSError: print("Pandoc not found. Long_description conversion failure.") return long_description From 18d700200340242d45733c6cc0812ee7e3b54bdc Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Thu, 8 Sep 2016 01:03:44 -0400 Subject: [PATCH 016/302] version bump to 0.2.9 the 0.2.8 distribution was fine, but setup.py does not run on case-sensitive systems. This might be confusing later, so releasing another version --- CHANGELOG.md | 2 +- curtsies/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f453b1a..07f625a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [0.2.8] - 2016-09-07 +## [0.2.9] - 2016-09-07 - fix #90 again - strip ansi escape sequences if parsing fmtstr input fails - prevent invalid negative cursor positions in CursorAwareWindow (fixes bpython #607) diff --git a/curtsies/__init__.py b/curtsies/__init__.py index e1f5fa5..cce97a2 100644 --- a/curtsies/__init__.py +++ b/curtsies/__init__.py @@ -1,5 +1,5 @@ """Terminal-formatted strings""" -__version__='0.2.8' +__version__='0.2.9' from .window import FullscreenWindow, CursorAwareWindow from .input import Input From 213487e8e5b1594d0866e88ac4f43cf6ad45632d Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Mon, 10 Oct 2016 13:36:17 -0400 Subject: [PATCH 017/302] add sequences for home and end keys --- curtsies/curtsieskeys.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/curtsies/curtsieskeys.py b/curtsies/curtsieskeys.py index ebe2370..d1a60e3 100644 --- a/curtsies/curtsieskeys.py +++ b/curtsies/curtsieskeys.py @@ -81,7 +81,8 @@ (b'\x1b[H', u''), # reported by amorozov in bpython #490 (b'\x1b[F', u''), # reported by amorozov in bpython #490 - # see curtsies #78 - taken from https://github.com/jquast/blessed/blob/e9ad7b85dfcbbba49010ab8c13e3a5920d81b010/blessed/keyboard.py#L409 + (b'\x1bOH', u''), # reported by mixmastamyk in curtsies #78 + (b'\x1bOF', u''), # reported by mixmastamyk in curtsies #78 # not fixing for back compat. # (b"\x1b[1~", u''), # find From 6aefffbcca4b27913cd24abd796048b33ccab5fc Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Mon, 10 Oct 2016 14:24:28 -0400 Subject: [PATCH 018/302] Release version 0.2.10 --- CHANGELOG.md | 5 +++++ curtsies/__init__.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07f625a..fc8e30a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [Unreleased] + +## [0.2.10] - 2016-10-10 +- Add sequences for home and end (fixes Curtsies #78) + ## [0.2.9] - 2016-09-07 - fix #90 again - strip ansi escape sequences if parsing fmtstr input fails diff --git a/curtsies/__init__.py b/curtsies/__init__.py index cce97a2..a1fda67 100644 --- a/curtsies/__init__.py +++ b/curtsies/__init__.py @@ -1,5 +1,5 @@ """Terminal-formatted strings""" -__version__='0.2.9' +__version__='0.2.10' from .window import FullscreenWindow, CursorAwareWindow from .input import Input From 4beb188eeebd7ed8f6a9b4b469d6b2da6f2c7f8d Mon Sep 17 00:00:00 2001 From: Sindhu S Date: Thu, 27 Nov 2014 18:49:21 +0530 Subject: [PATCH 019/302] Minor grammar fixes --- docs/FmtStr.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/FmtStr.rst b/docs/FmtStr.rst index 78437f7..0f5f098 100644 --- a/docs/FmtStr.rst +++ b/docs/FmtStr.rst @@ -20,15 +20,15 @@ FmtStr - Example str(full) print(full) -We start here with such a complicated example because it you only need something simple like +We start here with such a complicated example because it you only need something simple like: .. python_terminal_session:: from curtsies.fmtfuncs import * print(blue(bold(u'Deep blue sea'))) -another library may be a better fit than Curtsies. -Unlink other libraries, Curtsies allows these colored strings to be further manipulated after +then another library may be a better fit than Curtsies. +Unlike other libraries, Curtsies allows these colored strings to be further manipulated after they are created. FmtStr - Rationale @@ -46,7 +46,7 @@ If all you need is to print colored text, many other libraries also make `ANSI e In all of the libraries listed above the expression ``blue('hi') + ' ' + green('there)`` or equivalent evaluates to a Python string, not a colored string object. If all you plan -to do with this string is print it, this is great. But if you need to +to do with this string is print it, this is great but if you need to do more formatting with this colored string later, the length will be something like 29 instead of 9; structured formatting information is lost. Methods like :py:meth:`center ` @@ -201,5 +201,5 @@ FmtStr - API Docs .. automodule:: curtsies.fmtfuncs -:py:class:`FmtStr` instances respond to most :class:`str` methods as you might expect, but the result -of these methods sometimes loses its formatting. +:py:class:`FmtStr` instances respond to most :class:`str` methods, but some +of these methods will return strings with their formatting missing. From 72f3a6b17e642a7746fc7a1b49b2692f63bfe866 Mon Sep 17 00:00:00 2001 From: Sindhu S Date: Thu, 27 Nov 2014 18:51:07 +0530 Subject: [PATCH 020/302] Minor typo fix --- docs/FmtStr.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/FmtStr.rst b/docs/FmtStr.rst index 0f5f098..abb403f 100644 --- a/docs/FmtStr.rst +++ b/docs/FmtStr.rst @@ -90,7 +90,7 @@ A :py:class:`FmtStr` can be sliced to produce a new :py:class:`FmtStr` object: >>> f.splice('something longer', 2) blue("h")+"something longer"+blue("ot")+blue(" there")+on_red(" Tom!") -:py:class:`FmtStr` greedily absorb strings, but no formatting is applied to this added text +:py:class:`FmtStr` greedily absorbs strings, but no formatting is applied to this added text. >>> from curtsies.fmtfuncs import * >>> f = blue("The story so far:") + "In the beginning..." From 8d1788f7566e06e4582b9df8003f3cfaf58ec0ba Mon Sep 17 00:00:00 2001 From: Dan Puttick Date: Wed, 26 Oct 2016 15:21:37 -0400 Subject: [PATCH 021/302] Moved notes into a notes/ directory --- notesfromdarius.txt => notes/notesfromdarius.txt | 0 timing_notes.txt => notes/timing_notes.txt | 0 todo => notes/todo.txt | 0 window_resize_notes.txt => notes/window_resize_notes.txt | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename notesfromdarius.txt => notes/notesfromdarius.txt (100%) rename timing_notes.txt => notes/timing_notes.txt (100%) rename todo => notes/todo.txt (100%) rename window_resize_notes.txt => notes/window_resize_notes.txt (100%) diff --git a/notesfromdarius.txt b/notes/notesfromdarius.txt similarity index 100% rename from notesfromdarius.txt rename to notes/notesfromdarius.txt diff --git a/timing_notes.txt b/notes/timing_notes.txt similarity index 100% rename from timing_notes.txt rename to notes/timing_notes.txt diff --git a/todo b/notes/todo.txt similarity index 100% rename from todo rename to notes/todo.txt diff --git a/window_resize_notes.txt b/notes/window_resize_notes.txt similarity index 100% rename from window_resize_notes.txt rename to notes/window_resize_notes.txt From 73ef2712d271ae5755e1402ee5fce8323dedfdd9 Mon Sep 17 00:00:00 2001 From: Dan Puttick Date: Wed, 19 Oct 2016 22:31:32 -0400 Subject: [PATCH 022/302] Fixed typos in docs/FmtStr.rst --- docs/FmtStr.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/FmtStr.rst b/docs/FmtStr.rst index abb403f..bfb8c90 100644 --- a/docs/FmtStr.rst +++ b/docs/FmtStr.rst @@ -74,7 +74,7 @@ blue("asdf")+on_red("adsf") FmtStr - Using ============== -A :py:class:`FmtStr` can be sliced to produce a new :py:class:`FmtStr` object: +A :py:class:`FmtStr` can be sliced to produce a new :py:class:`FmtStr` objects: >>> from curtsies.fmtfuncs import * >>> (blue('asdf') + on_red('adsf'))[3:7] @@ -90,7 +90,7 @@ A :py:class:`FmtStr` can be sliced to produce a new :py:class:`FmtStr` object: >>> f.splice('something longer', 2) blue("h")+"something longer"+blue("ot")+blue(" there")+on_red(" Tom!") -:py:class:`FmtStr` greedily absorbs strings, but no formatting is applied to this added text. +:py:class:`FmtStr` greedily absorb strings, but no formatting is applied to this added text: >>> from curtsies.fmtfuncs import * >>> f = blue("The story so far:") + "In the beginning..." @@ -99,7 +99,7 @@ A :py:class:`FmtStr` can be sliced to produce a new :py:class:`FmtStr` object: >>> f blue("The story so far:")+"In the beginning..." -It's easy to turn ANSI terminal formatted strings into :py:class:`FmtStr` +It's easy to turn ANSI terminal formatted strings into :py:class:`FmtStr`: >>> from curtsies.fmtfuncs import * >>> from curtsies import FmtStr From 52c4979f4f5e8053dfb7f5041dabe2ff15cc7812 Mon Sep 17 00:00:00 2001 From: Dan Puttick Date: Wed, 19 Oct 2016 23:07:02 -0400 Subject: [PATCH 023/302] Added formatting to docs/FSArray.rst --- docs/FSArray.rst | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/FSArray.rst b/docs/FSArray.rst index f138f9a..ca32809 100644 --- a/docs/FSArray.rst +++ b/docs/FSArray.rst @@ -16,12 +16,12 @@ FSArray - Example blue(on_green(u'hey!'))]) a.dumb_display() -`fsarray` is a convenience function returning a FSArray constructed from its arguments. +:py:class:`curtsies.formatstringarray.fsarray` is a convenience function returning a :py:class:`~curtsies.formatstringarray.FSArray` constructed from its arguments. FSArray - Using =============== -Arrays can be composed to build up complex text interfaces:: +:py:class:`~curtsies.formatstringarray.FSArray` objects can be composed to build up complex text interfaces:: >>> import time >>> from curtsies import FSArray, fsarray, fmtstr @@ -54,15 +54,14 @@ Arrays can be composed to build up complex text interfaces:: An array like shown above might be repeatedly constructed and rendered with a :py:mod:`curtsies.window` object. -Slicing works like it does with FmtStrs, but in two dimensions. -FSArrays are *mutable*, so array assignment syntax can be used for natural +Slicing works like it does with a :py:class:`FmtStr`, but in two dimensions. +:py:class:`~curtsies.formatstringarray.FSArray`s are *mutable*, so array assignment syntax can be used for natural compositing as in the above exaple. If you're dealing with terminal output, the *width* of a string becomes more important than it's *length* (see :ref:`len-vs-width`). -In the future FSArrays will do slicing and array assignment based on width -instead of number of characters, but this is not currently implemented. +In the future :py:class:`~curtsies.formatstringarray.FSArray`s will do slicing and array assignment based on width instead of number of characters, but this is not currently implemented. FSArray - API docs ================== From 47e69e3c725eb5e0877cdbd7bdb1700861cc04d4 Mon Sep 17 00:00:00 2001 From: Dan Puttick Date: Wed, 2 Nov 2016 15:45:00 -0400 Subject: [PATCH 024/302] Added Python version info to index.rst and readme.md Resolves #81 --- docs/index.rst | 2 +- readme.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 4c51360..d3601d7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,7 +5,7 @@ Curtsies documentation |curtsiestitle| -Curtsies is a library for interacting with the terminal. +Curtsies is a Python 2.6+ & 3.3+ compatible library for interacting with the terminal. :py:class:`~curtsies.formatstring.FmtStr` objects are strings formatted with colors and styles displayable in a terminal with `ANSI escape sequences `_. diff --git a/readme.md b/readme.md index e83b56d..d66c8da 100644 --- a/readme.md +++ b/readme.md @@ -2,7 +2,7 @@ [![Documentation Status](https://readthedocs.org/projects/curtsies/badge/?version=latest)](https://readthedocs.org/projects/curtsies/?badge=latest) ![Curtsies Logo](http://ballingt.com/assets/curtsiestitle.png) -Curtsies is a library for interacting with the terminal. +Curtsies is a Python 2.6+ & 3.3+ compatible library for interacting with the terminal. This is what using (nearly every feature of) curtsies looks like: ```python From 2c9f4b94cd86a2c35371598dd729b72ef5828a17 Mon Sep 17 00:00:00 2001 From: Dan Puttick Date: Wed, 2 Nov 2016 16:16:55 -0400 Subject: [PATCH 025/302] Edits to FmtStr.rst --- docs/FmtStr.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/FmtStr.rst b/docs/FmtStr.rst index bfb8c90..5ed55c4 100644 --- a/docs/FmtStr.rst +++ b/docs/FmtStr.rst @@ -43,10 +43,10 @@ If all you need is to print colored text, many other libraries also make `ANSI e * `Clint `_ (``pip install clint``) * `colors `_ (``pip install colors``) -In all of the libraries listed above the expression ``blue('hi') + ' ' + green('there)`` +In all of the libraries listed above, the expression ``blue('hi') + ' ' + green('there)`` or equivalent evaluates to a Python string, not a colored string object. If all you plan -to do with this string is print it, this is great but if you need to +to do with this string is print it, this is great. But, if you need to do more formatting with this colored string later, the length will be something like 29 instead of 9; structured formatting information is lost. Methods like :py:meth:`center ` @@ -62,9 +62,9 @@ won't properly format the string for display. u' \x1b[31m\x1b[42mRed on green?\x1b[m\x0f \x1b[33mIck!\x1b[m\x0f ' :py:class:`FmtStr` objects can be combined and composited to create more complicated -:py:class:`FmtStr` object, +:py:class:`FmtStr` objects, useful for building flashy terminal interfaces with overlapping -windows/widgets than can change size and depend on each others sizes. +windows/widgets that can change size and depend on each other's sizes. One :py:class:`FmtStr` can have several kinds of formatting applied to different parts of it. >>> from curtsies.fmtfuncs import * @@ -176,7 +176,7 @@ FmtStr - len vs width --------------------- The amount of horizontal space a string takes up in a terminal may differ from the length of the string returned by ``len()``. -:py:class:`FmtStr` objects have a width property useful when writing layout code: +:py:class:`FmtStr` objects have a :py:class:`FmtStr.width` property useful when writing layout code: >>> #encoding: utf8 ... From 89a95bc64872b7d9ecc5c10aa1362e6e829d14a4 Mon Sep 17 00:00:00 2001 From: Dan Puttick Date: Wed, 2 Nov 2016 17:35:04 -0400 Subject: [PATCH 026/302] Changed curtsies.formatstringarray.FSArray references to curtsies.FSArray Not sure if this is the right thing to do, but it seems to me that users will access FSArrays directly via curtsies.FSArray so this is less confusing. In any case, the documentation for FSArray was being repeated at the bottom so we should probably pick one format and stick with it? --- docs/FSArray.rst | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/docs/FSArray.rst b/docs/FSArray.rst index ca32809..1e35427 100644 --- a/docs/FSArray.rst +++ b/docs/FSArray.rst @@ -1,7 +1,7 @@ FSArray ^^^^^^^ -:py:class:`~curtsies.formatstringarray.FSArray` is a two dimensional grid of colored and styled characters. +:py:class:`~curtsies.FSArray` is a two dimensional grid of colored and styled characters. FSArray - Example ================= @@ -16,12 +16,12 @@ FSArray - Example blue(on_green(u'hey!'))]) a.dumb_display() -:py:class:`curtsies.formatstringarray.fsarray` is a convenience function returning a :py:class:`~curtsies.formatstringarray.FSArray` constructed from its arguments. +:py:class:`~curtsies.fsarray` is a convenience function returning a :py:class:`~curtsies.FSArray` constructed from its arguments. FSArray - Using =============== -:py:class:`~curtsies.formatstringarray.FSArray` objects can be composed to build up complex text interfaces:: +:py:class:`~curtsies.FSArray` objects can be composed to build up complex text interfaces:: >>> import time >>> from curtsies import FSArray, fsarray, fmtstr @@ -54,21 +54,19 @@ FSArray - Using An array like shown above might be repeatedly constructed and rendered with a :py:mod:`curtsies.window` object. -Slicing works like it does with a :py:class:`FmtStr`, but in two dimensions. -:py:class:`~curtsies.formatstringarray.FSArray`s are *mutable*, so array assignment syntax can be used for natural +Slicing works like it does with a :py:class:`~curtsies.formatstring.FmtStr`, but in two dimensions. +:py:class:`~curtsies.FSArray` are *mutable*, so array assignment syntax can be used for natural compositing as in the above exaple. If you're dealing with terminal output, the *width* of a string becomes more important than it's *length* (see :ref:`len-vs-width`). -In the future :py:class:`~curtsies.formatstringarray.FSArray`s will do slicing and array assignment based on width instead of number of characters, but this is not currently implemented. +In the future :py:class:`~curtsies.FSArray` will do slicing and array assignment based on width instead of number of characters, but this is not currently implemented. FSArray - API docs ================== -.. automodule:: curtsies.formatstringarray - :members: FSArray, fsarray +.. autofunction:: curtsies.fsarray .. autoclass:: curtsies.FSArray :members: - From 798a3fa701f00b04d37270504fba14279c7ce774 Mon Sep 17 00:00:00 2001 From: Dan Puttick Date: Wed, 2 Nov 2016 18:33:06 -0400 Subject: [PATCH 027/302] Changing curtsies.formatstring.FmtStr references to curtsies.Fmtstr See previous commit, doing the same for FmtStr --- docs/FmtStr.rst | 35 +++++++++++++++++------------------ docs/index.rst | 4 ++-- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/docs/FmtStr.rst b/docs/FmtStr.rst index 5ed55c4..25168c0 100644 --- a/docs/FmtStr.rst +++ b/docs/FmtStr.rst @@ -1,7 +1,7 @@ FmtStr ^^^^^^ -:py:class:`~curtsies.formatstring.FmtStr` is a string with each character colored +:py:class:`~curtsies.FmtStr` is a string with each character colored and styled in ways representable by `ANSI escape codes `_. .. automodule:: curtsies.formatstring @@ -61,11 +61,11 @@ won't properly format the string for display. >>> message.center(50) u' \x1b[31m\x1b[42mRed on green?\x1b[m\x0f \x1b[33mIck!\x1b[m\x0f ' -:py:class:`FmtStr` objects can be combined and composited to create more complicated -:py:class:`FmtStr` objects, +:py:class:`~curtsies.FmtStr` objects can be combined and composited to create more complicated +:py:class:`~curtsies.FmtStr` objects, useful for building flashy terminal interfaces with overlapping windows/widgets that can change size and depend on each other's sizes. -One :py:class:`FmtStr` can have several kinds of formatting applied to different parts of it. +One :py:class:`~curtsies.FmtStr` can have several kinds of formatting applied to different parts of it. >>> from curtsies.fmtfuncs import * >>> blue('asdf') + on_red('adsf') @@ -74,13 +74,13 @@ blue("asdf")+on_red("adsf") FmtStr - Using ============== -A :py:class:`FmtStr` can be sliced to produce a new :py:class:`FmtStr` objects: +A :py:class:`~curtsies.FmtStr` can be sliced to produce a new :py:class:`~curtsies.FmtStr` objects: >>> from curtsies.fmtfuncs import * >>> (blue('asdf') + on_red('adsf'))[3:7] blue("f")+on_red("ads") -:py:class:`FmtStr` are *immutable* - but you can create new ones with :py:meth:`FmtStr.splice`: +:py:class:`~curtsies.FmtStr` are *immutable* - but you can create new ones with :py:meth:`~curtsies.FmtStr.splice`: >>> from curtsies.fmtfuncs import * >>> f = blue('hey there') + on_red(' Tom!') @@ -90,7 +90,7 @@ A :py:class:`FmtStr` can be sliced to produce a new :py:class:`FmtStr` objects: >>> f.splice('something longer', 2) blue("h")+"something longer"+blue("ot")+blue(" there")+on_red(" Tom!") -:py:class:`FmtStr` greedily absorb strings, but no formatting is applied to this added text: +:py:class:`~curtsies.FmtStr` greedily absorb strings, but no formatting is applied to this added text: >>> from curtsies.fmtfuncs import * >>> f = blue("The story so far:") + "In the beginning..." @@ -99,7 +99,7 @@ A :py:class:`FmtStr` can be sliced to produce a new :py:class:`FmtStr` objects: >>> f blue("The story so far:")+"In the beginning..." -It's easy to turn ANSI terminal formatted strings into :py:class:`FmtStr`: +It's easy to turn ANSI terminal formatted strings into :py:class:`~curtsies.FmtStr`: >>> from curtsies.fmtfuncs import * >>> from curtsies import FmtStr @@ -113,8 +113,8 @@ FmtStr - Using str methods -------------------------- All sorts of `string methods `_ -can be used on a :py:class:`FmtStr`, so you can often -use :py:class:`FmtStr` objects where you had strings in your program before:: +can be used on a :py:class:`~curtsies.FmtStr`, so you can often +use :py:class:`~curtsies.FmtStr` objects where you had strings in your program before:: >>> from curtsies.fmtfuncs import * >>> f = blue(underline('As you like it')) @@ -125,7 +125,7 @@ use :py:class:`FmtStr` objects where you had strings in your program before:: >>> blue(', ').join(['a', red('b')]) "a"+blue(", ")+red("b") -If :py:class:`FmtStr` doesn't implement a method, it tries its best to use the string +If :py:class:`~curtsies.FmtStr` doesn't implement a method, it tries its best to use the string method, which often works pretty well:: >>> from curtsies.fmtfuncs import * @@ -163,7 +163,7 @@ In Python 2, you might run into something like this: >>> red('hi') ValueError: unicode string required, got 'hi' -:py:class:`FmtStr` requires unicode strings, so in Python 2 it is convenient to use the unicode_literals compiler directive: +:py:class:`~curtsies.FmtStr` requires unicode strings, so in Python 2 it is convenient to use the unicode_literals compiler directive: >>> from __future__ import unicode_literals >>> from curtsies.fmtfuncs import * @@ -176,7 +176,7 @@ FmtStr - len vs width --------------------- The amount of horizontal space a string takes up in a terminal may differ from the length of the string returned by ``len()``. -:py:class:`FmtStr` objects have a :py:class:`FmtStr.width` property useful when writing layout code: +:py:class:`~curtsies.FmtStr` objects have a :py:class:`~curtsies.FmtStr.width` property useful when writing layout code: >>> #encoding: utf8 ... @@ -193,13 +193,12 @@ As shown above, `full width characters `_. -:py:class:`~curtsies.formatstringarray.FSArray` objects contain multiple such strings +:py:class:`~curtsies.FSArray` objects contain multiple such strings with each formatted string on its own row, and can be superimposed onto each other to build complex grids of colored and styled characters. From bfc229214c3a485301dcd071003ada356afbb2dd Mon Sep 17 00:00:00 2001 From: Dan Puttick Date: Wed, 2 Nov 2016 19:09:48 -0400 Subject: [PATCH 028/302] Fixed typos in input.rst, curtsies.terminal -> curtsies.event Resolves #93 --- docs/Input.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/Input.rst b/docs/Input.rst index c8e3339..3cd8e04 100644 --- a/docs/Input.rst +++ b/docs/Input.rst @@ -63,9 +63,9 @@ must be used within the context of that :class:`~curtsies.input.Input` object. Within the (context-manager) context of an Input generator, an in-stream is put in raw mode or cbreak mode, and keypresses are stored to be reported -later. Original tty attribute are recorded to be restored on exiting +later. Original tty attributes are recorded to be restored on exiting the context. The SigInt signal handler may be replaced if this behavior was -specified on creation of the :class:`~curtsies.input.Input` object +specified on creation of the :class:`~curtsies.input.Input` object. Input - Notes ============= @@ -98,7 +98,7 @@ Input - Events To see what a given keypress is called (what unicode string is returned by ``Terminal.next()``), try -``python -m curtsies.terminal`` and play around. +``python -m curtsies.events`` and play around. Events returned by :py:class:`~curtsies.input.Input` fall into two categories: instances of subclasses of :class:`curtsies.event.Event` and Keypress strings. From 31b8f3a4bd474ea72c0d15b8dc38982c44f67fd4 Mon Sep 17 00:00:00 2001 From: Dan Puttick Date: Fri, 4 Nov 2016 15:01:36 -0400 Subject: [PATCH 029/302] Changed curtsies.window.X references to curtsies.X throughout docs --- docs/index.rst | 4 ++-- docs/window.rst | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 545c84d..38b267c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -15,8 +15,8 @@ can be superimposed onto each other to build complex grids of colored and styled characters. Such grids of characters can be efficiently rendered to the terminal in alternate screen mode -(no scrollback history, like ``Vim``, ``top`` etc.) by :py:class:`~curtsies.window.FullscreenWindow` objects -or to the normal history-preserving screen by :py:class:`~curtsies.window.CursorAwareWindow` objects. +(no scrollback history, like ``Vim``, ``top`` etc.) by :py:class:`~curtsies.FullscreenWindow` objects +or to the normal history-preserving screen by :py:class:`~curtsies.CursorAwareWindow` objects. User keyboard input events like pressing the up arrow key are detected by an :py:class:`~curtsies.input.Input` object. See the :doc:`quickstart` to get started using all of these classes. diff --git a/docs/window.rst b/docs/window.rst index d74fcfd..759b682 100644 --- a/docs/window.rst +++ b/docs/window.rst @@ -3,16 +3,16 @@ Window Objects .. automodule:: curtsies.window -Windows successively render 2D grids of text (usually instances of :py:class:`~curtsies.formatstringarray.FSArray`) +Windows successively render 2D grids of text (usually instances of :py:class:`~curtsies.FSArray`) to the terminal. A window owns its output stream - it is assumed (but not enforced) that no additional data is written to this stream between renders, an assumption which allowing for example portions of the screen which do not change between renderings not to be redrawn during a rendering. -There are two useful window classes, both subclasses of :py:class:`~curtsies.window.BaseWindow`: :py:class:`~curtsies.window.FullscreenWindow` +There are two useful window classes, both subclasses of :py:class:`~curtsies.window.BaseWindow`. :py:class:`~curtsies.FullscreenWindow` renders to the terminal's `alternate screen buffer `_ (no history preserved, like command line tools ``Vim`` and ``top``) -while :py:class:`~curtsies.window.CursorAwareWindow` renders to the normal screen. +while :py:class:`~curtsies.CursorAwareWindow` renders to the normal screen. It is also is capable of querying the terminal for the cursor location, and uses this functionality to detect how a terminal moves its contents around during a window size change. @@ -41,20 +41,20 @@ Any change that does occur in cursor position is attributed to movement of conte in response to a window size change and is used to calculate how this content has moved, necessary because this behavior differs between terminal emulators. -Entering the context of a FullscreenWindow object hides the cursor and switches to -the alternate terminal screen. Entering the context of a CursorAwareWindow hides +Entering the context of a :py:class:`~curtsies.FullscreenWindow` object hides the cursor and switches to +the alternate terminal screen. Entering the context of a :py:class:`~curtsies.CursorAwareWindow` hides the cursor, turns on cbreak mode, and records the cursor position. Leaving the context does more or less the inverse. Window Objects - API Docs ========================= -.. autoclass:: BaseWindow +.. autoclass:: curtsies.window.BaseWindow :members: -.. autoclass:: FullscreenWindow +.. autoclass:: curtsies.FullscreenWindow :members: -.. autoclass:: CursorAwareWindow +.. autoclass:: curtsies.CursorAwareWindow :members: From fc29278a14d49537a224959d8437dbdf4eec9726 Mon Sep 17 00:00:00 2001 From: Dan Puttick Date: Fri, 4 Nov 2016 16:31:28 -0400 Subject: [PATCH 030/302] Changed curtsies.input.Input refs to curtsies.Input Also had to make some changes to Events to make all the refs work. Some small typo fixes sprinkled in too (sorry!) --- docs/Input.rst | 57 ++++++++++++++++++++++++++------------------------ docs/index.rst | 2 +- 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/docs/Input.rst b/docs/Input.rst index 3cd8e04..dba5c9d 100644 --- a/docs/Input.rst +++ b/docs/Input.rst @@ -1,8 +1,8 @@ Input ^^^^^ -.. automodule:: curtsies.input +.. automodule:: curtsies.Input -:py:class:`~curtsies.input.Input` objects provide user keypress events +:py:class:`~curtsies.Input` objects provide user keypress events and other control events. Input - Example @@ -19,93 +19,96 @@ Input - Example Input - Getting Keyboard Events =============================== -The simplest way to use an :class:`~curtsies.input.Input` object is to +The simplest way to use an :py:class:`~curtsies.Input` object is to iterate over it in a for loop: each time a keypress is detected or other event occurs, an event is produced and can be acted upon. Since it's iterable, ``next()`` can be used to wait for a single event. -:meth:`~curtsies.input.Input.send` works like ``next()`` but takes a timeout +:py:meth:`~curtsies.Input.send` works like ``next()`` but takes a timeout in seconds, which if reached will cause None to be returned signalling that no keypress or other event occured within the timeout. Key events are unicode strings, but sometimes event objects -(see :mod:`curtsies.events`) are returned instead. -Built-in events signal SigInt events from the OS and PasteEvents consisting +(see :class:`~curtsies.events.Event`) are returned instead. +Built-in events signal :py:class:`~curtsies.events.SigIntEvent` +events from the OS and :py:class:`~curtsies.events.PasteEvent` consisting of multiple keypress events if reporting of these types of events was enabled -in instatiation of the :py:class:`~curtsies.input.Input` object. +in instantiation of the :py:class:`~curtsies.Input` object. Input - Using as a Reactor ========================== Custom events can also be scheduled to be returned from -:py:class:`~curtsies.input.Input` with callback functions +:py:class:`~curtsies.Input` with callback functions created by the event trigger methods. Each of these methods returns a callback that will schedule an instance of the desired event type: -* Using a callback created by :py:meth:`~curtsies.input.Input.event_trigger` +* Using a callback created by :py:meth:`~curtsies.Input.event_trigger` schedules an event to be returned the next time an event is requested, but not if an event has already been requested (if called from another thread). -* :py:meth:`~curtsies.input.Input.threadsafe_event_trigger` does the same, +* :py:meth:`~curtsies.Input.threadsafe_event_trigger` does the same, but may notify a concurrent request for an event so that the custom event is immediately returned. -* :py:meth:`~curtsies.input.Input.scheduled_event_trigger` schedules an event +* :py:meth:`~curtsies.Input.scheduled_event_trigger` schedules an event to be returned at some point in the future. Input - Context =============== -``next()`` and :meth:`~curtsies.input.Input.send()`` -must be used within the context of that :class:`~curtsies.input.Input` object. +``next()`` and :meth:`~curtsies.Input.send()` +must be used within the context of that :class:`~curtsies.Input` object. Within the (context-manager) context of an Input generator, an in-stream is put in raw mode or cbreak mode, and keypresses are stored to be reported later. Original tty attributes are recorded to be restored on exiting the context. The SigInt signal handler may be replaced if this behavior was -specified on creation of the :class:`~curtsies.input.Input` object. +specified on creation of the :class:`~curtsies.Input` object. Input - Notes ============= -``Input`` takes an optional argument for how to name -keypress events, which is 'curtsies' by default. -For compatibility with curses code, you can use 'curses' names, +:py:class:`~curtsies.Input` takes an optional argument ``keynames`` for how to name +keypress events, which is ``'curtsies'`` by default. +For compatibility with curses code, you can use ``'curses'`` names, but note that curses doesn't have nice key names for many key combinations so you'll be putting up with names like ``u'\xe1'`` for option-j and ``'\x86'`` for ctrl-option-f. Pass 'plain' for this parameter to return a simple unicode representation. -PasteEvent objects representing multple keystrokes in very rapid succession +:py:class:`~curtsies.events.PasteEvent` objects representing multiple +keystrokes in very rapid succession (typically because the user pasted in text, but possibly because they typed -two keys simultaneously. How many bytes must occur together to trigger such -an event is customizable via the paste_threshold argument to the ``Input()`` -- by default it's one greater than the maximum possible keypress +two keys simultaneously). How many bytes must occur together to trigger such +an event is customizable via the ``paste_threshold`` argument to the :py:class:`~curtsies.Input` +object - by default it's one greater than the maximum possible keypress length in bytes. -If ``sigint_event=True`` is passed to ``Input()``, SIGINT signals from the -operating system (which usually raise a KeyboardInterrupt exception) -will be returned as ``SigIntEvent()`` instances. +If ``sigint_event=True`` is passed to :py:class:`~curtsies.Input`, ``SIGINT`` signals from the +operating system (which usually raise a ``KeyboardInterrupt`` exception) +will be returned as :py:class:`~curtsies.events.SigIntEvent` instances. To set a timeout on the blocking get, treat it like a generator and call ``.send(timeout)``. The call will return None if no event is available. Input - Events ============== -.. automodule:: curtsies.events To see what a given keypress is called (what unicode string is returned by ``Terminal.next()``), try ``python -m curtsies.events`` and play around. -Events returned by :py:class:`~curtsies.input.Input` fall into two categories: -instances of subclasses of :class:`curtsies.event.Event` and +Events returned by :py:class:`~curtsies.Input` fall into two categories: +instances of subclasses of :py:class:`~curtsies.events.Event` and Keypress strings. Input - Event Objects --------------------- +.. autoclass:: curtsies.events.Event + .. autoclass:: curtsies.events.SigIntEvent .. autoclass:: curtsies.events.PasteEvent diff --git a/docs/index.rst b/docs/index.rst index 38b267c..f12c428 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -18,7 +18,7 @@ Such grids of characters can be efficiently rendered to the terminal in alternat (no scrollback history, like ``Vim``, ``top`` etc.) by :py:class:`~curtsies.FullscreenWindow` objects or to the normal history-preserving screen by :py:class:`~curtsies.CursorAwareWindow` objects. User keyboard input events like pressing the up arrow key are detected by an -:py:class:`~curtsies.input.Input` object. See the :doc:`quickstart` to get started using +:py:class:`~curtsies.Input` object. See the :doc:`quickstart` to get started using all of these classes. .. toctree:: From 1747f91b9984b058c0357f4f321b329c4ac86c76 Mon Sep 17 00:00:00 2001 From: Dan Puttick Date: Fri, 4 Nov 2016 16:33:12 -0400 Subject: [PATCH 031/302] Small changes to Input.rst --- docs/Input.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/Input.rst b/docs/Input.rst index dba5c9d..760b901 100644 --- a/docs/Input.rst +++ b/docs/Input.rst @@ -30,7 +30,7 @@ that no keypress or other event occured within the timeout. Key events are unicode strings, but sometimes event objects (see :class:`~curtsies.events.Event`) are returned instead. -Built-in events signal :py:class:`~curtsies.events.SigIntEvent` +Built-in events signal :py:class:`~curtsies.events.SigIntEvent` events from the OS and :py:class:`~curtsies.events.PasteEvent` consisting of multiple keypress events if reporting of these types of events was enabled in instantiation of the :py:class:`~curtsies.Input` object. @@ -76,10 +76,10 @@ keypress events, which is ``'curtsies'`` by default. For compatibility with curses code, you can use ``'curses'`` names, but note that curses doesn't have nice key names for many key combinations so you'll be putting up with names like ``u'\xe1'`` for -option-j and ``'\x86'`` for ctrl-option-f. -Pass 'plain' for this parameter to return a simple unicode representation. +``option-j`` and ``'\x86'`` for ``ctrl-option-f``. +Pass ``'plain'`` for this parameter to return a simple unicode representation. -:py:class:`~curtsies.events.PasteEvent` objects representing multiple +:py:class:`~curtsies.events.PasteEvent` objects representing multiple keystrokes in very rapid succession (typically because the user pasted in text, but possibly because they typed two keys simultaneously). How many bytes must occur together to trigger such @@ -92,7 +92,7 @@ operating system (which usually raise a ``KeyboardInterrupt`` exception) will be returned as :py:class:`~curtsies.events.SigIntEvent` instances. To set a timeout on the blocking get, treat it like a generator and call -``.send(timeout)``. The call will return None if no event is available. +``.send(timeout)``. The call will return ``None`` if no event is available. Input - Events ============== From aefc1852722bcce2c42b8612c7f0f9afd2593d74 Mon Sep 17 00:00:00 2001 From: Dan Puttick Date: Fri, 4 Nov 2016 15:56:12 -0400 Subject: [PATCH 032/302] Fixed typo in Input.rst --- docs/Input.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Input.rst b/docs/Input.rst index 760b901..7e9ab34 100644 --- a/docs/Input.rst +++ b/docs/Input.rst @@ -133,7 +133,7 @@ Keypress events are Unicode strings in both Python 2 and 3 like: Likely points of confusion for keypress strings: * Enter is ```` -* Modern meta (the escape-prepending version) key is ```` while control and shift keys are is ```` (note the + vs -) +* Modern meta (the escape-prepending version) key is ```` while control and shift keys are ```` (note the + vs -) * Letter keys are capitalized in ```` while they are lowercase in ```` (this should be fixed in the next api-breaking release) * Some special characters lose their special names when used with modifier keys, for example: From ba8ca438ea7f073c5cca80fd573e038252e60d5f Mon Sep 17 00:00:00 2001 From: Dan Puttick Date: Fri, 4 Nov 2016 16:38:05 -0400 Subject: [PATCH 033/302] Missed one last FmtStr --- docs/FSArray.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/FSArray.rst b/docs/FSArray.rst index 1e35427..1091592 100644 --- a/docs/FSArray.rst +++ b/docs/FSArray.rst @@ -54,7 +54,7 @@ FSArray - Using An array like shown above might be repeatedly constructed and rendered with a :py:mod:`curtsies.window` object. -Slicing works like it does with a :py:class:`~curtsies.formatstring.FmtStr`, but in two dimensions. +Slicing works like it does with a :py:class:`~curtsies.FmtStr`, but in two dimensions. :py:class:`~curtsies.FSArray` are *mutable*, so array assignment syntax can be used for natural compositing as in the above exaple. From dca85345e23e1a17ce9803b9b0b4d0e53d3d84cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20Sz=C3=B6ll=C5=91si?= Date: Sun, 2 Apr 2017 17:19:56 +0200 Subject: [PATCH 034/302] Fix: multiple ANSI styling tokens --- curtsies/escseqparse.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/curtsies/escseqparse.py b/curtsies/escseqparse.py index 00b5ba4..6fd84f7 100644 --- a/curtsies/escseqparse.py +++ b/curtsies/escseqparse.py @@ -40,7 +40,7 @@ def parse(s): try: tok = token_type(token) if tok: - stuff.append(tok) + stuff.extend(tok) except ValueError: raise ValueError("Can't parse escape sequence: %r %r %r %r" % (s, repr(front), token, repr(rest))) if not rest: @@ -97,18 +97,22 @@ def token_type(info): if info['command'] == 'm': # The default action for ESC[m is to act like ESC[0m # Ref: https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_codes - value, = info['numbers'] if len(info['numbers']) else [0] - if value in FG_NUMBER_TO_COLOR: return {'fg':FG_NUMBER_TO_COLOR[value]} - if value in BG_NUMBER_TO_COLOR: return {'bg':BG_NUMBER_TO_COLOR[value]} - if value in NUMBER_TO_STYLE: return {NUMBER_TO_STYLE[value]:True} - if value == RESET_ALL: return dict(dict((k, None) for k in STYLES), **{'fg':None, 'bg':None}) - if value == RESET_FG: return {'fg':None} - if value == RESET_BG: return {'bg':None} - + values = info['numbers'] if len(info['numbers']) else [0] + tokens = [] + for value in values: + if value in FG_NUMBER_TO_COLOR: tokens.append({'fg':FG_NUMBER_TO_COLOR[value]}) + if value in BG_NUMBER_TO_COLOR: tokens.append({'bg':BG_NUMBER_TO_COLOR[value]}) + if value in NUMBER_TO_STYLE: tokens.append({NUMBER_TO_STYLE[value]:True}) + if value == RESET_ALL: tokens.append(dict(dict((k, None) for k in STYLES), **{'fg':None, 'bg':None})) + if value == RESET_FG: tokens.append({'fg':None}) + if value == RESET_BG: tokens.append({'bg':None}) + + if tokens: + return tokens + else: + raise ValueError("Can't parse escape seq %r" % info) elif info['command'] == 'H': # fix for bpython #76 - return {} - - raise ValueError("Can't parse escape seq %r" % info) + return [{}] if __name__ == '__main__': import doctest; doctest.testmod() From 9893d7c890f139199ef9729204bd5fe175e77b8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20Sz=C3=B6ll=C5=91si?= Date: Sun, 2 Apr 2017 18:49:47 +0200 Subject: [PATCH 035/302] Add test for bold colored string. --- tests/test_fmtstr.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_fmtstr.py b/tests/test_fmtstr.py index a3ee9f7..8e2d918 100644 --- a/tests/test_fmtstr.py +++ b/tests/test_fmtstr.py @@ -58,6 +58,7 @@ def test_simple_escapes(self): self.assertEqual(str(fmtstr('\x1b[43mhello\x1b[49m')), '\x1b[43mhello\x1b[49m') self.assertEqual(str(fmtstr('\x1b[43mhello\x1b[0m')), '\x1b[43mhello\x1b[49m') self.assertEqual(str(fmtstr('\x1b[43mhello')), '\x1b[43mhello\x1b[49m') + self.assertEqual(str(fmtstr('\x1b[32;1mhello')), '\x1b[32m\x1b[1mhello\x1b[0m\x1b[39m') self.assertEqual(str(fmtstr('\x1b[33m\x1b[43mhello\x1b[0m')), '\x1b[33m\x1b[43mhello\x1b[49m\x1b[39m') From e1d56974a8aee91db9ffc8be16f9b1b359fae1f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20Sz=C3=B6ll=C5=91si?= Date: Mon, 3 Apr 2017 21:26:00 +0200 Subject: [PATCH 036/302] Handling of unsupported SGR codes --- curtsies/formatstring.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/curtsies/formatstring.py b/curtsies/formatstring.py index 6ecf31d..88fa834 100644 --- a/curtsies/formatstring.py +++ b/curtsies/formatstring.py @@ -93,8 +93,10 @@ def color_str(self): s = self.s for k, v in sorted(self.atts.items()): # (self.atts sorted for the sake of always acting the same.) - assert k in xforms, "XXX Do we actually get cases like this?" - if v is False: + if k not in xforms: + # Unsupported SGR code + continue + elif v is False: continue elif v is True: s = xforms[k](s) From 23f3565743e6349e4d7dcc0f1e85d84860841c00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20Sz=C3=B6ll=C5=91si?= Date: Mon, 3 Apr 2017 21:42:07 +0200 Subject: [PATCH 037/302] Add tests for e1d5697 --- tests/test_fmtstr.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_fmtstr.py b/tests/test_fmtstr.py index 8e2d918..f49f605 100644 --- a/tests/test_fmtstr.py +++ b/tests/test_fmtstr.py @@ -59,6 +59,8 @@ def test_simple_escapes(self): self.assertEqual(str(fmtstr('\x1b[43mhello\x1b[0m')), '\x1b[43mhello\x1b[49m') self.assertEqual(str(fmtstr('\x1b[43mhello')), '\x1b[43mhello\x1b[49m') self.assertEqual(str(fmtstr('\x1b[32;1mhello')), '\x1b[32m\x1b[1mhello\x1b[0m\x1b[39m') + self.assertEqual(str(fmtstr('\x1b[2mhello')), 'hello') + self.assertEqual(str(fmtstr('\x1b[32;2mhello')), '\x1b[32mhello\x1b[39m') self.assertEqual(str(fmtstr('\x1b[33m\x1b[43mhello\x1b[0m')), '\x1b[33m\x1b[43mhello\x1b[49m\x1b[39m') From 6a8d5a0e161691acf54c2ca39c1d0fe812f40dc9 Mon Sep 17 00:00:00 2001 From: Tom Ballinger Date: Tue, 4 Apr 2017 07:36:40 -0700 Subject: [PATCH 038/302] F3 as submitted by cool-RR --- curtsies/curtsieskeys.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/curtsies/curtsieskeys.py b/curtsies/curtsieskeys.py index d1a60e3..b0a0e3f 100644 --- a/curtsies/curtsieskeys.py +++ b/curtsies/curtsieskeys.py @@ -104,4 +104,6 @@ (b"\x1b[OF", u''), # end (1) (b"\x1b[OH", u''), # home (7) + # reported by cool-RR + (b"\x1b[[C", u''), ]) From c84f717720447f44377d2ff6b41766c542600fc1 Mon Sep 17 00:00:00 2001 From: Tom Ballinger Date: Tue, 4 Apr 2017 07:50:21 -0700 Subject: [PATCH 039/302] fix gameexample Python 3 error --- examples/gameexample.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/examples/gameexample.py b/examples/gameexample.py index 3c2a587..22f204d 100644 --- a/examples/gameexample.py +++ b/examples/gameexample.py @@ -6,6 +6,16 @@ from curtsies import FullscreenWindow, Input, FSArray from curtsies.fmtfuncs import red, bold, green, on_blue, yellow, on_red +PY2 = sys.version_info[0] == 2 + + +def unicode_str(obj): + """Get unicode str in Python 2 or 3""" + if PY2: + return str(obj).decode('utf8') + return str(obj) + + class Entity(object): def __init__(self, display, x, y, speed=1): self.display = display @@ -70,7 +80,7 @@ def tick(self): self.turn += 1 if self.turn % 20 == 0: self.player.speed = max(1, self.player.speed - 1) - self.player.display = on_blue(green(bold(str(self.player.speed).decode('utf8')))) + self.player.display = on_blue(green(bold(unicode_str(self.player.speed)))) def get_array(self): a = FSArray(self.height, self.width) From d34767c43003695e503754e6af508a5ecfa3215a Mon Sep 17 00:00:00 2001 From: Tom Ballinger Date: Tue, 4 Apr 2017 08:04:21 -0700 Subject: [PATCH 040/302] add rest of F keys for cool-RR --- curtsies/curtsieskeys.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/curtsies/curtsieskeys.py b/curtsies/curtsieskeys.py index b0a0e3f..61bf495 100644 --- a/curtsies/curtsieskeys.py +++ b/curtsies/curtsieskeys.py @@ -105,5 +105,10 @@ (b"\x1b[OH", u''), # home (7) # reported by cool-RR + (b"\x1b[[A", u''), + (b"\x1b[[B", u''), (b"\x1b[[C", u''), - ]) + (b"\x1b[[D", u''), + (b"\x1b[[E", u''), + # cool-RR says the rest were good, see issue #99 +]) From 70e3b2824df7aa40e181cefc89aba1add790c843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20Sz=C3=B6ll=C5=91si?= Date: Sun, 28 May 2017 16:27:04 +0200 Subject: [PATCH 041/302] Implement splitlines method of FmtStr class --- curtsies/formatstring.py | 7 +++++++ docs/FmtStr.rst | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/curtsies/formatstring.py b/curtsies/formatstring.py index 88fa834..39f595b 100644 --- a/curtsies/formatstring.py +++ b/curtsies/formatstring.py @@ -292,6 +292,13 @@ def split(self, sep=None, maxsplit=None, regex=False): [0] + [m.end() for m in matches], [m.start() for m in matches] + [len(s)])] + def splitlines(self, keepends=False): + """Return a list of lines, split on newline characters, + include line boundaries, if keepends is true.""" + lines = self.split('\n') + return [line+'\n' for line in lines] if keepends else ( + lines if lines[-1] else lines[:-1]) + # proxying to the string via __getattr__ is insufficient # because we shouldn't drop foreground or formatting info def ljust(self, width, fillchar=None): diff --git a/docs/FmtStr.rst b/docs/FmtStr.rst index 25168c0..7aed784 100644 --- a/docs/FmtStr.rst +++ b/docs/FmtStr.rst @@ -196,7 +196,7 @@ FmtStr - API Docs .. autofunction:: curtsies.fmtstr .. autoclass:: curtsies.FmtStr - :members: width, splice, copy_with_new_atts, copy_with_new_str, join, split, width_aware_slice + :members: width, splice, copy_with_new_atts, copy_with_new_str, join, split, splitlines, width_aware_slice .. automodule:: curtsies.fmtfuncs From 4b5f7fc66ed13e4d53fc6ad6a3b9ca919b0649ac Mon Sep 17 00:00:00 2001 From: Tom Ballinger Date: Tue, 25 Jul 2017 17:13:50 -0700 Subject: [PATCH 042/302] expect terminal tests skipped on Travis to fail --- tests/test_terminal.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/test_terminal.py b/tests/test_terminal.py index f053dd6..767ede2 100644 --- a/tests/test_terminal.py +++ b/tests/test_terminal.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import functools import locale import os import sys @@ -23,7 +24,7 @@ IS_TRAVIS = bool(os.environ.get("TRAVIS")) try: - from unittest import skipUnless, skipIf + from unittest import skipUnless, skipIf, skipFailure except ImportError: def skipUnless(condition, reason): if condition: @@ -36,6 +37,21 @@ def skipIf(condition, reason): else: return lambda x: x + import nose + + def skipFailure(reason): + def dec(func): + @functools.wraps(func) + def inner(*args, **kwargs): + try: + func(*args, **kwargs) + except Exception: + raise nose.SkipTest + else: + raise AssertionError('Failure expected') + return inner + return dec + class FakeStdin(StringIO): encoding = 'ascii' @@ -146,17 +162,20 @@ def setUp(self): blessings.Terminal.height = 3 blessings.Terminal.width = 6 + @skipFailure("This isn't passing locally for me anymore :/") def test_render(self): with self.window: self.assertEqual(self.window.top_usable_row, 0) self.window.render_to_terminal([u'hi', u'there']) self.assertEqual(self.screen.display, [u'hi ', u'there ', u' ']) + @skipFailure("This isn't passing locally for me anymore :/") def test_cursor_position(self): with self.window: self.window.render_to_terminal([u'hi', u'there'], cursor_pos=(2, 4)) self.assertEqual(self.window.get_cursor_position(), (2, 4)) + @skipFailure("This isn't passing locally for me anymore :/") def test_inital_cursor_position(self): self.screen.cursor.y += 1 @@ -186,6 +205,7 @@ def setUp(self): def extra_bytes_callback(self, bytes): self.extra_bytes.append(bytes) + @skipFailure("This isn't passing locally for me anymore :/") def test_report_extra_bytes(self): with self.window: pass # should have triggered getting initial cursor position From 77912b540a842706f1bf5e2e66ca210673552b86 Mon Sep 17 00:00:00 2001 From: Tom Ballinger Date: Tue, 25 Jul 2017 17:43:32 -0700 Subject: [PATCH 043/302] don't use ctrl-c handlers if not on main thread --- curtsies/input.py | 14 ++++++++++---- tests/test_input.py | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/curtsies/input.py b/curtsies/input.py index 3a439f4..ca73f64 100644 --- a/curtsies/input.py +++ b/curtsies/input.py @@ -5,6 +5,7 @@ import select import sys import termios +import threading import time import tty @@ -23,6 +24,10 @@ # the paste logic that reads more data as needed might not work. +def is_main_thread(): + return isinstance(threading.current_thread(), threading._MainThread) + + class ReplacedSigIntHandler(object): def __init__(self, handler): self.handler = handler @@ -38,7 +43,8 @@ def __exit__(self, type, value, traceback): class Input(object): """Keypress and control event generator""" def __init__(self, in_stream=None, keynames='curtsies', - paste_threshold=events.MAX_KEYPRESS_SIZE+1, sigint_event=False): + paste_threshold=events.MAX_KEYPRESS_SIZE+1, sigint_event=False, + signint_callback_provider=None): """Returns an Input instance. Args: @@ -79,13 +85,13 @@ def __enter__(self): attrs[-1][VDSUSP] = 0 termios.tcsetattr(self.in_stream, termios.TCSANOW, attrs) - if self.sigint_event: + if self.sigint_event and is_main_thread(): self.orig_sigint_handler = signal.getsignal(signal.SIGINT) signal.signal(signal.SIGINT, self.sigint_handler) return self def __exit__(self, type, value, traceback): - if self.sigint_event: + if self.sigint_event and is_main_thread(): signal.signal(signal.SIGINT, self.orig_sigint_handler) termios.tcsetattr(self.in_stream, termios.TCSANOW, self.original_stty) @@ -146,7 +152,7 @@ def _wait_for_read_ready_or_timeout(self, timeout): def send(self, timeout=None): """Returns an event or None if no events occur before timeout.""" - if self.sigint_event: + if self.sigint_event and is_main_thread(): with ReplacedSigIntHandler(self.sigint_handler): return self._send(timeout) else: diff --git a/tests/test_input.py b/tests/test_input.py index f895df7..88a6ae7 100644 --- a/tests/test_input.py +++ b/tests/test_input.py @@ -132,3 +132,22 @@ def send_sigint(): self.assertEqual(type(inp.send(1)), events.SigIntEvent) self.assertEqual(inp.send(0), None) t.join() + + def test_create_in_thread_with_sigint_event(self): + def create(): + inp = Input(sigint_event=True) + + t = threading.Thread(target=create) + t.start() + t.join() + + def test_use_in_thread_with_sigint_event(self): + inp = Input(sigint_event=True) + def use(): + with inp: + pass + + t = threading.Thread(target=use) + t.start() + t.join() + From d94c70cce8f6e406176d2d74b038e13985089067 Mon Sep 17 00:00:00 2001 From: "cody.j.b.scott@gmail.com" Date: Tue, 10 Oct 2017 09:12:15 -0400 Subject: [PATCH 044/302] Add Python3 classifier Motivated by caniusepython3[0] reporting curtsies doesn't support Python3. ```shell $ caniusepython3 --projects curtsies Finding and checking dependencies ... You need 1 project to transition to Python 3. Of that 1 project, 1 has no direct dependencies blocking its transition: curtsies ``` 0: https://pypi.python.org/pypi/caniusepython3 --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 6975992..8eaf27e 100644 --- a/setup.py +++ b/setup.py @@ -54,5 +54,6 @@ def get_long_description(): 'License :: OSI Approved :: MIT License', 'Operating System :: POSIX', 'Programming Language :: Python', + 'Programming Language :: Python :: 3', ], zip_safe=False) From f677bee31fd9674524d8a3833d26fd065ae634e7 Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Wed, 11 Oct 2017 21:31:32 +0100 Subject: [PATCH 045/302] Include escape sequence for Ctrl-DELETE in curtsieskeys --- curtsies/curtsieskeys.py | 1 + 1 file changed, 1 insertion(+) diff --git a/curtsies/curtsieskeys.py b/curtsies/curtsieskeys.py index 61bf495..dd69255 100644 --- a/curtsies/curtsieskeys.py +++ b/curtsies/curtsieskeys.py @@ -89,6 +89,7 @@ (b"\x1b[2~", u''), # insert (0) (b"\x1b[3~", u''), # delete (.), "Execute" + (b"\x1b[3;5~", u''), # not fixing for back compat. # (b"\x1b[4~", u''), # select - (b"\x1b[5~", u''), # pgup (9) - (b"\x1b[6~", u''), # pgdown (3) - (b"\x1b[7~", u''), # home - (b"\x1b[8~", u''), # end - (b"\x1b[OA", u''), # up (8) - (b"\x1b[OB", u''), # down (2) - (b"\x1b[OC", u''), # right (6) - (b"\x1b[OD", u''), # left (4) - (b"\x1b[OF", u''), # end (1) - (b"\x1b[OH", u''), # home (7) + (b"\x1b[5~", ''), # pgup (9) + (b"\x1b[6~", ''), # pgdown (3) + (b"\x1b[7~", ''), # home + (b"\x1b[8~", ''), # end + (b"\x1b[OA", ''), # up (8) + (b"\x1b[OB", ''), # down (2) + (b"\x1b[OC", ''), # right (6) + (b"\x1b[OD", ''), # left (4) + (b"\x1b[OF", ''), # end (1) + (b"\x1b[OH", ''), # home (7) # reported by cool-RR - (b"\x1b[[A", u''), - (b"\x1b[[B", u''), - (b"\x1b[[C", u''), - (b"\x1b[[D", u''), - (b"\x1b[[E", u''), + (b"\x1b[[A", ''), + (b"\x1b[[B", ''), + (b"\x1b[[C", ''), + (b"\x1b[[D", ''), + (b"\x1b[[E", ''), # cool-RR says the rest were good, see issue #99 #reported by alethiophile see issue #119 - (b"\x1b[1;3C", u''), #alt-right - (b"\x1b[1;3B", u''), #alt-down - (b"\x1b[1;3D", u''), #alt-left - (b"\x1b[1;3A", u''), #alt-up - (b"\x1b[5;3~", u''), #alt-pageup - (b"\x1b[6;3~", u''), #alt-pagedown - (b"\x1b[1;3H", u''), #alt-home - (b"\x1b[1;3F", u''), #alt-end - (b"\x1b[1;2C", u''), - (b"\x1b[1;2B", u''), - (b"\x1b[1;2D", u''), - (b"\x1b[1;2A", u''), - (b"\x1b[3;2~", u''), - (b"\x1b[5;2~", u''), - (b"\x1b[6;2~", u''), - (b"\x1b[1;2H", u''), - (b"\x1b[1;2F", u''), + (b"\x1b[1;3C", ''), #alt-right + (b"\x1b[1;3B", ''), #alt-down + (b"\x1b[1;3D", ''), #alt-left + (b"\x1b[1;3A", ''), #alt-up + (b"\x1b[5;3~", ''), #alt-pageup + (b"\x1b[6;3~", ''), #alt-pagedown + (b"\x1b[1;3H", ''), #alt-home + (b"\x1b[1;3F", ''), #alt-end + (b"\x1b[1;2C", ''), + (b"\x1b[1;2B", ''), + (b"\x1b[1;2D", ''), + (b"\x1b[1;2A", ''), + (b"\x1b[3;2~", ''), + (b"\x1b[5;2~", ''), + (b"\x1b[6;2~", ''), + (b"\x1b[1;2H", ''), + (b"\x1b[1;2F", ''), #end of keys reported by alethiophile ]) diff --git a/curtsies/escseqparse.py b/curtsies/escseqparse.py index 20790f2..6ab7084 100644 --- a/curtsies/escseqparse.py +++ b/curtsies/escseqparse.py @@ -144,7 +144,7 @@ def token_type(info): if value in FG_NUMBER_TO_COLOR: tokens.append({'fg':FG_NUMBER_TO_COLOR[value]}) if value in BG_NUMBER_TO_COLOR: tokens.append({'bg':BG_NUMBER_TO_COLOR[value]}) if value in NUMBER_TO_STYLE: tokens.append({NUMBER_TO_STYLE[value]:True}) - if value == RESET_ALL: tokens.append(dict(dict((k, None) for k in STYLES), **{'fg':None, 'bg':None})) + if value == RESET_ALL: tokens.append(dict({k: None for k in STYLES}, **{'fg':None, 'bg':None})) if value == RESET_FG: tokens.append({'fg':None}) if value == RESET_BG: tokens.append({'bg':None}) # fmt: on @@ -164,4 +164,4 @@ def token_type(info): doctest.testmod() # print(peel_off_esc_code('stuff')) # print(peel_off_esc_code('Amore')) - print((repr(parse("stuff is the bestyay")))) + print(repr(parse("stuff is the bestyay"))) diff --git a/curtsies/events.py b/curtsies/events.py index 5ac2a18..f8a01a9 100644 --- a/curtsies/events.py +++ b/curtsies/events.py @@ -20,68 +20,68 @@ CURTSIES_NAMES = {} -control_chars = dict( - (chr_byte(i), u"" % chr(i + 0x60)) for i in range(0x00, 0x1B) -) +control_chars = { + chr_byte(i): "" % chr(i + 0x60) for i in range(0x00, 0x1B) +} CURTSIES_NAMES.update(control_chars) for i in range(0x00, 0x80): - CURTSIES_NAMES[b"\x1b" + chr_byte(i)] = u"" % chr(i) + CURTSIES_NAMES[b"\x1b" + chr_byte(i)] = "" % chr(i) for i in range(0x00, 0x1B): # Overwrite the control keys with better labels - CURTSIES_NAMES[b"\x1b" + chr_byte(i)] = u"" % chr(i + 0x40) + CURTSIES_NAMES[b"\x1b" + chr_byte(i)] = "" % chr(i + 0x40) for i in range(0x00, 0x80): - CURTSIES_NAMES[chr_byte(i + 0x80)] = u"" % chr(i) + CURTSIES_NAMES[chr_byte(i + 0x80)] = "" % chr(i) for i in range(0x00, 0x1B): # Overwrite the control keys with better labels - CURTSIES_NAMES[chr_byte(i + 0x80)] = u"" % chr(i + 0x40) + CURTSIES_NAMES[chr_byte(i + 0x80)] = "" % chr(i + 0x40) from .curtsieskeys import CURTSIES_NAMES as special_curtsies_names CURTSIES_NAMES.update(special_curtsies_names) CURSES_NAMES = {} -CURSES_NAMES[b"\x1bOP"] = u"KEY_F(1)" -CURSES_NAMES[b"\x1bOQ"] = u"KEY_F(2)" -CURSES_NAMES[b"\x1bOR"] = u"KEY_F(3)" -CURSES_NAMES[b"\x1bOS"] = u"KEY_F(4)" -CURSES_NAMES[b"\x1b[15~"] = u"KEY_F(5)" -CURSES_NAMES[b"\x1b[17~"] = u"KEY_F(6)" -CURSES_NAMES[b"\x1b[18~"] = u"KEY_F(7)" -CURSES_NAMES[b"\x1b[19~"] = u"KEY_F(8)" -CURSES_NAMES[b"\x1b[20~"] = u"KEY_F(9)" -CURSES_NAMES[b"\x1b[21~"] = u"KEY_F(10)" -CURSES_NAMES[b"\x1b[23~"] = u"KEY_F(11)" -CURSES_NAMES[b"\x1b[24~"] = u"KEY_F(12)" +CURSES_NAMES[b"\x1bOP"] = "KEY_F(1)" +CURSES_NAMES[b"\x1bOQ"] = "KEY_F(2)" +CURSES_NAMES[b"\x1bOR"] = "KEY_F(3)" +CURSES_NAMES[b"\x1bOS"] = "KEY_F(4)" +CURSES_NAMES[b"\x1b[15~"] = "KEY_F(5)" +CURSES_NAMES[b"\x1b[17~"] = "KEY_F(6)" +CURSES_NAMES[b"\x1b[18~"] = "KEY_F(7)" +CURSES_NAMES[b"\x1b[19~"] = "KEY_F(8)" +CURSES_NAMES[b"\x1b[20~"] = "KEY_F(9)" +CURSES_NAMES[b"\x1b[21~"] = "KEY_F(10)" +CURSES_NAMES[b"\x1b[23~"] = "KEY_F(11)" +CURSES_NAMES[b"\x1b[24~"] = "KEY_F(12)" # see bpython #626 -CURSES_NAMES[b"\x1b[11~"] = u"KEY_F(1)" -CURSES_NAMES[b"\x1b[12~"] = u"KEY_F(2)" -CURSES_NAMES[b"\x1b[13~"] = u"KEY_F(3)" -CURSES_NAMES[b"\x1b[14~"] = u"KEY_F(4)" - -CURSES_NAMES[b"\x1b[A"] = u"KEY_UP" -CURSES_NAMES[b"\x1b[B"] = u"KEY_DOWN" -CURSES_NAMES[b"\x1b[C"] = u"KEY_RIGHT" -CURSES_NAMES[b"\x1b[D"] = u"KEY_LEFT" -CURSES_NAMES[b"\x1b[F"] = u"KEY_END" # https://github.com/bpython/bpython/issues/490 -CURSES_NAMES[b"\x1b[H"] = u"KEY_HOME" # https://github.com/bpython/bpython/issues/490 -CURSES_NAMES[b"\x08"] = u"KEY_BACKSPACE" -CURSES_NAMES[b"\x1b[Z"] = u"KEY_BTAB" +CURSES_NAMES[b"\x1b[11~"] = "KEY_F(1)" +CURSES_NAMES[b"\x1b[12~"] = "KEY_F(2)" +CURSES_NAMES[b"\x1b[13~"] = "KEY_F(3)" +CURSES_NAMES[b"\x1b[14~"] = "KEY_F(4)" + +CURSES_NAMES[b"\x1b[A"] = "KEY_UP" +CURSES_NAMES[b"\x1b[B"] = "KEY_DOWN" +CURSES_NAMES[b"\x1b[C"] = "KEY_RIGHT" +CURSES_NAMES[b"\x1b[D"] = "KEY_LEFT" +CURSES_NAMES[b"\x1b[F"] = "KEY_END" # https://github.com/bpython/bpython/issues/490 +CURSES_NAMES[b"\x1b[H"] = "KEY_HOME" # https://github.com/bpython/bpython/issues/490 +CURSES_NAMES[b"\x08"] = "KEY_BACKSPACE" +CURSES_NAMES[b"\x1b[Z"] = "KEY_BTAB" # see curtsies #78 - taken from https://github.com/jquast/blessed/blob/e9ad7b85dfcbbba49010ab8c13e3a5920d81b010/blessed/keyboard.py#L409 # fmt: off -CURSES_NAMES[b'\x1b[1~'] = u'KEY_FIND' # find -CURSES_NAMES[b'\x1b[2~'] = u'KEY_IC' # insert (0) -CURSES_NAMES[b'\x1b[3~'] = u'KEY_DC' # delete (.), "Execute" -CURSES_NAMES[b'\x1b[4~'] = u'KEY_SELECT' # select -CURSES_NAMES[b'\x1b[5~'] = u'KEY_PPAGE' # pgup (9) -CURSES_NAMES[b'\x1b[6~'] = u'KEY_NPAGE' # pgdown (3) -CURSES_NAMES[b'\x1b[7~'] = u'KEY_HOME' # home -CURSES_NAMES[b'\x1b[8~'] = u'KEY_END' # end -CURSES_NAMES[b'\x1b[OA'] = u'KEY_UP' # up (8) -CURSES_NAMES[b'\x1b[OB'] = u'KEY_DOWN' # down (2) -CURSES_NAMES[b'\x1b[OC'] = u'KEY_RIGHT' # right (6) -CURSES_NAMES[b'\x1b[OD'] = u'KEY_LEFT' # left (4) -CURSES_NAMES[b'\x1b[OF'] = u'KEY_END' # end (1) -CURSES_NAMES[b'\x1b[OH'] = u'KEY_HOME' # home (7) +CURSES_NAMES[b'\x1b[1~'] = 'KEY_FIND' # find +CURSES_NAMES[b'\x1b[2~'] = 'KEY_IC' # insert (0) +CURSES_NAMES[b'\x1b[3~'] = 'KEY_DC' # delete (.), "Execute" +CURSES_NAMES[b'\x1b[4~'] = 'KEY_SELECT' # select +CURSES_NAMES[b'\x1b[5~'] = 'KEY_PPAGE' # pgup (9) +CURSES_NAMES[b'\x1b[6~'] = 'KEY_NPAGE' # pgdown (3) +CURSES_NAMES[b'\x1b[7~'] = 'KEY_HOME' # home +CURSES_NAMES[b'\x1b[8~'] = 'KEY_END' # end +CURSES_NAMES[b'\x1b[OA'] = 'KEY_UP' # up (8) +CURSES_NAMES[b'\x1b[OB'] = 'KEY_DOWN' # down (2) +CURSES_NAMES[b'\x1b[OC'] = 'KEY_RIGHT' # right (6) +CURSES_NAMES[b'\x1b[OD'] = 'KEY_LEFT' # left (4) +CURSES_NAMES[b'\x1b[OF'] = 'KEY_END' # end (1) +CURSES_NAMES[b'\x1b[OH'] = 'KEY_HOME' # home (7) # fmt: on KEYMAP_PREFIXES = set() @@ -237,14 +237,14 @@ def key_name(): except UnicodeDecodeError: # this sequence can't be decoded with this encoding, so we need to represent the bytes if len(seq) == 1: - return u"x%02X" % ord(seq) + return "x%02X" % ord(seq) # TODO figure out a better thing to return here else: raise NotImplementedError( "are multibyte unnameable sequences possible?" ) - return u"bytes: " + u"-".join( - u"x%02X" % ord(seq[i : i + 1]) for i in range(len(seq)) + return "bytes: " + "-".join( + "x%02X" % ord(seq[i : i + 1]) for i in range(len(seq)) ) # TODO if this isn't possible, return multiple meta keys as a paste event if paste events enabled elif keynames == "curtsies": @@ -309,8 +309,8 @@ def pp_event(seq): return str(seq) # Get the original sequence back if seq is a pretty name already - rev_curses = dict((v, k) for k, v in CURSES_NAMES.items()) - rev_curtsies = dict((v, k) for k, v in CURTSIES_NAMES.items()) + rev_curses = {v: k for k, v in CURSES_NAMES.items()} + rev_curtsies = {v: k for k, v in CURTSIES_NAMES.items()} bytes_seq = None # type: Optional[bytes] if seq in rev_curses: bytes_seq = rev_curses[seq] @@ -349,7 +349,7 @@ def ask_what_they_pressed(seq, Normal): if r.lower().strip() == "ok": break while True: - print("Press the key that produced %r again please" % (seq,)) + print(f"Press the key that produced {seq!r} again please") retry = os.read(sys.stdin.fileno(), 1000) if seq == retry: break @@ -357,7 +357,7 @@ def ask_what_they_pressed(seq, Normal): with Normal: name = raw_input("Describe in English what key you pressed: ") f = open("keylog.txt", "a") - f.write("%r is called %s\n" % (seq, name)) + f.write(f"{seq!r} is called {name}\n") f.close() print( "Thanks! Please open an issue at https://github.com/bpython/curtsies/issues" diff --git a/curtsies/formatstring.py b/curtsies/formatstring.py index ac4358d..675ea6d 100644 --- a/curtsies/formatstring.py +++ b/curtsies/formatstring.py @@ -47,7 +47,7 @@ } # type: Mapping[Text, Callable[[Text], Text]] two_arg_xforms = { - 'fg' : lambda s, v: '%s%s%s' % (seq(v), s, seq(RESET_FG)), + 'fg' : lambda s, v: '{}{}{}'.format(seq(v), s, seq(RESET_FG)), 'bg' : lambda s, v: seq(v)+s+seq(RESET_BG), } # type: Mapping[Text, Callable[[Text, int], Text]] @@ -82,7 +82,7 @@ def stable_format_dict(d): Does not work for dicts with unicode strings as values.""" inner = ', '.join('{}: {}'.format(repr(k)[1:] - if repr(k).startswith(u"u'") or repr(k).startswith(u'u"') + if repr(k).startswith("u'") or repr(k).startswith('u"') else repr(k), v) for k, v in sorted(d.items())) @@ -182,7 +182,7 @@ def pp_att(att): if att == 'fg': return FG_NUMBER_TO_COLOR[self.atts[att]] elif att == 'bg': return 'on_' + BG_NUMBER_TO_COLOR[self.atts[att]] else: return att - atts_out = dict((k, v) for (k, v) in self.atts.items() if v) + atts_out = {k: v for (k, v) in self.atts.items() if v} return (''.join(pp_att(att)+'(' for att in sorted(atts_out)) + (repr(self.s) if PY3 else repr(self.s)[1:]) + ')'*len(atts_out)) @@ -229,7 +229,7 @@ def request(self, max_width): width = 0 start_offset = i = self.internal_offset - replacement_char = u' ' + replacement_char = ' ' while True: w = wcswidth(s[i]) @@ -301,9 +301,9 @@ def from_str(cls, s): if isinstance(x, dict): cur_fmt.update(x) elif isinstance(x, unicode): - atts = parse_args((), dict((k, v) + atts = parse_args((), {k: v for k, v in cur_fmt.items() - if v is not None)) + if v is not None}) chunks.append(Chunk(x, atts=atts)) else: raise Exception("logic error") @@ -315,8 +315,8 @@ def copy_with_new_str(self, new_str): # type: (Text) -> FmtStr """Copies the current FmtStr's attributes while changing its string.""" # What to do when there are multiple Chunks with conflicting atts? - old_atts = dict((att, value) for bfs in self.chunks - for (att, value) in bfs.atts.items()) + old_atts = {att: value for bfs in self.chunks + for (att, value) in bfs.atts.items()} return FmtStr(Chunk(new_str, old_atts)) def setitem(self, startindex, fs): @@ -453,7 +453,7 @@ def ljust(self, width, fillchar=None): to_add = ' ' * (width - len(self.s)) shared = self.shared_atts if 'bg' in shared: - return self + fmtstr(to_add, bg=shared[str('bg')]) if to_add else self + return self + fmtstr(to_add, bg=shared['bg']) if to_add else self else: uniform = self.new_with_atts_removed('bg') return uniform + fmtstr(to_add, **self.shared_atts) if to_add else uniform @@ -469,7 +469,7 @@ def rjust(self, width, fillchar=None): to_add = ' ' * (width - len(self.s)) shared = self.shared_atts if 'bg' in shared: - return fmtstr(to_add, bg=shared[str('bg')]) + self if to_add else self + return fmtstr(to_add, bg=shared['bg']) + self if to_add else self else: uniform = self.new_with_atts_removed('bg') return fmtstr(to_add, **self.shared_atts) + uniform if to_add else uniform @@ -487,7 +487,7 @@ def __unicode__(self): def __str__(self): if self._str is not None: return self._str - self._str = str('').join(str(fs) for fs in self.chunks) + self._str = ''.join(str(fs) for fs in self.chunks) return self._str def __len__(self): @@ -538,7 +538,7 @@ def __add__(self, other): elif isinstance(other, (bytes, unicode)): return FmtStr(*(self.chunks + [Chunk(other)])) else: - raise TypeError('Can\'t add %r and %r' % (self, other)) + raise TypeError(f'Can\'t add {self!r} and {other!r}') def __radd__(self, other): # type: (Union[FmtStr, Text]) -> FmtStr @@ -580,7 +580,7 @@ def new_with_atts_removed(self, *attributes): def __getattr__(self, att): # thanks to @aerenchyma/@jczett if not hasattr(self.s, att): - raise AttributeError("No attribute %r" % (att,)) + raise AttributeError(f"No attribute {att!r}") @no_type_check def func_help(*args, **kwargs): result = getattr(self.s, att)(*args, **kwargs) @@ -727,7 +727,7 @@ def interval_overlap(a, b, x, y): assert False -def width_aware_slice(s, start, end, replacement_char=u' '): +def width_aware_slice(s, start, end, replacement_char=' '): # type: (Text, int, int, Text) -> Text """ >>> width_aware_slice(u'a\uff25iou', 0, 2)[1] == u' ' @@ -802,7 +802,7 @@ def normalize_slice(length, index): raise NotImplementedError("You can't use steps with slicing yet") if is_int: if index.start < 0 or index.start > length: - raise IndexError("index out of bounds: %r for length %s" % (index, length)) + raise IndexError(f"index out of bounds: {index!r} for length {length}") return index def parse_args(args, kwargs): @@ -857,5 +857,5 @@ def fmtstr(string, *args, **kwargs): elif isinstance(string, (bytes, unicode)): string = FmtStr.from_str(string) else: - raise ValueError("Bad Args: %r (of type %s), %r, %r" % (string, type(string), args, kwargs)) + raise ValueError("Bad Args: {!r} (of type {}), {!r}, {!r}".format(string, type(string), args, kwargs)) return string.copy_with_new_atts(**atts) diff --git a/curtsies/window.py b/curtsies/window.py index 32222af..61d7d98 100644 --- a/curtsies/window.py +++ b/curtsies/window.py @@ -503,9 +503,9 @@ def render_to_terminal(self, array, cursor_pos=(0, 0)): self.top_usable_row -= 1 else: offscreen_scrolls += 1 - current_lines_by_row = dict( - (k - 1, v) for k, v in current_lines_by_row.items() - ) + current_lines_by_row = { + k - 1: v for k, v in current_lines_by_row.items() + } logger.debug("new top_usable_row: %d" % self.top_usable_row) # since scrolling moves the cursor self.write(self.t.move(height - 1, 0)) @@ -576,7 +576,7 @@ def main(): w = FullscreenWindow(sys.stdout) rows, columns = w.t.height, w.t.width with w: - a = [fmtstr(((".row%r." % (row,)) * rows)[:columns]) for row in range(rows)] + a = [fmtstr(((f".row{row!r}.") * rows)[:columns]) for row in range(rows)] w.render_to_terminal(a) diff --git a/examples/demo_fullscreen_with_input.py b/examples/demo_fullscreen_with_input.py index a9755a9..6bd832c 100644 --- a/examples/demo_fullscreen_with_input.py +++ b/examples/demo_fullscreen_with_input.py @@ -10,16 +10,16 @@ def fullscreen_winch_with_input(): print('this should be just off-screen') w = FullscreenWindow(sys.stdout) def sigwinch_handler(signum, frame): - print('sigwinch! Changed from %r to %r' % ((rows, columns), (w.height, w.width))) + print('sigwinch! Changed from {!r} to {!r}'.format((rows, columns), (w.height, w.width))) signal.signal(signal.SIGWINCH, sigwinch_handler) with w: with Cbreak(sys.stdin): for e in input.Input(): rows, columns = w.height, w.width - a = [fmtstr((('.%sx%s.%r.' % (rows, columns, e)) * rows)[:columns]) for row in range(rows)] + a = [fmtstr(((f'.{rows}x{columns}.{e!r}.') * rows)[:columns]) for row in range(rows)] w.render_to_terminal(a) - if e == u'': + if e == '': break if __name__ == '__main__': diff --git a/examples/demo_input_paste.py b/examples/demo_input_paste.py index 1108ce5..0c24106 100644 --- a/examples/demo_input_paste.py +++ b/examples/demo_input_paste.py @@ -11,7 +11,7 @@ def paste(): for e in input_generator: print(repr(e)) - if e == u'': + if e == '': break time.sleep(1) diff --git a/examples/demo_input_timeout.py b/examples/demo_input_timeout.py index c1779a6..2b9d9b2 100644 --- a/examples/demo_input_timeout.py +++ b/examples/demo_input_timeout.py @@ -13,7 +13,7 @@ def main(): print(repr(input_generator.send(.2))) for e in input_generator: print(repr(e)) - if e == u'': + if e == '': break if __name__ == '__main__': main() diff --git a/examples/demo_scrolling.py b/examples/demo_scrolling.py index bbc7a9b..fe6ea73 100644 --- a/examples/demo_scrolling.py +++ b/examples/demo_scrolling.py @@ -17,7 +17,7 @@ def sigwinch_handler(signum, frame): dy = w.get_cursor_vertical_diff() old_rows, old_columns = rows, columns rows, columns = w.height, w.width - print('sigwinch! Changed from %r to %r' % ((old_rows, old_columns), (rows, columns))) + print('sigwinch! Changed from {!r} to {!r}'.format((old_rows, old_columns), (rows, columns))) print('cursor moved %d lines down' % dy) w.write(w.t.move_up) w.write(w.t.move_up) @@ -25,9 +25,9 @@ def sigwinch_handler(signum, frame): with w: for e in input.Input(): rows, columns = w.height, w.width - a = [fmtstr(((u'.%sx%s.' % (rows, columns)) * rows)[:columns]) for row in range(rows)] + a = [fmtstr(((f'.{rows}x{columns}.') * rows)[:columns]) for row in range(rows)] w.render_to_terminal(a) - if e == u'': + if e == '': break if __name__ == '__main__': cursor_winch() diff --git a/examples/demo_window.py b/examples/demo_window.py index 8ce5920..a163f38 100644 --- a/examples/demo_window.py +++ b/examples/demo_window.py @@ -36,7 +36,7 @@ def array_size_test(window): w.scroll_down() elif isinstance(c, events.WindowChangeEvent): a = w.array_from_text("window just changed to %d rows and %d columns" % (c.rows, c.columns)) - elif c == u'': # allows exit without keyboard interrupt + elif c == '': # allows exit without keyboard interrupt break elif c == '\x0c': # ctrl-L [w.write('\n') for _ in range(rows)] diff --git a/examples/fps.py b/examples/fps.py index d13d443..ce24896 100644 --- a/examples/fps.py +++ b/examples/fps.py @@ -33,7 +33,7 @@ def realtime(fps=15): while when < time.time(): when += dt schedule_next_frame(when) - elif e == u'': + elif e == '': break else: world.process_event(e) diff --git a/examples/gameexample.py b/examples/gameexample.py index 17744c2..67df9ff 100644 --- a/examples/gameexample.py +++ b/examples/gameexample.py @@ -40,8 +40,8 @@ def __init__(self, width, height): self.width = width self.height = height n = 5 - self.player = Entity(on_blue(green(bold(u'5'))), width // 2, height // 2 - 2, speed=5) - self.npcs = [Entity(on_blue(red(u'X')), i * width // (n * 2), j * height // (n * 2)) + self.player = Entity(on_blue(green(bold('5'))), width // 2, height // 2 - 2, speed=5) + self.npcs = [Entity(on_blue(red('X')), i * width // (n * 2), j * height // (n * 2)) for i in range(1, 2*n, 2) for j in range(1, 2*n, 2)] self.turn = 0 diff --git a/examples/snake.py b/examples/snake.py index 1b0d695..922554d 100644 --- a/examples/snake.py +++ b/examples/snake.py @@ -40,8 +40,8 @@ def render(self): a = FSArray(self.height, self.width) for row, col in self.snake_parts: - a[row, col] = u'x' - a[self.apple[0], self.apple[1]] = u'o' + a[row, col] = 'x' + a[self.apple[0], self.apple[1]] = 'o' return a def tick(self, e): diff --git a/examples/tttplaybitboard.py b/examples/tttplaybitboard.py index d1f8c1e..a29d278 100755 --- a/examples/tttplaybitboard.py +++ b/examples/tttplaybitboard.py @@ -15,8 +15,8 @@ from curtsies import FullscreenWindow, Input, fsarray def main(argv): - pool = dict((name[:-5], play) for name, play in globals().items() - if name.endswith('_play')) + pool = {name[:-5]: play for name, play in globals().items() + if name.endswith('_play')} faceoff = [human_play, max_play] try: if len(argv) == 1: diff --git a/tests/test_configfile_keynames.py b/tests/test_configfile_keynames.py index 32e814c..2a09059 100644 --- a/tests/test_configfile_keynames.py +++ b/tests/test_configfile_keynames.py @@ -18,12 +18,12 @@ def config(self, mapping, curtsies): ) def test_simple(self): - self.config("M-m", u"") - self.config("M-m", u"") - self.config("C-m", u"") - self.config("C-[", u"") - self.config("C-\\", u"") - self.config("C-]", u"") - self.config("C-^", u"") - self.config("C-_", u"") # ??? for bpython compatibility - self.config("F1", u"") + self.config("M-m", "") + self.config("M-m", "") + self.config("C-m", "") + self.config("C-[", "") + self.config("C-\\", "") + self.config("C-]", "") + self.config("C-^", "") + self.config("C-_", "") # ??? for bpython compatibility + self.config("F1", "") diff --git a/tests/test_events.py b/tests/test_events.py index fd10cc6..0f1ab3a 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -13,14 +13,14 @@ def test_simple(self): def test_helpers(self): self.assertEqual(events.chr_byte(97), b"a") - self.assertEqual(events.chr_uni(97), u"a") + self.assertEqual(events.chr_uni(97), "a") class TestCurtsiesNames(unittest.TestCase): def spot_check(self): - self.assertEqual(events.CURTSIES_NAMES[b"\x1b\x08"], u"") - self.assertEqual(events.CURTSIES_NAMES[b"\x00"], u"") - self.assertEqual(events.CURTSIES_NAMES[b"\xea"], u"") + self.assertEqual(events.CURTSIES_NAMES[b"\x1b\x08"], "") + self.assertEqual(events.CURTSIES_NAMES[b"\x00"], "") + self.assertEqual(events.CURTSIES_NAMES[b"\xea"], "") def test_all_values_unicode(self): for seq, e in events.CURTSIES_NAMES.items(): @@ -28,7 +28,7 @@ def test_all_values_unicode(self): def test_all_keys_bytes(self): for seq, e in events.CURTSIES_NAMES.items(): - self.assertEqual(type(e), type(u"")) + self.assertEqual(type(e), type("")) class TestDecodable(unittest.TestCase): @@ -42,19 +42,19 @@ def test_utf8_full(self): get_utf_full = partial( events.get_key, encoding="utf-8", keynames="curtsies", full=True ) - self.assertEqual(get_utf_full([b"h"]), u"h") - self.assertEqual(get_utf_full([b"\x1b", b"["]), u"") + self.assertEqual(get_utf_full([b"h"]), "h") + self.assertEqual(get_utf_full([b"\x1b", b"["]), "") self.assertRaises(UnicodeDecodeError, get_utf_full, [b"\xfe\xfe"]) - self.assertRaises(ValueError, get_utf_full, u"a") + self.assertRaises(ValueError, get_utf_full, "a") def test_utf8(self): get_utf = partial( events.get_key, encoding="utf-8", keynames="curtsies", full=False ) - self.assertEqual(get_utf([b"h"]), u"h") + self.assertEqual(get_utf([b"h"]), "h") self.assertEqual(get_utf([b"\x1b", b"["]), None) self.assertEqual(get_utf([b"\xe2"]), None) - self.assertRaises(ValueError, get_utf, u"a") + self.assertRaises(ValueError, get_utf, "a") def test_multibyte_utf8(self): get_utf = partial( @@ -62,11 +62,11 @@ def test_multibyte_utf8(self): ) self.assertEqual(get_utf([b"\xc3"]), None) self.assertEqual(get_utf([b"\xe2"]), None) - self.assertEqual(get_utf([b"\xe2"], full=True), u"") - self.assertEqual(get_utf([b"\xc3", b"\x9f"]), u"ß") + self.assertEqual(get_utf([b"\xe2"], full=True), "") + self.assertEqual(get_utf([b"\xc3", b"\x9f"]), "ß") self.assertEqual(get_utf([b"\xe2"]), None) self.assertEqual(get_utf([b"\xe2", b"\x88"]), None) - self.assertEqual(get_utf([b"\xe2", b"\x88", b"\x82"]), u"∂") + self.assertEqual(get_utf([b"\xe2", b"\x88", b"\x82"]), "∂") def test_sequences_without_names(self): get_utf = partial( @@ -88,15 +88,15 @@ def test_full(self): get_ascii_full = partial( events.get_key, encoding="ascii", keynames="curtsies", full=True ) - self.assertEqual(get_ascii_full([b"a"]), u"a") - self.assertEqual(get_ascii_full([b"\xe1"]), u"") - self.assertEqual(get_ascii_full([b"\xe1"], keynames="curses"), u"xE1") + self.assertEqual(get_ascii_full([b"a"]), "a") + self.assertEqual(get_ascii_full([b"\xe1"]), "") + self.assertEqual(get_ascii_full([b"\xe1"], keynames="curses"), "xE1") def test_simple(self): get_ascii_full = partial(events.get_key, encoding="ascii", keynames="curtsies") - self.assertEqual(get_ascii_full([b"a"]), u"a") - self.assertEqual(get_ascii_full([b"\xe1"]), u"") - self.assertEqual(get_ascii_full([b"\xe1"], keynames="curses"), u"xE1") + self.assertEqual(get_ascii_full([b"a"]), "a") + self.assertEqual(get_ascii_full([b"\xe1"]), "") + self.assertEqual(get_ascii_full([b"\xe1"], keynames="curses"), "xE1") class TestUnknownEncoding(unittest.TestCase): @@ -105,7 +105,7 @@ def test_simple(self): self.assertEqual(get_utf16([b"a"]), None) self.assertEqual(get_utf16([b"a"], full=True), None) self.assertEqual(get_utf16([b"\xe1"]), None) - self.assertEqual(get_utf16([b"\xe1"], full=True), u"") + self.assertEqual(get_utf16([b"\xe1"], full=True), "") class TestSpecialKeys(unittest.TestCase): @@ -113,10 +113,10 @@ def test_simple(self): seq = [b"\x1b", b"[", b"1", b";", b"9", b"C"] self.assertEqual( [events.get_key(seq[:i], encoding="utf8") for i in range(1, len(seq) + 1)], - [None, None, None, None, None, u""], + [None, None, None, None, None, ""], ) class TestPPEvent(unittest.TestCase): def test(self): - self.assertEqual(events.pp_event(u"a"), "a") + self.assertEqual(events.pp_event("a"), "a") diff --git a/tests/test_input.py b/tests/test_input.py index 4109ffa..d058235 100644 --- a/tests/test_input.py +++ b/tests/test_input.py @@ -48,7 +48,7 @@ def test_iter(self): def test_send(self): inp = Input() inp.unprocessed_bytes = [b"a"] - self.assertEqual(inp.send("nonsensical value"), u"a") + self.assertEqual(inp.send("nonsensical value"), "a") def test_send_nonblocking_no_event(self): inp = Input() @@ -81,7 +81,7 @@ def side_effect(): r = inp.send(0) self.assertEqual(type(r), events.PasteEvent) - self.assertEqual(r.events, [u"a"] * n) + self.assertEqual(r.events, ["a"] * n) def test_event_trigger(self): inp = Input() diff --git a/tests/test_terminal.py b/tests/test_terminal.py index a2e7503..20fc602 100644 --- a/tests/test_terminal.py +++ b/tests/test_terminal.py @@ -107,7 +107,7 @@ def inner(*args, **flags): to.write(" ") to.write( ", ".join( - "{0}: {1}".format(name, repr(arg)) for name, arg in flags.items() + "{}: {}".format(name, repr(arg)) for name, arg in flags.items() ) ) to.write(os.linesep) From 7350b57e7121c2710bbbfdffe5fbd41087b6a62a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 21 Dec 2020 13:58:06 +0100 Subject: [PATCH 120/302] Use open --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c213668..55c0e02 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ def version(): return ast.parse(line).body[0].value.s def long_description(): - with io.open("readme.md", encoding="utf-8") as f: + with open("readme.md", encoding="utf-8") as f: return f.read() setup( From c703c15dbe19a3371e3a7af7d73b88e0513d0c05 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 21 Dec 2020 14:08:19 +0100 Subject: [PATCH 121/302] Update string handling --- examples/gameexample.py | 11 +---------- tests/test_events.py | 2 +- tests/test_fmtstr.py | 16 ++-------------- 3 files changed, 4 insertions(+), 25 deletions(-) diff --git a/examples/gameexample.py b/examples/gameexample.py index 67df9ff..6852000 100644 --- a/examples/gameexample.py +++ b/examples/gameexample.py @@ -4,15 +4,6 @@ from curtsies import FullscreenWindow, Input, FSArray from curtsies.fmtfuncs import red, bold, green, on_blue, yellow, on_red -PY2 = sys.version_info[0] == 2 - - -def unicode_str(obj): - """Get unicode str in Python 2 or 3""" - if PY2: - return str(obj).decode('utf8') - return str(obj) - class Entity: def __init__(self, display, x, y, speed=1): @@ -78,7 +69,7 @@ def tick(self): self.turn += 1 if self.turn % 20 == 0: self.player.speed = max(1, self.player.speed - 1) - self.player.display = on_blue(green(bold(unicode_str(self.player.speed)))) + self.player.display = on_blue(green(bold(str(self.player.speed)))) def get_array(self): a = FSArray(self.height, self.width) diff --git a/tests/test_events.py b/tests/test_events.py index 0f1ab3a..691ce2a 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -28,7 +28,7 @@ def test_all_values_unicode(self): def test_all_keys_bytes(self): for seq, e in events.CURTSIES_NAMES.items(): - self.assertEqual(type(e), type("")) + self.assertEqual(type(e), str) class TestDecodable(unittest.TestCase): diff --git a/tests/test_fmtstr.py b/tests/test_fmtstr.py index 31ed0ab..1564920 100644 --- a/tests/test_fmtstr.py +++ b/tests/test_fmtstr.py @@ -32,20 +32,11 @@ def skip(f): PY2 = sys.version_info[0] == 2 -try: - unicode = unicode -except: - unicode = str def repr_without_leading_u(s): - assert isinstance(s, type("")) - if PY2: - r = repr(s) - assert r[0] == "u" - return r[1:] - else: - return repr(s) + assert isinstance(s, str) + return repr(s) class TestFmtStrInitialization(unittest.TestCase): @@ -381,19 +372,16 @@ def test_simple_composition(self): class TestUnicode(unittest.TestCase): def test_output_type(self): self.assertEqual(type(str(fmtstr("hello", "blue"))), str) - self.assertEqual(type(unicode(fmtstr("hello", "blue"))), unicode) def test_normal_chars(self): fmtstr("a", "blue") str(fmtstr("a", "blue")) - unicode(fmtstr("a", "blue")) self.assertTrue(True) def test_funny_chars(self): fmtstr("⁇", "blue") str(Chunk("⁇", {"fg": "blue"})) str(fmtstr("⁇", "blue")) - unicode(fmtstr("⁇", "blue")) self.assertTrue(True) def test_right_sequence_in_py3(self): From c870678c8db7c4a2dc8185840382b8a4269d3808 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 21 Dec 2020 14:08:36 +0100 Subject: [PATCH 122/302] Remove Python 2 workarounds --- curtsies/events.py | 16 +++------- curtsies/formatstring.py | 55 ++++++++++++----------------------- curtsies/formatstringarray.py | 10 ++----- curtsies/input.py | 33 ++++++--------------- tests/test_fmtstr.py | 16 ++-------- tests/test_input.py | 15 +--------- tests/test_terminal.py | 51 +++++++------------------------- tests/test_window.py | 17 ++--------- 8 files changed, 49 insertions(+), 164 deletions(-) diff --git a/curtsies/events.py b/curtsies/events.py index f8a01a9..b21fe8b 100644 --- a/curtsies/events.py +++ b/curtsies/events.py @@ -7,16 +7,8 @@ from typing import Text, Optional, List, Union -PY3 = sys.version_info[0] >= 3 - -if PY3: - raw_input = input - unicode = str - chr_byte = lambda i: chr(i).encode("latin-1") - chr_uni = chr -else: - chr_byte = chr - chr_uni = lambda i: chr(i).decode("latin-1") +chr_byte = lambda i: chr(i).encode("latin-1") +chr_uni = chr CURTSIES_NAMES = {} @@ -345,7 +337,7 @@ def ask_what_they_pressed(seq, Normal): print("Unidentified character sequence!") with Normal: while True: - r = raw_input("type 'ok' to prove you're not pounding keys ") + r = input("type 'ok' to prove you're not pounding keys ") if r.lower().strip() == "ok": break while True: @@ -355,7 +347,7 @@ def ask_what_they_pressed(seq, Normal): break print("nope, that wasn't it") with Normal: - name = raw_input("Describe in English what key you pressed: ") + name = input("Describe in English what key you pressed: ") f = open("keylog.txt", "a") f.write(f"{seq!r} is called {name}\n") f.close() diff --git a/curtsies/formatstring.py b/curtsies/formatstring.py index 675ea6d..684361f 100644 --- a/curtsies/formatstring.py +++ b/curtsies/formatstring.py @@ -33,10 +33,6 @@ RESET_ALL, RESET_BG, RESET_FG, seq) -PY3 = sys.version_info[0] >= 3 - -if PY3: - unicode = str one_arg_xforms = { 'bold' : lambda s: seq(STYLES['bold']) +s+seq(RESET_ALL), @@ -97,7 +93,7 @@ def __init__(self, string, atts=None): # type: (Text, Mapping[str, Union[int, bool]]) -> None if atts is None: atts = {} - if not isinstance(string, unicode): + if not isinstance(string, str): raise ValueError("unicode string required, got %r" % string) self._s = string # type: Text self._atts = FrozenDict(atts) @@ -144,11 +140,11 @@ def color_str(self): s = two_arg_xforms[k](s, v) return s - def __unicode__(self): + def __str__(self): # type: () -> Text value = self.color_str if isinstance(value, bytes): - return value.decode('utf8', 'replace') + return value.decode("utf8", "replace") return value def __eq__(self, other): @@ -161,12 +157,6 @@ def __hash__(self): # type: () -> int return hash(self.s, self.atts) - if PY3: - __str__ = __unicode__ - else: - def __str__(self): - return unicode(self).encode('utf8') - def __repr__(self): # type: () -> str return 'Chunk({s}{sep}{atts})'.format( @@ -268,7 +258,6 @@ def __init__(self, *components): self.chunks = list(components) # caching these leads to a significant speedup - self._str = None self._unicode = None # type: Optional[Text] self._len = None # type: Optional[int] self._s = None # type: Optional[Text] @@ -300,7 +289,7 @@ def from_str(cls, s): for x in tokens_and_strings: if isinstance(x, dict): cur_fmt.update(x) - elif isinstance(x, unicode): + elif isinstance(x, str): atts = parse_args((), {k: v for k, v in cur_fmt.items() if v is not None}) @@ -407,6 +396,8 @@ def join(self, iterable): before = self.chunks if isinstance(s, FmtStr): chunks.extend(s.chunks) + elif isinstance(s, (bytes, str)): + chunks.extend(fmtstr(s).chunks) # TODO just make a chunk directly elif isinstance(s, (bytes, unicode)): chunks.extend(fmtstr(s).chunks) #TODO just make a chunk directly else: @@ -474,22 +465,13 @@ def rjust(self, width, fillchar=None): uniform = self.new_with_atts_removed('bg') return fmtstr(to_add, **self.shared_atts) + uniform if to_add else uniform - def __unicode__(self): + def __str__(self): # type: () -> Text if self._unicode is not None: return self._unicode - self._unicode = ''.join(unicode(fs) for fs in self.chunks) + self._unicode = "".join(str(fs) for fs in self.chunks) return self._unicode - if PY3: - __str__ = __unicode__ - else: - def __str__(self): - if self._str is not None: - return self._str - self._str = ''.join(str(fs) for fs in self.chunks) - return self._str - def __len__(self): # type: () -> int if self._len is not None: @@ -523,7 +505,7 @@ def __repr__(self): def __eq__(self, other): # type: (Any) -> bool - if isinstance(other, (unicode, bytes, FmtStr)): + if isinstance(other, (str, bytes, FmtStr)): return str(self) == str(other) return False @@ -535,7 +517,7 @@ def __add__(self, other): # type: (Union[FmtStr, Text]) -> FmtStr if isinstance(other, FmtStr): return FmtStr(*(self.chunks + other.chunks)) - elif isinstance(other, (bytes, unicode)): + elif isinstance(other, (bytes, str)): return FmtStr(*(self.chunks + [Chunk(other)])) else: raise TypeError(f'Can\'t add {self!r} and {other!r}') @@ -544,7 +526,7 @@ def __radd__(self, other): # type: (Union[FmtStr, Text]) -> FmtStr if isinstance(other, FmtStr): return FmtStr(*(x for x in (other.chunks + self.chunks))) - elif isinstance(other, (bytes, unicode)): + elif isinstance(other, (bytes, str)): return FmtStr(*(x for x in ([Chunk(other)] + self.chunks))) else: raise TypeError('Can\'t add those') @@ -581,15 +563,17 @@ def __getattr__(self, att): # thanks to @aerenchyma/@jczett if not hasattr(self.s, att): raise AttributeError(f"No attribute {att!r}") + @no_type_check def func_help(*args, **kwargs): result = getattr(self.s, att)(*args, **kwargs) - if isinstance(result, (bytes, unicode)): + if isinstance(result, (bytes, str)): return fmtstr(result, **self.shared_atts) elif isinstance(result, list): return [fmtstr(x, **self.shared_atts) for x in result] else: - return result + return result + return func_help @property @@ -806,15 +790,14 @@ def normalize_slice(length, index): return index def parse_args(args, kwargs): - # type: (Tuple[Union[bytes, unicode], ...], MutableMapping[str, Union[int, bool, str]]) -> Mapping[str, Union[int, bool]] + # type: (Tuple[Union[bytes, str], ...], MutableMapping[str, Union[int, bool, str]]) -> Mapping[str, Union[int, bool]] """Returns a kwargs dictionary by turning args into kwargs""" if 'style' in kwargs: args += (cast(str, kwargs['style']),) del kwargs['style'] for arg in args: - if PY3: - arg = cast(str, arg) - if not isinstance(arg, (bytes, unicode)): + arg = cast(str, arg) + if not isinstance(arg, (bytes, str)): raise ValueError("args must be strings:" + repr(args)) if arg.lower() in FG_COLORS: if 'fg' in kwargs: raise ValueError("fg specified twice") @@ -854,7 +837,7 @@ def fmtstr(string, *args, **kwargs): atts = parse_args(args, kwargs) if isinstance(string, FmtStr): pass - elif isinstance(string, (bytes, unicode)): + elif isinstance(string, (bytes, str)): string = FmtStr.from_str(string) else: raise ValueError("Bad Args: {!r} (of type {}), {!r}, {!r}".format(string, type(string), args, kwargs)) diff --git a/curtsies/formatstringarray.py b/curtsies/formatstringarray.py index fec29eb..35c704b 100644 --- a/curtsies/formatstringarray.py +++ b/curtsies/formatstringarray.py @@ -41,13 +41,7 @@ no_type_check, ) -PY3 = sys.version_info[0] >= 3 - -if PY3: - unicode = str - -actualize = str if PY3 else unicode - +actualize = str logger = logging.getLogger(__name__) # TODO check that strings used in arrays don't have tabs or spaces in them! @@ -165,7 +159,7 @@ def __setitem__(self, slicetuple, value): logger.debug("slice: %r", slicetuple) if isinstance(slicetuple, slice): rowslice, colslice = slicetuple, slice(None) - if isinstance(value, (bytes, unicode)): + if isinstance(value, (bytes, str)): raise ValueError( "if slice is 2D, value must be 2D as in of list type []" ) diff --git a/curtsies/input.py b/curtsies/input.py index dbd31a0..37bada4 100644 --- a/curtsies/input.py +++ b/curtsies/input.py @@ -19,8 +19,6 @@ from typing import Callable, Type, TextIO, Optional, List, Union, Text, cast, Tuple, Any from types import TracebackType, FrameType -PY3 = sys.version_info[0] >= 3 - READ_SIZE = 1024 assert READ_SIZE >= events.MAX_KEYPRESS_SIZE # if a keypress could require more bytes than we read to be identified, @@ -137,12 +135,10 @@ def __iter__(self): # type: () -> Input return self - def next(self): + def __next__(self): # type: () -> Union[None, Text, events.Event] return self.send(None) - __next__ = next - def unget_bytes(self, string): # type: (bytes) -> None """Adds bytes to be internal buffer to be read @@ -291,26 +287,15 @@ def _nonblocking_read(self): # type: () -> int """Returns the number of characters read and adds them to self.unprocessed_bytes""" with Nonblocking(self.in_stream): - if PY3: - try: - data = os.read(self.in_stream.fileno(), READ_SIZE) - except BlockingIOError: - return 0 - if data: - self.unprocessed_bytes.extend( - data[i : i + 1] for i in range(len(data)) - ) - return len(data) - else: - return 0 + try: + data = os.read(self.in_stream.fileno(), READ_SIZE) + except BlockingIOError: + return 0 + if data: + self.unprocessed_bytes.extend(data[i : i + 1] for i in range(len(data))) + return len(data) else: - try: - data = os.read(self.in_stream.fileno(), READ_SIZE) - except OSError: - return 0 - else: - self.unprocessed_bytes.extend(data) - return len(data) + return 0 def event_trigger(self, event_type): # type: (Type[events.Event]) -> Callable diff --git a/tests/test_fmtstr.py b/tests/test_fmtstr.py index 1564920..a420e3c 100644 --- a/tests/test_fmtstr.py +++ b/tests/test_fmtstr.py @@ -22,16 +22,7 @@ from curtsies.termformatconstants import FG_COLORS from curtsies.formatstringarray import fsarray, FSArray, FormatStringTest -try: - from unittest import skip -except ImportError: - - def skip(f): - return lambda self: None - - -PY2 = sys.version_info[0] == 2 - +from unittest import skip def repr_without_leading_u(s): @@ -514,10 +505,7 @@ def test_char_width_aware_slice(self): class TestChunk(unittest.TestCase): def test_repr(self): c = Chunk("a", {"fg": 32}) - if PY2: - self.assertEqual(repr(c), """Chunk(u'a', {'fg': 32})""") - else: - self.assertEqual(repr(c), """Chunk('a', {'fg': 32})""") + self.assertEqual(repr(c), """Chunk('a', {'fg': 32})""") class TestChunkSplitter(unittest.TestCase): diff --git a/tests/test_input.py b/tests/test_input.py index d058235..1348682 100644 --- a/tests/test_input.py +++ b/tests/test_input.py @@ -5,20 +5,7 @@ import time import unittest from unittest.mock import Mock - -try: - from unittest import skip, skipUnless -except ImportError: - - def skip(f): - return lambda self: None - - def skipUnless(condition, reason): - if condition: - return lambda x: x - else: - return lambda x: None - +from unittest import skip, skipUnless from curtsies import events from curtsies.input import Input diff --git a/tests/test_terminal.py b/tests/test_terminal.py index 20fc602..7be12f6 100644 --- a/tests/test_terminal.py +++ b/tests/test_terminal.py @@ -4,10 +4,8 @@ import sys import unittest -if sys.version_info[0] == 3: - from io import StringIO -else: - from StringIO import StringIO +from io import StringIO +from unittest import skipUnless, skipIf, expectedFailure import blessings import pyte @@ -21,39 +19,6 @@ # (and still reporting isatty as True) IS_TRAVIS = bool(os.environ.get("TRAVIS")) -try: - from unittest import skipUnless, skipIf, skipFailure -except ImportError: - - def skipUnless(condition, reason): - if condition: - return lambda x: x - else: - return lambda x: None - - def skipIf(condition, reason): - if condition: - return lambda x: None - else: - return lambda x: x - - import nose - - def skipFailure(reason): - def dec(func): - @functools.wraps(func) - def inner(*args, **kwargs): - try: - func(*args, **kwargs) - except Exception: - raise nose.SkipTest - else: - raise AssertionError("Failure expected") - - return inner - - return dec - class FakeStdin(StringIO): encoding = "ascii" @@ -178,20 +143,23 @@ def setUp(self): blessings.Terminal.height = 3 blessings.Terminal.width = 6 - @skipFailure("This isn't passing locally for me anymore :/") + # This isn't passing locally for me anymore :/ + @expectedFailure def test_render(self): with self.window: self.assertEqual(self.window.top_usable_row, 0) self.window.render_to_terminal(["hi", "there"]) self.assertEqual(self.screen.display, ["hi ", "there ", " "]) - @skipFailure("This isn't passing locally for me anymore :/") + # This isn't passing locally for me anymore :/ + @expectedFailure def test_cursor_position(self): with self.window: self.window.render_to_terminal(["hi", "there"], cursor_pos=(2, 4)) self.assertEqual(self.window.get_cursor_position(), (2, 4)) - @skipFailure("This isn't passing locally for me anymore :/") + # This isn't passing locally for me anymore :/ + @expectedFailure def test_inital_cursor_position(self): self.screen.cursor.y += 1 @@ -223,7 +191,8 @@ def setUp(self): def extra_bytes_callback(self, bytes): self.extra_bytes.append(bytes) - @skipFailure("This isn't passing locally for me anymore :/") + # This isn't passing locally for me anymore :/ + @expectedFailure def test_report_extra_bytes(self): with self.window: pass # should have triggered getting initial cursor position diff --git a/tests/test_window.py b/tests/test_window.py index bea304f..d4e7dff 100644 --- a/tests/test_window.py +++ b/tests/test_window.py @@ -2,21 +2,8 @@ import sys from curtsies.window import BaseWindow, FullscreenWindow, CursorAwareWindow - -if sys.version_info[0] == 3: - from io import StringIO -else: - from cStringIO import StringIO - -try: - from unittest import skipIf -except ImportError: - - def skipIf(condition, reason): - if condition: - return lambda x: x - else: - return lambda x: None +from io import StringIO +from unittest import skipIf fds_closed = not sys.stdin.isatty() or not sys.stdout.isatty() From 93a89fdcbfbddbc7f6d472560089d75f5aa1bae2 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 21 Dec 2020 17:23:31 +0100 Subject: [PATCH 123/302] Use fstrings and .format --- curtsies/formatstringarray.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/curtsies/formatstringarray.py b/curtsies/formatstringarray.py index 35c704b..84c4af9 100644 --- a/curtsies/formatstringarray.py +++ b/curtsies/formatstringarray.py @@ -210,18 +210,18 @@ def __setitem__(self, slicetuple, value): + self.rows[rowslice.stop :] ) msg = ( - "You are trying to fit this value {0} into the region {1}: {2}".format( + "You are trying to fit this value {} into the region {}: {}".format( fmtstr("".join(value), bg="cyan"), fmtstr("").join(grid_value), "\n ".join(grid_fsarray[x] for x in range(len(self.rows))), ) ) raise ValueError( - """Error you are trying to replace a region of {0} rows by {1} - columns for and area of {2} with a value of len {3}. The value + """Error you are trying to replace a region of {} rows by {} + columns for and area of {} with a value of len {}. The value used to replace the region must equal the area of the region replace. - {4}""".format( + {}""".format( rowslice.stop - rowslice.start, colslice.stop - colslice.start, area, @@ -253,11 +253,11 @@ def diff(cls, a, b, ignore_formatting=False): def underline(x): # type: (Text) -> Text - return "\x1b[4m%s\x1b[0m" % (x,) + return f"\x1b[4m{x}\x1b[0m" def blink(x): # type: (Text) -> Text - return "\x1b[5m%s\x1b[0m" % (x,) + return f"\x1b[5m{x}\x1b[0m" a_rows = [] b_rows = [] @@ -308,13 +308,13 @@ def assertFSArraysEqual(self, a, b): self.assertEqual( (a.width, b.height), (a.width, b.height), - "fsarray dimensions do not match: %s %s" % (a.shape, b.shape), + f"fsarray dimensions do not match: {a.shape} {b.shape}", ) for i, (a_row, b_row) in enumerate(zip(a, b)): self.assertEqual( a_row, b_row, - "FSArrays differ first on line %s:\n%s" % (i, FSArray.diff(a, b)), + "FSArrays differ first on line {}:\n{}".format(i, FSArray.diff(a, b)), ) def assertFSArraysEqualIgnoringFormatting(self, a, b): From 9df867eef646ad22e31f8f306396396d52248d66 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 21 Dec 2020 17:24:08 +0100 Subject: [PATCH 124/302] Remove useless parens --- examples/tron.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/tron.py b/examples/tron.py index 6045f63..d6bd19e 100644 --- a/examples/tron.py +++ b/examples/tron.py @@ -165,14 +165,14 @@ def do_introduction(window): def mainloop(window, p2_bot=False): p1_attrs = { - "appearance": on_blue((cyan("1"))), + "appearance": on_blue(cyan("1")), "x": window.width // 4, "y": window.height // 2, "keys": {"w": 90, "a": 180, "s": 270, "d": 0}, } p2_attrs = { - "appearance": on_red((yellow("2"))), + "appearance": on_red(yellow("2")), "x": 3 * window.width // 4, "y": window.height // 2, "keys": {"": 90, "": 180, "": 270, "": 0}, From ad4860bba09743d40212cadf2fd9ce7922d2864c Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 21 Dec 2020 17:43:26 +0100 Subject: [PATCH 125/302] Move tests to /tests --- curtsies/formatstringarray.py | 48 +++-------------------------------- tests/test_fmtstr.py | 41 +++++++++++++++++++++++++++--- 2 files changed, 42 insertions(+), 47 deletions(-) diff --git a/curtsies/formatstringarray.py b/curtsies/formatstringarray.py index 84c4af9..0f4f737 100644 --- a/curtsies/formatstringarray.py +++ b/curtsies/formatstringarray.py @@ -23,7 +23,6 @@ import sys import logging -import unittest from .formatstring import fmtstr from .formatstring import normalize_slice @@ -209,12 +208,10 @@ def __setitem__(self, slicetuple, value): ] + self.rows[rowslice.stop :] ) - msg = ( - "You are trying to fit this value {} into the region {}: {}".format( - fmtstr("".join(value), bg="cyan"), - fmtstr("").join(grid_value), - "\n ".join(grid_fsarray[x] for x in range(len(self.rows))), - ) + msg = "You are trying to fit this value {} into the region {}: {}".format( + fmtstr("".join(value), bg="cyan"), + fmtstr("").join(grid_value), + "\n ".join(grid_fsarray[x] for x in range(len(self.rows))), ) raise ValueError( """Error you are trying to replace a region of {} rows by {} @@ -300,43 +297,6 @@ def simple_format(x): return "\n".join(actualize(l) for l in x) -class FormatStringTest(unittest.TestCase): - def assertFSArraysEqual(self, a, b): - # type: (FSArray, FSArray) -> None - self.assertEqual(type(a), FSArray) - self.assertEqual(type(b), FSArray) - self.assertEqual( - (a.width, b.height), - (a.width, b.height), - f"fsarray dimensions do not match: {a.shape} {b.shape}", - ) - for i, (a_row, b_row) in enumerate(zip(a, b)): - self.assertEqual( - a_row, - b_row, - "FSArrays differ first on line {}:\n{}".format(i, FSArray.diff(a, b)), - ) - - def assertFSArraysEqualIgnoringFormatting(self, a, b): - # type: (FSArray, FSArray) -> None - """Also accepts arrays of strings""" - self.assertEqual( - len(a), - len(b), - "fsarray heights do not match: %s %s \n%s \n%s" - % (len(a), len(b), simple_format(a), simple_format(b)), - ) - for i, (a_row, b_row) in enumerate(zip(a, b)): - a_row = a_row.s if isinstance(a_row, FmtStr) else a_row - b_row = b_row.s if isinstance(b_row, FmtStr) else b_row - self.assertEqual( - a_row, - b_row, - "FSArrays differ first on line %s:\n%s" - % (i, FSArray.diff(a, b, ignore_formatting=True)), - ) - - if __name__ == "__main__": a = FSArray(3, 14, bg="blue") a[0:2, 5:11] = cast( diff --git a/tests/test_fmtstr.py b/tests/test_fmtstr.py index a420e3c..3c3db27 100644 --- a/tests/test_fmtstr.py +++ b/tests/test_fmtstr.py @@ -20,7 +20,7 @@ bold, ) from curtsies.termformatconstants import FG_COLORS -from curtsies.formatstringarray import fsarray, FSArray, FormatStringTest +from curtsies.formatstringarray import fsarray, FSArray from unittest import skip @@ -576,6 +576,43 @@ def test_oomerror(self): a[2:-2, 2:-2] = fsarray(["asdf", "zxcv"]) +class FormatStringTest(unittest.TestCase): + def assertFSArraysEqual(self, a, b): + # type: (FSArray, FSArray) -> None + self.assertEqual(type(a), FSArray) + self.assertEqual(type(b), FSArray) + self.assertEqual( + (a.width, b.height), + (a.width, b.height), + f"fsarray dimensions do not match: {a.shape} {b.shape}", + ) + for i, (a_row, b_row) in enumerate(zip(a, b)): + self.assertEqual( + a_row, + b_row, + "FSArrays differ first on line {}:\n{}".format(i, FSArray.diff(a, b)), + ) + + def assertFSArraysEqualIgnoringFormatting(self, a, b): + # type: (FSArray, FSArray) -> None + """Also accepts arrays of strings""" + self.assertEqual( + len(a), + len(b), + "fsarray heights do not match: %s %s \n%s \n%s" + % (len(a), len(b), simple_format(a), simple_format(b)), + ) + for i, (a_row, b_row) in enumerate(zip(a, b)): + a_row = a_row.s if isinstance(a_row, FmtStr) else a_row + b_row = b_row.s if isinstance(b_row, FmtStr) else b_row + self.assertEqual( + a_row, + b_row, + "FSArrays differ first on line %s:\n%s" + % (i, FSArray.diff(a, b, ignore_formatting=True)), + ) + + class TestFSArrayWithDiff(FormatStringTest): def test_diff_testing(self): a = fsarray(["abc", "def"]) @@ -596,6 +633,4 @@ def test_diff_testing(self): if __name__ == "__main__": - import fmtstr.fmtstr - unittest.main() From 9280100fc45e3c2dba48d3f63a07be6477b25e33 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 21 Dec 2020 17:48:32 +0100 Subject: [PATCH 126/302] Apply black --- curtsies/events.py | 4 +- curtsies/formatstring.py | 330 +++++++++++++++++++++++++-------------- curtsies/input.py | 6 +- curtsies/window.py | 4 +- 4 files changed, 218 insertions(+), 126 deletions(-) diff --git a/curtsies/events.py b/curtsies/events.py index b21fe8b..2380eb2 100644 --- a/curtsies/events.py +++ b/curtsies/events.py @@ -12,9 +12,7 @@ CURTSIES_NAMES = {} -control_chars = { - chr_byte(i): "" % chr(i + 0x60) for i in range(0x00, 0x1B) -} +control_chars = {chr_byte(i): "" % chr(i + 0x60) for i in range(0x00, 0x1B)} CURTSIES_NAMES.update(control_chars) for i in range(0x00, 0x80): CURTSIES_NAMES[b"\x1b" + chr_byte(i)] = "" % chr(i) diff --git a/curtsies/formatstring.py b/curtsies/formatstring.py index 684361f..c0dcd03 100644 --- a/curtsies/formatstring.py +++ b/curtsies/formatstring.py @@ -19,7 +19,23 @@ red('hello') """ -from typing import Iterator, Text, Tuple, List, Union, Optional, Any, Mapping, cast, MutableMapping, no_type_check, Type, cast, Callable, Iterable +from typing import ( + Iterator, + Text, + Tuple, + List, + Union, + Optional, + Any, + Mapping, + cast, + MutableMapping, + no_type_check, + Type, + cast, + Callable, + Iterable, +) import itertools @@ -28,11 +44,17 @@ from wcwidth import wcswidth from .escseqparse import parse, remove_ansi -from .termformatconstants import (FG_COLORS, BG_COLORS, STYLES, - FG_NUMBER_TO_COLOR, BG_NUMBER_TO_COLOR, - RESET_ALL, RESET_BG, RESET_FG, - seq) - +from .termformatconstants import ( + FG_COLORS, + BG_COLORS, + STYLES, + FG_NUMBER_TO_COLOR, + BG_NUMBER_TO_COLOR, + RESET_ALL, + RESET_BG, + RESET_FG, + seq, +) one_arg_xforms = { 'bold' : lambda s: seq(STYLES['bold']) +s+seq(RESET_ALL), @@ -55,6 +77,7 @@ class FrozenDict(dict): """Immutable dictionary class""" + @no_type_check def __setitem__(self, key, value): raise Exception("Cannot change value.") @@ -77,18 +100,23 @@ def stable_format_dict(d): """A sorted, python2/3 stable formatting of a dictionary. Does not work for dicts with unicode strings as values.""" - inner = ', '.join('{}: {}'.format(repr(k)[1:] - if repr(k).startswith("u'") or repr(k).startswith('u"') - else repr(k), - v) - for k, v in sorted(d.items())) - return '{%s}' % inner + inner = ", ".join( + "{}: {}".format( + repr(k)[1:] + if repr(k).startswith("u'") or repr(k).startswith('u"') + else repr(k), + v, + ) + for k, v in sorted(d.items()) + ) + return "{%s}" % inner class Chunk: """A string with a single set of formatting attributes Subject to change, not part of the API""" + def __init__(self, string, atts=None): # type: (Text, Mapping[str, Union[int, bool]]) -> None if atts is None: @@ -121,7 +149,7 @@ def width(self): raise ValueError("Can't calculate width of string %r" % self._s) return width - #TODO cache this + # TODO cache this @property def color_str(self): # type: () -> Text @@ -159,22 +187,31 @@ def __hash__(self): def __repr__(self): # type: () -> str - return 'Chunk({s}{sep}{atts})'.format( + return "Chunk({s}{sep}{atts})".format( s=repr(self.s), - sep=', ' if self.atts else '', - atts = stable_format_dict(self.atts) if self.atts else '') + sep=", " if self.atts else "", + atts=stable_format_dict(self.atts) if self.atts else "", + ) def repr_part(self): # type: () -> str """FmtStr repr is build by concatenating these.""" + def pp_att(att): # type: (str) -> str - if att == 'fg': return FG_NUMBER_TO_COLOR[self.atts[att]] - elif att == 'bg': return 'on_' + BG_NUMBER_TO_COLOR[self.atts[att]] - else: return att + if att == "fg": + return FG_NUMBER_TO_COLOR[self.atts[att]] + elif att == "bg": + return "on_" + BG_NUMBER_TO_COLOR[self.atts[att]] + else: + return att + atts_out = {k: v for (k, v) in self.atts.items() if v} - return (''.join(pp_att(att)+'(' for att in sorted(atts_out)) - + (repr(self.s) if PY3 else repr(self.s)[1:]) + ')'*len(atts_out)) + return ( + "".join(pp_att(att) + "(" for att in sorted(atts_out)) + + repr(self.s) + + ")" * len(atts_out) + ) def splitter(self): # type: () -> ChunkSplitter @@ -209,7 +246,7 @@ def request(self, max_width): # type: (int) -> Optional[Tuple[int, Chunk]] """Requests a sub-chunk of max_width or shorter. Returns None if no chunks left.""" if max_width < 1: - raise ValueError('requires positive integer max_width') + raise ValueError("requires positive integer max_width") s = self.chunk.s length = len(s) @@ -219,7 +256,7 @@ def request(self, max_width): width = 0 start_offset = i = self.internal_offset - replacement_char = ' ' + replacement_char = " " while True: w = wcswidth(s[i]) @@ -231,30 +268,46 @@ def request(self, max_width): # if not adding it us makes us short, this must have been a double-width character if width < max_width: - assert width + 1 == max_width, 'unicode character width of more than 2!?!' - assert w == 2, 'unicode character of width other than 2?' - return (width + 1, Chunk(s[start_offset:self.internal_offset] + replacement_char, - atts=self.chunk.atts)) - return (width, Chunk(s[start_offset:self.internal_offset], atts=self.chunk.atts)) + assert ( + width + 1 == max_width + ), "unicode character width of more than 2!?!" + assert w == 2, "unicode character of width other than 2?" + return ( + width + 1, + Chunk( + s[start_offset : self.internal_offset] + replacement_char, + atts=self.chunk.atts, + ), + ) + return ( + width, + Chunk(s[start_offset : self.internal_offset], atts=self.chunk.atts), + ) # otherwise add this width width += w # If one more char would put us over, return whatever we've got if i + 1 == length: - self.internal_offset = i + 1 # beware the fencepost, i is an index not an offset + self.internal_offset = ( + i + 1 + ) # beware the fencepost, i is an index not an offset self.internal_width += width - return (width, Chunk(s[start_offset:self.internal_offset], atts=self.chunk.atts)) + return ( + width, + Chunk(s[start_offset : self.internal_offset], atts=self.chunk.atts), + ) # otherwise attempt to add the next character i += 1 class FmtStr: """A string whose substrings carry attributes.""" + def __init__(self, *components): # type: (*Chunk) -> None # These assertions below could be useful for debugging, but slow things down considerably - #assert all([len(x) > 0 for x in components]) - #self.chunks = [x for x in components if len(x) > 0] + # assert all([len(x) > 0 for x in components]) + # self.chunks = [x for x in components if len(x) > 0] self.chunks = list(components) # caching these leads to a significant speedup @@ -278,7 +331,7 @@ def from_str(cls, s): '|'+on_blue(red('hey'))+'|' """ - if '\x1b[' in s: + if "\x1b[" in s: try: tokens_and_strings = parse(s) except ValueError: @@ -290,9 +343,9 @@ def from_str(cls, s): if isinstance(x, dict): cur_fmt.update(x) elif isinstance(x, str): - atts = parse_args((), {k: v - for k, v in cur_fmt.items() - if v is not None}) + atts = parse_args( + (), {k: v for k, v in cur_fmt.items() if v is not None} + ) chunks.append(Chunk(x, atts=atts)) else: raise Exception("logic error") @@ -304,26 +357,29 @@ def copy_with_new_str(self, new_str): # type: (Text) -> FmtStr """Copies the current FmtStr's attributes while changing its string.""" # What to do when there are multiple Chunks with conflicting atts? - old_atts = {att: value for bfs in self.chunks - for (att, value) in bfs.atts.items()} + old_atts = { + att: value for bfs in self.chunks for (att, value) in bfs.atts.items() + } return FmtStr(Chunk(new_str, old_atts)) def setitem(self, startindex, fs): # type: (int, Union[Text, FmtStr]) -> FmtStr """Shim for easily converting old __setitem__ calls""" - return self.setslice_with_length(startindex, startindex+1, fs, len(self)) + return self.setslice_with_length(startindex, startindex + 1, fs, len(self)) def setslice_with_length(self, startindex, endindex, fs, length): # type: (int, int, Union[Text, FmtStr], int) -> FmtStr """Shim for easily converting old __setitem__ calls""" if len(self) < startindex: - fs = ' '*(startindex - len(self)) + fs + fs = " " * (startindex - len(self)) + fs if len(self) > endindex: - fs = fs + ' '*(endindex - startindex - len(fs)) + fs = fs + " " * (endindex - startindex - len(fs)) assert len(fs) == endindex - startindex, (len(fs), startindex, endindex) result = self.splice(fs, startindex, endindex) if len(result) > length: - raise ValueError("Your change is resulting in a longer fmtstr than the original length and this is not supported.") + raise ValueError( + "Your change is resulting in a longer fmtstr than the original length and this is not supported." + ) return result def splice(self, new_str, start, end=None): @@ -342,9 +398,9 @@ def splice(self, new_str, start, end=None): end = start tail = None - for bfs, bfs_start, bfs_end in zip(self.chunks, - self.divides[:-1], - self.divides[1:]): + for bfs, bfs_start, bfs_end in zip( + self.chunks, self.divides[:-1], self.divides[1:] + ): if end == bfs_start == 0: new_components.extend(new_fs.chunks) new_components.append(bfs) @@ -353,17 +409,17 @@ def splice(self, new_str, start, end=None): elif bfs_start <= start < bfs_end: divide = start - bfs_start head = Chunk(bfs.s[:divide], atts=bfs.atts) - tail = Chunk(bfs.s[end - bfs_start:], atts=bfs.atts) + tail = Chunk(bfs.s[end - bfs_start :], atts=bfs.atts) new_components.extend([head] + new_fs.chunks) inserted = True if bfs_start < end < bfs_end: - tail = Chunk(bfs.s[end - bfs_start:], atts=bfs.atts) + tail = Chunk(bfs.s[end - bfs_start :], atts=bfs.atts) new_components.append(tail) elif bfs_start < end < bfs_end: divide = start - bfs_start - tail = Chunk(bfs.s[end - bfs_start:], atts=bfs.atts) + tail = Chunk(bfs.s[end - bfs_start :], atts=bfs.atts) new_components.append(tail) elif bfs_start >= end or bfs_end <= start: @@ -398,8 +454,6 @@ def join(self, iterable): chunks.extend(s.chunks) elif isinstance(s, (bytes, str)): chunks.extend(fmtstr(s).chunks) # TODO just make a chunk directly - elif isinstance(s, (bytes, unicode)): - chunks.extend(fmtstr(s).chunks) #TODO just make a chunk directly else: raise TypeError("expected str or FmtStr, %r found" % type(s)) return FmtStr(*chunks) @@ -412,24 +466,31 @@ def split(self, sep=None, maxsplit=None, regex=False): Capture groups are ignored in regex, the whole pattern is matched and used to split the original FmtStr.""" if maxsplit is not None: - raise NotImplementedError('no maxsplit yet') + raise NotImplementedError("no maxsplit yet") s = self.s if sep is None: - sep = r'\s+' + sep = r"\s+" elif not regex: sep = re.escape(sep) matches = list(re.finditer(sep, s)) - return [self[start:end] for start, end in zip( - [0] + [m.end() for m in matches], - [m.start() for m in matches] + [len(s)])] + return [ + self[start:end] + for start, end in zip( + [0] + [m.end() for m in matches], + [m.start() for m in matches] + [len(s)], + ) + ] def splitlines(self, keepends=False): # type: (bool) -> List[FmtStr] """Return a list of lines, split on newline characters, include line boundaries, if keepends is true.""" - lines = self.split('\n') - return [line+'\n' for line in lines] if keepends else ( - lines if lines[-1] else lines[:-1]) + lines = self.split("\n") + return ( + [line + "\n" for line in lines] + if keepends + else (lines if lines[-1] else lines[:-1]) + ) # proxying to the string via __getattr__ is insufficient # because we shouldn't drop foreground or formatting info @@ -441,12 +502,12 @@ def ljust(self, width, fillchar=None): """ if fillchar is not None: return fmtstr(self.s.ljust(width, fillchar), **self.shared_atts) - to_add = ' ' * (width - len(self.s)) + to_add = " " * (width - len(self.s)) shared = self.shared_atts - if 'bg' in shared: - return self + fmtstr(to_add, bg=shared['bg']) if to_add else self + if "bg" in shared: + return self + fmtstr(to_add, bg=shared["bg"]) if to_add else self else: - uniform = self.new_with_atts_removed('bg') + uniform = self.new_with_atts_removed("bg") return uniform + fmtstr(to_add, **self.shared_atts) if to_add else uniform def rjust(self, width, fillchar=None): @@ -457,12 +518,12 @@ def rjust(self, width, fillchar=None): """ if fillchar is not None: return fmtstr(self.s.rjust(width, fillchar), **self.shared_atts) - to_add = ' ' * (width - len(self.s)) + to_add = " " * (width - len(self.s)) shared = self.shared_atts - if 'bg' in shared: - return fmtstr(to_add, bg=shared['bg']) + self if to_add else self + if "bg" in shared: + return fmtstr(to_add, bg=shared["bg"]) + self if to_add else self else: - uniform = self.new_with_atts_removed('bg') + uniform = self.new_with_atts_removed("bg") return fmtstr(to_add, **self.shared_atts) + uniform if to_add else uniform def __str__(self): @@ -493,15 +554,14 @@ def width(self): def width_at_offset(self, n): # type: (int) -> int """Returns the horizontal position of character n of the string""" - #TODO make more efficient? + # TODO make more efficient? width = wcswidth(self.s[:n]) assert width != -1 return width - def __repr__(self): # type: () -> str - return '+'.join(fs.repr_part() for fs in self.chunks) + return "+".join(fs.repr_part() for fs in self.chunks) def __eq__(self, other): # type: (Any) -> bool @@ -520,7 +580,7 @@ def __add__(self, other): elif isinstance(other, (bytes, str)): return FmtStr(*(self.chunks + [Chunk(other)])) else: - raise TypeError(f'Can\'t add {self!r} and {other!r}') + raise TypeError(f"Can't add {self!r} and {other!r}") def __radd__(self, other): # type: (Union[FmtStr, Text]) -> FmtStr @@ -529,25 +589,30 @@ def __radd__(self, other): elif isinstance(other, (bytes, str)): return FmtStr(*(x for x in ([Chunk(other)] + self.chunks))) else: - raise TypeError('Can\'t add those') + raise TypeError("Can't add those") def __mul__(self, other): # type: (int) -> FmtStr if isinstance(other, int): return sum([self for _ in range(other)], FmtStr()) - raise TypeError('Can\'t multiply those') - #TODO ensure emtpy FmtStr isn't a problem + raise TypeError("Can't multiply those") + + # TODO ensure emtpy FmtStr isn't a problem @property def shared_atts(self): # type: () -> Mapping[str, Union[int, bool]] """Gets atts shared among all nonzero length component Chunks""" - #TODO cache this, could get ugly for large FmtStrs + # TODO cache this, could get ugly for large FmtStrs atts = {} first = self.chunks[0] for att in sorted(first.atts): - #TODO how to write this without the '???'? - if all(fs.atts.get(att, '???') == first.atts[att] for fs in self.chunks if len(fs) > 0): + # TODO how to write this without the '???'? + if all( + fs.atts.get(att, "???") == first.atts[att] + for fs in self.chunks + if len(fs) > 0 + ): atts[att] = first.atts[att] return atts @@ -555,7 +620,7 @@ def new_with_atts_removed(self, *attributes): # type: (*Text) -> FmtStr """Returns a new FmtStr with the same content but some attributes removed""" - result = FmtStr(*[Chunk(bfs.s, bfs.atts.remove(*attributes)) for bfs in self.chunks]) # type: ignore + result = FmtStr(*[Chunk(bfs.s, bfs.atts.remove(*attributes)) for bfs in self.chunks]) # type: ignore return result @no_type_check @@ -605,18 +670,20 @@ def __getitem__(self, index): if end - start == len(chunk): parts.append(chunk) else: - s_part = chunk.s[max(0, index.start - counter): index.stop - counter] + s_part = chunk.s[ + max(0, index.start - counter) : index.stop - counter + ] parts.append(Chunk(s_part, chunk.atts)) counter += len(chunk) if index.stop < counter: break - return FmtStr(*parts) if parts else fmtstr('') + return FmtStr(*parts) if parts else fmtstr("") def width_aware_slice(self, index): # type: (Union[int, slice]) -> FmtStr """Slice based on the number of columns it would take to display the substring.""" if wcswidth(self.s) == -1: - raise ValueError('bad values for width aware slicing') + raise ValueError("bad values for width aware slicing") index = normalize_slice(self.width, index) counter = 0 parts = [] @@ -627,12 +694,14 @@ def width_aware_slice(self, index): if end - start == chunk.width: parts.append(chunk) else: - s_part = width_aware_slice(chunk.s, max(0, index.start - counter), index.stop - counter) + s_part = width_aware_slice( + chunk.s, max(0, index.start - counter), index.stop - counter + ) parts.append(Chunk(s_part, chunk.atts)) counter += chunk.width if index.stop < counter: break - return FmtStr(*parts) if parts else fmtstr('') + return FmtStr(*parts) if parts else fmtstr("") def width_aware_splitlines(self, columns): # type: (int) -> Iterator[FmtStr] @@ -643,7 +712,7 @@ def width_aware_splitlines(self, columns): if columns < 2: raise ValueError("Column width %s is too narrow." % columns) if wcswidth(self.s) == -1: - raise ValueError('bad values for width aware slicing') + raise ValueError("bad values for width aware slicing") return self._width_aware_splitlines(columns) def _width_aware_splitlines(self, columns): @@ -674,10 +743,10 @@ def _getitem_normalized(self, index): """Builds the more compact fmtstrs by using fromstr( of the control sequences)""" index = normalize_slice(len(self), index) counter = 0 - output = '' + output = "" for fs in self.chunks: if index.start < counter + len(fs) and index.stop > counter: - s_part = fs.s[max(0, index.start - counter):index.stop - counter] + s_part = fs.s[max(0, index.start - counter) : index.stop - counter] piece = Chunk(s_part, fs.atts).color_str output += piece counter += len(fs) @@ -711,7 +780,7 @@ def interval_overlap(a, b, x, y): assert False -def width_aware_slice(s, start, end, replacement_char=' '): +def width_aware_slice(s, start, end, replacement_char=" "): # type: (Text, int, int, Text) -> Text """ >>> width_aware_slice(u'a\uff25iou', 0, 2)[1] == u' ' @@ -725,13 +794,15 @@ def width_aware_slice(s, start, end, replacement_char=' '): for char, char_start, char_end in zip(s, divides[:-1], divides[1:]): if char_start == start and char_end == start: continue # don't use zero-width characters at the beginning of a slice - # (combining characters combine with the chars before themselves) + # (combining characters combine with the chars before themselves) elif char_start >= start and char_end <= end: new_chunk_chars.append(char) else: - new_chunk_chars.extend(replacement_char * interval_overlap(char_start, char_end, start, end)) + new_chunk_chars.extend( + replacement_char * interval_overlap(char_start, char_end, start, end) + ) - return ''.join(new_chunk_chars) + return "".join(new_chunk_chars) def linesplit(string, columns): @@ -750,37 +821,50 @@ def linesplit(string, columns): string = fmtstr(string) string_s = string.s - matches = list(re.finditer(r'\s+', string_s)) - spaces = [string[m.start():m.end()] for m in matches if m.start() != 0 and m.end() != len(string_s)] - words = [string[start:end] for start, end in zip( + matches = list(re.finditer(r"\s+", string_s)) + spaces = [ + string[m.start() : m.end()] + for m in matches + if m.start() != 0 and m.end() != len(string_s) + ] + words = [ + string[start:end] + for start, end in zip( [0] + [m.end() for m in matches], - [m.start() for m in matches] + [len(string_s)]) if start != end] + [m.start() for m in matches] + [len(string_s)], + ) + if start != end + ] - word_to_lines = lambda word: [word[columns*i:columns*(i+1)] for i in range((len(word) - 1) // columns + 1)] + word_to_lines = lambda word: [ + word[columns * i : columns * (i + 1)] + for i in range((len(word) - 1) // columns + 1) + ] lines = word_to_lines(words[0]) for word, space in zip(words[1:], spaces): if len(lines[-1]) + len(word) < columns: - lines[-1] += fmtstr(' ', **space.shared_atts) + lines[-1] += fmtstr(" ", **space.shared_atts) lines[-1] += word else: lines.extend(word_to_lines(word)) return lines + def normalize_slice(length, index): # type: (int, Union[int, slice]) -> slice "Fill in the Nones in a slice." is_int = False if isinstance(index, int): is_int = True - index = slice(index, index+1) + index = slice(index, index + 1) if index.start is None: index = slice(0, index.stop, index.step) if index.stop is None: index = slice(index.start, length, index.step) - if index.start < -1: # XXX why must this be -1? + if index.start < -1: # XXX why must this be -1? index = slice(length - index.start, index.stop, index.step) - if index.stop < -1: # XXX why must this be -1? + if index.stop < -1: # XXX why must this be -1? index = slice(index.start, length - index.stop, index.step) if index.step is not None: raise NotImplementedError("You can't use steps with slicing yet") @@ -789,41 +873,45 @@ def normalize_slice(length, index): raise IndexError(f"index out of bounds: {index!r} for length {length}") return index + def parse_args(args, kwargs): # type: (Tuple[Union[bytes, str], ...], MutableMapping[str, Union[int, bool, str]]) -> Mapping[str, Union[int, bool]] """Returns a kwargs dictionary by turning args into kwargs""" - if 'style' in kwargs: - args += (cast(str, kwargs['style']),) - del kwargs['style'] + if "style" in kwargs: + args += (cast(str, kwargs["style"]),) + del kwargs["style"] for arg in args: arg = cast(str, arg) if not isinstance(arg, (bytes, str)): raise ValueError("args must be strings:" + repr(args)) if arg.lower() in FG_COLORS: - if 'fg' in kwargs: raise ValueError("fg specified twice") - kwargs['fg'] = FG_COLORS[cast(str, arg)] - elif arg.lower().startswith('on_') and arg[3:].lower() in BG_COLORS: - if 'bg' in kwargs: raise ValueError("fg specified twice") - kwargs['bg'] = BG_COLORS[cast(str, arg[3:])] + if "fg" in kwargs: + raise ValueError("fg specified twice") + kwargs["fg"] = FG_COLORS[cast(str, arg)] + elif arg.lower().startswith("on_") and arg[3:].lower() in BG_COLORS: + if "bg" in kwargs: + raise ValueError("fg specified twice") + kwargs["bg"] = BG_COLORS[cast(str, arg[3:])] elif arg.lower() in STYLES: kwargs[arg] = True else: - raise ValueError("couldn't process arg: "+repr(arg)) + raise ValueError("couldn't process arg: " + repr(arg)) for k in kwargs: - if k not in ['fg', 'bg'] + list(STYLES.keys()): + if k not in ["fg", "bg"] + list(STYLES.keys()): raise ValueError("Can't apply that transformation") - if 'fg' in kwargs: - if kwargs['fg'] in FG_COLORS: - kwargs['fg'] = FG_COLORS[cast(str, kwargs['fg'])] - if kwargs['fg'] not in list(FG_COLORS.values()): - raise ValueError("Bad fg value: %r" % kwargs['fg']) - if 'bg' in kwargs: - if kwargs['bg'] in BG_COLORS: - kwargs['bg'] = BG_COLORS[cast(str, kwargs['bg'])] - if kwargs['bg'] not in list(BG_COLORS.values()): - raise ValueError("Bad bg value: %r" % kwargs['bg']) + if "fg" in kwargs: + if kwargs["fg"] in FG_COLORS: + kwargs["fg"] = FG_COLORS[cast(str, kwargs["fg"])] + if kwargs["fg"] not in list(FG_COLORS.values()): + raise ValueError("Bad fg value: %r" % kwargs["fg"]) + if "bg" in kwargs: + if kwargs["bg"] in BG_COLORS: + kwargs["bg"] = BG_COLORS[cast(str, kwargs["bg"])] + if kwargs["bg"] not in list(BG_COLORS.values()): + raise ValueError("Bad bg value: %r" % kwargs["bg"]) return cast(MutableMapping[str, Union[int, bool]], kwargs) + def fmtstr(string, *args, **kwargs): # type: (Union[Text, FmtStr], *Any, **Any) -> FmtStr """ @@ -840,5 +928,9 @@ def fmtstr(string, *args, **kwargs): elif isinstance(string, (bytes, str)): string = FmtStr.from_str(string) else: - raise ValueError("Bad Args: {!r} (of type {}), {!r}, {!r}".format(string, type(string), args, kwargs)) + raise ValueError( + "Bad Args: {!r} (of type {}), {!r}, {!r}".format( + string, type(string), args, kwargs + ) + ) return string.copy_with_new_atts(**atts) diff --git a/curtsies/input.py b/curtsies/input.py index 37bada4..50b0e9e 100644 --- a/curtsies/input.py +++ b/curtsies/input.py @@ -123,7 +123,11 @@ def __enter__(self): def __exit__(self, type=None, value=None, traceback=None): # type: (Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]) -> None - if self.sigint_event and is_main_thread() and self.orig_sigint_handler is not None: + if ( + self.sigint_event + and is_main_thread() + and self.orig_sigint_handler is not None + ): signal.signal(signal.SIGINT, self.orig_sigint_handler) termios.tcsetattr(self.in_stream, termios.TCSANOW, self.original_stty) diff --git a/curtsies/window.py b/curtsies/window.py index 61d7d98..212073d 100644 --- a/curtsies/window.py +++ b/curtsies/window.py @@ -503,9 +503,7 @@ def render_to_terminal(self, array, cursor_pos=(0, 0)): self.top_usable_row -= 1 else: offscreen_scrolls += 1 - current_lines_by_row = { - k - 1: v for k, v in current_lines_by_row.items() - } + current_lines_by_row = {k - 1: v for k, v in current_lines_by_row.items()} logger.debug("new top_usable_row: %d" % self.top_usable_row) # since scrolling moves the cursor self.write(self.t.move(height - 1, 0)) From 0329b48349e7e9f62245bad7f07a9981b2708025 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 21 Dec 2020 19:11:29 +0100 Subject: [PATCH 127/302] Use assertIsInstance --- tests/test_fmtstr.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_fmtstr.py b/tests/test_fmtstr.py index 3c3db27..941f78b 100644 --- a/tests/test_fmtstr.py +++ b/tests/test_fmtstr.py @@ -579,8 +579,8 @@ def test_oomerror(self): class FormatStringTest(unittest.TestCase): def assertFSArraysEqual(self, a, b): # type: (FSArray, FSArray) -> None - self.assertEqual(type(a), FSArray) - self.assertEqual(type(b), FSArray) + self.assertIsInstance(a, FSArray) + self.assertIsInstance(b, FSArray) self.assertEqual( (a.width, b.height), (a.width, b.height), From 5ebdba27041ff541d350733123a95b6fe42b34ea Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 21 Dec 2020 19:53:50 +0100 Subject: [PATCH 128/302] Fix docstring --- curtsies/formatstringarray.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/curtsies/formatstringarray.py b/curtsies/formatstringarray.py index 0f4f737..fa13c0b 100644 --- a/curtsies/formatstringarray.py +++ b/curtsies/formatstringarray.py @@ -142,13 +142,13 @@ def shape(self): @property def height(self): # type: () -> int - "The number of rows" + """The number of rows""" return len(self.rows) @property def width(self): # type: () -> int - "The number of columns" + """The number of columns""" return self.num_columns # TODO rework this next major version bump From e78f2783839be98076069cd003d87f5feef6b22b Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 21 Dec 2020 19:54:55 +0100 Subject: [PATCH 129/302] Use any to abort as soon as condition is true --- curtsies/formatstringarray.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/curtsies/formatstringarray.py b/curtsies/formatstringarray.py index fa13c0b..f70a87f 100644 --- a/curtsies/formatstringarray.py +++ b/curtsies/formatstringarray.py @@ -64,8 +64,8 @@ def fsarray(strings, *args, **kwargs): if "width" in kwargs: width = kwargs["width"] del kwargs["width"] - if strings and max(len(s) for s in strings) > width: - raise ValueError("Those strings won't fit for width %d" % width) + if strings and any(len(s) > width for s in strings): + raise ValueError(f"Those strings won't fit for width {width}") else: width = max(len(s) for s in strings) if strings else 0 fstrings = [ From 715fb700f38beda49d3e458cec9a8e975ee7a44d Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 21 Dec 2020 20:08:01 +0100 Subject: [PATCH 130/302] Replace type check with current_thread() == main_thread() --- curtsies/input.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/curtsies/input.py b/curtsies/input.py index 50b0e9e..6c7006a 100644 --- a/curtsies/input.py +++ b/curtsies/input.py @@ -27,7 +27,7 @@ def is_main_thread(): # type: () -> bool - return isinstance(threading.current_thread(), threading._MainThread) # type: ignore + return threading.current_thread() == threading.main_thread() class ReplacedSigIntHandler: From 196ddfabebab71e9af7c99d3c1682a54bfd8168d Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 21 Dec 2020 20:26:02 +0100 Subject: [PATCH 131/302] Initialize dict directly --- curtsies/curtsieskeys.py | 253 +++++++++++++++++++-------------------- 1 file changed, 126 insertions(+), 127 deletions(-) diff --git a/curtsies/curtsieskeys.py b/curtsies/curtsieskeys.py index a1e7b49..7bd89df 100644 --- a/curtsies/curtsieskeys.py +++ b/curtsies/curtsieskeys.py @@ -8,131 +8,130 @@ # TODO add PAD keys hack as in bpython.cli # fmt: off -CURTSIES_NAMES = dict([ - (b' ', ''), - (b'\x1b ', ''), - (b'\t', ''), - (b'\x1b[Z', ''), - (b'\x1b[A', ''), - (b'\x1b[B', ''), - (b'\x1b[C', ''), - (b'\x1b[D', ''), - (b'\x1bOA', ''), # in issue 92 its shown these should be normal arrows, - (b'\x1bOB', ''), # not ctrl-arrows as we previously had them. - (b'\x1bOC', ''), - (b'\x1bOD', ''), - - (b'\x1b[1;5A', ''), - (b'\x1b[1;5B', ''), - (b'\x1b[1;5C', ''), # reported by myint - (b'\x1b[1;5D', ''), # reported by myint - - (b'\x1b[5A', ''), # not sure about these, someone wanted them for bpython - (b'\x1b[5B', ''), - (b'\x1b[5C', ''), - (b'\x1b[5D', ''), - - (b'\x1b[1;9A', ''), - (b'\x1b[1;9B', ''), - (b'\x1b[1;9C', ''), - (b'\x1b[1;9D', ''), - - (b'\x1b[1;10A', ''), - (b'\x1b[1;10B', ''), - (b'\x1b[1;10C', ''), - (b'\x1b[1;10D', ''), - - (b'\x1bOP', ''), - (b'\x1bOQ', ''), - (b'\x1bOR', ''), - (b'\x1bOS', ''), - - # see bpython #626 - (b'\x1b[11~', ''), - (b'\x1b[12~', ''), - (b'\x1b[13~', ''), - (b'\x1b[14~', ''), - - (b'\x1b[15~', ''), - (b'\x1b[17~', ''), - (b'\x1b[18~', ''), - (b'\x1b[19~', ''), - (b'\x1b[20~', ''), - (b'\x1b[21~', ''), - (b'\x1b[23~', ''), - (b'\x1b[24~', ''), - (b'\x00', ''), - (b'\x1c', ''), - (b'\x1d', ''), - (b'\x1e', ''), - (b'\x1f', ''), - (b'\x7f', ''), # for some folks this is ctrl-backspace apparently - (b'\x1b\x7f', ''), - (b'\xff', ''), - (b'\x1b\x1b[A', ''), # uncertain about these four - (b'\x1b\x1b[B', ''), - (b'\x1b\x1b[C', ''), - (b'\x1b\x1b[D', ''), - (b'\x1b', ''), - (b'\x1b[1~', ''), - (b'\x1b[4~', ''), - (b'\x1b\x1b[5~',''), - (b'\x1b\x1b[6~',''), - - (b'\x1b[H', ''), # reported by amorozov in bpython #490 - (b'\x1b[F', ''), # reported by amorozov in bpython #490 - - (b'\x1bOH', ''), # reported by mixmastamyk in curtsies #78 - (b'\x1bOF', ''), # reported by mixmastamyk in curtsies #78 - - # not fixing for back compat. - # (b"\x1b[1~", u''), # find - - (b"\x1b[2~", ''), # insert (0) - (b"\x1b[3~", ''), # delete (.), "Execute" - (b"\x1b[3;5~", ''), - - # not fixing for back compat. - # (b"\x1b[4~", u'', # select + + b"\x1b[5~": '', # pgup (9) + b"\x1b[6~": '', # pgdown (3) + b"\x1b[7~": '', # home + b"\x1b[8~": '', # end + b"\x1b[OA": '', # up (8) + b"\x1b[OB": '', # down (2) + b"\x1b[OC": '', # right (6) + b"\x1b[OD": '', # left (4) + b"\x1b[OF": '', # end (1) + b"\x1b[OH": '', # home (7) + + # reported by cool-RR + b"\x1b[[A": '', + b"\x1b[[B": '', + b"\x1b[[C": '', + b"\x1b[[D": '', + b"\x1b[[E": '', + # cool-RR says the rest were good: see issue #99 + + # reported by alethiophile see issue #119 + b"\x1b[1;3C": '', # alt-right + b"\x1b[1;3B": '', # alt-down + b"\x1b[1;3D": '', # alt-left + b"\x1b[1;3A": '', # alt-up + b"\x1b[5;3~": '', # alt-pageup + b"\x1b[6;3~": '', # alt-pagedown + b"\x1b[1;3H": '', # alt-home + b"\x1b[1;3F": '', # alt-end + b"\x1b[1;2C": '', + b"\x1b[1;2B": '', + b"\x1b[1;2D": '', + b"\x1b[1;2A": '', + b"\x1b[3;2~": '', + b"\x1b[5;2~": '', + b"\x1b[6;2~": '', + b"\x1b[1;2H": '', + b"\x1b[1;2F": '', + # end of keys reported by alethiophile +} # fmt: on From 39cfcaf8e9716ae09a491dc8a1232ba23d80ecf8 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 28 Dec 2020 10:56:32 +0100 Subject: [PATCH 132/302] Ignore more revisions --- .git-blame-ignore-revs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 4d8c1c8..6ab0a67 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -11,8 +11,9 @@ # When adding commits, write a comment describing its contents # followed by the 40-character commit ID on a new line. - # Initial formatting with Black dbbff8f74453a878fce0de7c7716efbf11e4586c # Formatting with Black 2b0522f4b9e38aff93a2a4d66cd388ec6b4ad448 +# Formatting with Black +9280100fc45e3c2dba48d3f63a07be6477b25e33 From 7f86c07d95aa22b004db9acf8f787e1abf49b581 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 29 Dec 2020 21:54:01 +0100 Subject: [PATCH 133/302] Bump Python version --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2bdda17..9a27585 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.black] line-length = 88 -target_version = ["py27"] +target_version = ["py36"] exclude = ''' ( /( @@ -18,4 +18,4 @@ exclude = ''' | docs/conf.py ) -''' \ No newline at end of file +''' From a467c0d24c59e9865fd8e20894152105365e0a05 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 3 Jan 2021 22:41:14 +0100 Subject: [PATCH 134/302] Fix invalid escape sequence deprecation warning --- curtsies/window.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/curtsies/window.py b/curtsies/window.py index 212073d..43b6f2e 100644 --- a/curtsies/window.py +++ b/curtsies/window.py @@ -354,9 +354,9 @@ def retrying_read(): c = retrying_read() resp += c m = re.search( - "(?P.*)" - "(?P\x1b\\[|\x9b)" - "(?P\\d+);(?P\\d+)R", + r"(?P.*)" + r"(?P\x1b\[|\x9b)" + r"(?P\d+);(?P\d+)R", resp, re.DOTALL, ) From 1aa3f6ad8e4febeccfdd4c77c8e55143261ec996 Mon Sep 17 00:00:00 2001 From: Joachim Durchholz Date: Sat, 2 Jan 2021 21:37:30 +0100 Subject: [PATCH 135/302] Improve code example in readme.md Bug fix: It wouldn't display the "Press escape to exit" message initially (made me believe it's not working at all). --- readme.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/readme.md b/readme.md index 61176a3..ef33b8c 100644 --- a/readme.md +++ b/readme.md @@ -13,11 +13,13 @@ from curtsies import FullscreenWindow, Input, FSArray from curtsies.fmtfuncs import red, bold, green, on_blue, yellow print(yellow('this prints normally, not to the alternate screen')) + with FullscreenWindow() as window: + a = FSArray(window.height, window.width) + msg = red(on_blue(bold('Press escape to exit, space to clear.'))) + a[0:1, 0:msg.width] = [msg] + window.render_to_terminal(a) with Input() as input_generator: - msg = red(on_blue(bold('Press escape to exit'))) - a = FSArray(window.height, window.width) - a[0:1, 0:msg.width] = [msg] for c in input_generator: if c == '': break From ffad3f0a178d4c4c4cfb855107350a61b6a8ae6a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 3 Jan 2021 23:06:12 +0100 Subject: [PATCH 136/302] Remove travis CI cruft --- readme.md | 1 - tests/test_terminal.py | 8 -------- 2 files changed, 9 deletions(-) diff --git a/readme.md b/readme.md index ef33b8c..381823a 100644 --- a/readme.md +++ b/readme.md @@ -1,4 +1,3 @@ -[![Build Status](https://travis-ci.org/bpython/curtsies.svg?branch=master)](https://travis-ci.org/bpython/curtsies) [![Documentation Status](https://readthedocs.org/projects/curtsies/badge/?version=latest)](https://readthedocs.org/projects/curtsies/?badge=latest) ![Curtsies Logo](http://ballingt.com/assets/curtsiestitle.png) diff --git a/tests/test_terminal.py b/tests/test_terminal.py index 7be12f6..175d830 100644 --- a/tests/test_terminal.py +++ b/tests/test_terminal.py @@ -14,12 +14,6 @@ from curtsies.window import BaseWindow, FullscreenWindow, CursorAwareWindow -# a few tests fail on TravisCI that have something to do with -# stdin not being able to be set to nonblocking -# (and still reporting isatty as True) -IS_TRAVIS = bool(os.environ.get("TRAVIS")) - - class FakeStdin(StringIO): encoding = "ascii" @@ -127,7 +121,6 @@ def __exit__(*args): pass -@skipIf(IS_TRAVIS, "Travis stdin behaves strangely, see issue 89") @skipUnless(sys.stdin.isatty(), "blessings Terminal needs streams open") class TestCursorAwareWindow(unittest.TestCase): def setUp(self): @@ -169,7 +162,6 @@ def test_inital_cursor_position(self): self.assertEqual(self.screen.display, [" ", "hi ", "there "]) -@skipIf(IS_TRAVIS, "Travis stdin behaves strangely, see issue 89") @skipUnless(sys.stdin.isatty(), "blessings Terminal needs streams open") class TestCursorAwareWindowWithExtraInput(unittest.TestCase): def setUp(self): From 31355f3fcaf9f9ce7ed08eb9fe37a3f8993e18cf Mon Sep 17 00:00:00 2001 From: Paolo Stivanin Date: Thu, 2 Apr 2020 13:00:33 +0200 Subject: [PATCH 137/302] Remove nose --- .github/workflows/build.yaml | 6 +++--- setup.cfg | 4 ---- setup.py | 9 +++++++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 09c581f..9a4ea44 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -22,10 +22,10 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install "blessings>=1.5" "wcwidth>=0.1.4" pyte nose + pip install "blessings>=1.5" "wcwidth>=0.1.4" pyte pytest - name: Build with Python ${{ matrix.python-version }} run: | python setup.py build - - name: Test with nosetest + - name: Test with pytest run: | - nosetests . + pytest -s . diff --git a/setup.cfg b/setup.cfg index 7d681fd..4455747 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,3 @@ -[nosetests] -with-doctest=1 -cover-package=curtsies -cover-html=1 [bdist_wheel] universal = 1 [mypy] diff --git a/setup.py b/setup.py index 55c0e02..7f985c7 100644 --- a/setup.py +++ b/setup.py @@ -11,10 +11,12 @@ def version(): if line.startswith("__version__"): return ast.parse(line).body[0].value.s + def long_description(): with open("readme.md", encoding="utf-8") as f: return f.read() + setup( name="curtsies", version=version(), @@ -28,9 +30,12 @@ def long_description(): packages=["curtsies"], install_requires=[ "blessings>=1.5", - "wcwidth>=0.1.4" + "wcwidth>=0.1.4", + ], + tests_require=[ + "pyte", + "pytest", ], - tests_require=["pyte", "nose",], classifiers=[ "Development Status :: 3 - Alpha", "Environment :: Console", From 953dbf2b84e0b2051d64bf4fa04ba55691a2d876 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 3 Jan 2021 23:35:37 +0100 Subject: [PATCH 138/302] Also run doctests --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 9a4ea44..c326784 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -28,4 +28,4 @@ jobs: python setup.py build - name: Test with pytest run: | - pytest -s . + pytest -s --doctest-modules curtsies tests From 6bd7802344f4fea7f7f001bf19fe8a9d9cd252b3 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 3 Jan 2021 23:51:24 +0100 Subject: [PATCH 139/302] Fix pytest invocation --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index c326784..14699c4 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -28,4 +28,4 @@ jobs: python setup.py build - name: Test with pytest run: | - pytest -s --doctest-modules curtsies tests + pytest -s --doctest-modules ./curtsies ./tests From 6881f86f5806f6d0e80e39177bca031fb2af678a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 17 Jan 2021 10:26:43 +0100 Subject: [PATCH 140/302] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9c3e37..b8ef0e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog ## [Unreleased] +- Drop supported for Python 2, 3.4 and 3.5. +- Migrate to pytest. Thanks to Paolo Stivanin +- Add new exmples. Thanks to rybarczykj +- mprove error messages. Thanks to Etienne Richart ## [0.3.4] - 2020-07-15 - Prevent crash when embedding in situations including the lldb debugger. Thanks Nathan Lanza! From a46ee84cb9611624ee088dc2ece6388d535cb913 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 17 Jan 2021 10:31:07 +0100 Subject: [PATCH 141/302] Fix hash invocation --- curtsies/formatstring.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/curtsies/formatstring.py b/curtsies/formatstring.py index c0dcd03..f344d6b 100644 --- a/curtsies/formatstring.py +++ b/curtsies/formatstring.py @@ -183,7 +183,7 @@ def __eq__(self, other): def __hash__(self): # type: () -> int - return hash(self.s, self.atts) + return hash((self.s, self.atts)) def __repr__(self): # type: () -> str From 7d0d3efe3310811f2f0501d5f359e31f50520c01 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 17 Jan 2021 10:35:23 +0100 Subject: [PATCH 142/302] Fix use of input_generator --- curtsies/window.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/curtsies/window.py b/curtsies/window.py index 43b6f2e..d25e5f6 100644 --- a/curtsies/window.py +++ b/curtsies/window.py @@ -533,8 +533,7 @@ def demo(): with FullscreenWindow(sys.stdout) as w: with input.Input(sys.stdin) as input_generator: rows, columns = w.t.height, w.t.width - while True: - c = input_generator.next() + for c in input_generator assert isinstance(c, Text) if c == "": sys.exit() # same as raise SystemExit() From 566d6cbbe5ef0fae4a9779d8ea6c8b170bf4741f Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 17 Jan 2021 10:47:11 +0100 Subject: [PATCH 143/302] Fix SyntaxError --- curtsies/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/curtsies/window.py b/curtsies/window.py index d25e5f6..61286ed 100644 --- a/curtsies/window.py +++ b/curtsies/window.py @@ -533,7 +533,7 @@ def demo(): with FullscreenWindow(sys.stdout) as w: with input.Input(sys.stdin) as input_generator: rows, columns = w.t.height, w.t.width - for c in input_generator + for c in input_generator: assert isinstance(c, Text) if c == "": sys.exit() # same as raise SystemExit() From f58a256fa27a6e1a747a8a2831472ec32fce2453 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 17 Jan 2021 11:06:40 +0100 Subject: [PATCH 144/302] Remove empty doc string --- curtsies/escseqparse.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/curtsies/escseqparse.py b/curtsies/escseqparse.py index 6ab7084..7455089 100644 --- a/curtsies/escseqparse.py +++ b/curtsies/escseqparse.py @@ -132,8 +132,7 @@ def peel_off_esc_code(s): def token_type(info): # type: (Token) -> Optional[List[Dict[Text, Union[Text, bool, None]]]] - """ - """ + if info["command"] == "m": # The default action for ESC[m is to act like ESC[0m # Ref: https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_codes From 69d51c900b9f357128053db8d8da2f400c3fe779 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 17 Jan 2021 11:06:58 +0100 Subject: [PATCH 145/302] Run black and codespell as Github Actions --- .github/workflows/lint.yaml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/lint.yaml diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 0000000..adddd5d --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,28 @@ +name: Linters + +on: + push: + pull_request: + +jobs: + black: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install black codespell + - name: Check with black + run: black --check . + + codespell: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: codespell-project/actions-codespell@master + with: + skip: '*.po' + ignore_words_list: ba,te,deltion From edbc76b5acb6bd279aa0271e663ea70ee53dae18 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 17 Jan 2021 11:22:35 +0100 Subject: [PATCH 146/302] Fix issues found by codespell --- .github/workflows/lint.yaml | 2 +- curtsies/formatstring.py | 4 ++-- curtsies/input.py | 2 +- curtsies/window.py | 6 +++--- docs/FSArray.rst | 2 +- docs/Input.rst | 2 +- docs/ansi.py | 2 +- docs/gameloop.rst | 2 +- notes/notesfromdarius.txt | 22 +++++++++++----------- tests/test_fmtstr.py | 2 +- 10 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index adddd5d..173c0d9 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -25,4 +25,4 @@ jobs: - uses: codespell-project/actions-codespell@master with: skip: '*.po' - ignore_words_list: ba,te,deltion + ignore_words_list: te,ot diff --git a/curtsies/formatstring.py b/curtsies/formatstring.py index f344d6b..6a83f06 100644 --- a/curtsies/formatstring.py +++ b/curtsies/formatstring.py @@ -461,7 +461,7 @@ def join(self, iterable): # TODO make this split work like str.split def split(self, sep=None, maxsplit=None, regex=False): # type: (Text, int, bool) -> List[FmtStr] - """Split based on seperator, optionally using a regex. + """Split based on separator, optionally using a regex. Capture groups are ignored in regex, the whole pattern is matched and used to split the original FmtStr.""" @@ -597,7 +597,7 @@ def __mul__(self, other): return sum([self for _ in range(other)], FmtStr()) raise TypeError("Can't multiply those") - # TODO ensure emtpy FmtStr isn't a problem + # TODO ensure empty FmtStr isn't a problem @property def shared_atts(self): diff --git a/curtsies/input.py b/curtsies/input.py index 6c7006a..c302d8e 100644 --- a/curtsies/input.py +++ b/curtsies/input.py @@ -333,7 +333,7 @@ def threadsafe_event_trigger(self, event_type): Returned callback function will create an event of type event_type which will interrupt an event request if one - is concurrently occuring, otherwise adding the event to a queue + is concurrently occurring, otherwise adding the event to a queue that will be checked on the next event request.""" readfd, writefd = os.pipe() self.readers.append(readfd) diff --git a/curtsies/window.py b/curtsies/window.py index 61286ed..8d224c8 100644 --- a/curtsies/window.py +++ b/curtsies/window.py @@ -152,7 +152,7 @@ def for_stdout(s): class FullscreenWindow(BaseWindow): - """2D-text rendering window that dissappears when its context is left + """2D-text rendering window that disappears when its context is left FullscreenWindow will only render arrays the size of the terminal or smaller, and leaves no trace on exit (like top or vim). It never @@ -280,7 +280,7 @@ def __init__( on leaving context hide_cursor (bool): Hides cursor while in context extra_bytes_callback (f(bytes) -> None): Will be called with extra - bytes inadvertantly read in get_cursor_position(). If not + bytes inadvertently read in get_cursor_position(). If not provided, a ValueError will be raised when this occurs. """ BaseWindow.__init__(self, out_stream=out_stream, hide_cursor=hide_cursor) @@ -337,7 +337,7 @@ def retrying_read(): c = in_stream.read(1) if c == "": raise ValueError( - "Stream should be blocking - should't" + "Stream should be blocking - shouldn't" " return ''. Returned %r so far", (resp,), ) diff --git a/docs/FSArray.rst b/docs/FSArray.rst index ab8be64..a4229ae 100644 --- a/docs/FSArray.rst +++ b/docs/FSArray.rst @@ -56,7 +56,7 @@ An array like the above might be repeatedly constructed and rendered with a :py: Slicing works like it does with a :py:class:`~curtsies.FmtStr`, but in two dimensions. :py:class:`~curtsies.FSArray` are *mutable*, so array assignment syntax can be used for natural -compositing as in the above exaple. +compositing as in the above example. If you're dealing with terminal output, the *width* of a string becomes more important than it's *length* (see :ref:`len-vs-width`). diff --git a/docs/Input.rst b/docs/Input.rst index 56d9f58..21aa989 100644 --- a/docs/Input.rst +++ b/docs/Input.rst @@ -26,7 +26,7 @@ and can be acted upon. Since it's iterable, ``next()`` can be used to wait for a single event. :py:meth:`~curtsies.Input.send` works like ``next()`` but takes a timeout in seconds, which if reached will cause None to be returned signalling -that no keypress or other event occured within the timeout. +that no keypress or other event occurred within the timeout. Key events are unicode strings, but sometimes event objects (see :class:`~curtsies.events.Event`) are returned instead. diff --git a/docs/ansi.py b/docs/ansi.py index 6e47709..f2df3a7 100644 --- a/docs/ansi.py +++ b/docs/ansi.py @@ -124,7 +124,7 @@ def _colorize_block_contents(self, block): last_end = 0 # iterate over all color codes for match in COLOR_PATTERN.finditer(raw): - # add any text preceeding this match + # add any text preceding this match head = raw[last_end : match.start()] self._add_text(head) # update the match end diff --git a/docs/gameloop.rst b/docs/gameloop.rst index 08385c3..154c25a 100644 --- a/docs/gameloop.rst +++ b/docs/gameloop.rst @@ -1,7 +1,7 @@ Gameloop Example ^^^^^^^^^^^^^^^^ -Use scheduled events for realtime interative programs: +Use scheduled events for realtime interactive programs: .. literalinclude:: ../examples/fps.py diff --git a/notes/notesfromdarius.txt b/notes/notesfromdarius.txt index 9021b0a..dd5a5eb 100644 --- a/notes/notesfromdarius.txt +++ b/notes/notesfromdarius.txt @@ -12,7 +12,7 @@ read this: http://www.cs.utexas.edu/~wcook/Drafts/2009/essay.pdf (erlang was also cool like that, maybe look at it again) -played at all with sounds fairly diff, cool in that syate very explicty very cool +played at all with sounds fairly diff, cool in that syate very explicitly very cool i assume you've seen how objc- does it? @@ -22,7 +22,7 @@ I was guessing it would use font, but I was wrong or font or something -check out Darius's halp - for editor <-> repl interaction without the repl +check out Darius's help - for editor <-> repl interaction without the repl I going to get some food @@ -33,10 +33,10 @@ this could also be pass this message right now it's not realtime it blocks on getting an event, and has no timer events or - butit'd be nice to change -I thoguht the simpler model was perfect for a repl - wait until user does somethign - but code can do anything +I thoguht the simpler model was perfect for a repl - wait until user does something - but code can do anything which way? -I guess I don't wan tto deal with threads unless each thread is an event driven loop <- I just don't hvae this yet +I guess I don't want tto deal with threads unless each thread is an event driven loop <- I just don't have this yet right now it's a single thread of execution @@ -51,7 +51,7 @@ run code run code oh code wants to read on stdin - so this is a greenet, suspend execution and -the realization I had was that in bptuhon-curtiess I 'm also emulating the terminal - I'm responsible for colelcting that the user hit a key at *any* time, not jsut during input +the realization I had was that in bptuhon-curtiess I 'm also emulating the terminal - I'm responsible for colelcting that the user hit a key at *any* time, not just during input so it really feels like an evented system? - at least queues and messaging and things @@ -90,8 +90,8 @@ could you only have i deeplly immutable - E not obvious what this does -then it's a boolean thing, so presence is enought -shoudl be documentation of that +then it's a boolean thing, so presence is enough +should be documentation of that self.atts is a dictionary of {fg:blue, bg:red, bold:Trueo @@ -101,7 +101,7 @@ I'm actually paying lea A. (hs'er) to make a logo! So if I ha Also if bpython starts to use this as ath main thing, I'lll need to work on cleanign it up. -I guess so I don't have release things set up - I could give you github comit access for this and just put it on another brnach for now? +I guess so I don't have release things set up - I could give you github commit access for this and just put it on another branch for now? git pull upstream @@ -110,11 +110,11 @@ the readme is the only docs right now mostly tests -I wrote the whole thing tried to do it clearly, then had performnace problems and went +I wrote the whole thing tried to do it clearly, then had performance problems and went just python slice bookkeeping -it's poorly named, it doens't accomplish that - it's just +it's poorly named, it doesn't accomplish that - it's just asdf[1:] normalize_slice(4, slice(1, None, None)) -> slice(1, 4, 1) @@ -149,7 +149,7 @@ meaning that sequence isn't recognizd or isn't valid this depends on every input having -if ab is passed it assumes it must have alrady been called with a +if ab is passed it assumes it must have already been called with a ther'e no speed concertn, we could assert that too diff --git a/tests/test_fmtstr.py b/tests/test_fmtstr.py index 941f78b..28ef608 100644 --- a/tests/test_fmtstr.py +++ b/tests/test_fmtstr.py @@ -347,7 +347,7 @@ def test_slice(self): self.assertEqual(s[1:], fmtstr("mp") + " ") self.assertEqual(blue("a\nb")[0:1], blue("a")) - # considering changing behavior so that this doens't work + # considering changing behavior so that this doesn't work # self.assertEqual(fmtstr('Hi!', 'blue')[15:18], fmtstr('', 'blue')) From 540f20ec31f1105f6c125b78165427d453edf59f Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 17 Jan 2021 11:23:13 +0100 Subject: [PATCH 147/302] No longer ignore curtsies/formatstring.py when running black --- curtsies/formatstring.py | 18 ++++++++++-------- pyproject.toml | 3 --- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/curtsies/formatstring.py b/curtsies/formatstring.py index 6a83f06..1da1536 100644 --- a/curtsies/formatstring.py +++ b/curtsies/formatstring.py @@ -57,20 +57,22 @@ ) one_arg_xforms = { - 'bold' : lambda s: seq(STYLES['bold']) +s+seq(RESET_ALL), - 'dark' : lambda s: seq(STYLES['dark']) +s+seq(RESET_ALL), - 'underline' : lambda s: seq(STYLES['underline'])+s+seq(RESET_ALL), - 'blink' : lambda s: seq(STYLES['blink']) +s+seq(RESET_ALL), - 'invert' : lambda s: seq(STYLES['invert']) +s+seq(RESET_ALL), + "bold": lambda s: seq(STYLES["bold"]) + s + seq(RESET_ALL), + "dark": lambda s: seq(STYLES["dark"]) + s + seq(RESET_ALL), + "underline": lambda s: seq(STYLES["underline"]) + s + seq(RESET_ALL), + "blink": lambda s: seq(STYLES["blink"]) + s + seq(RESET_ALL), + "invert": lambda s: seq(STYLES["invert"]) + s + seq(RESET_ALL), } # type: Mapping[Text, Callable[[Text], Text]] two_arg_xforms = { - 'fg' : lambda s, v: '{}{}{}'.format(seq(v), s, seq(RESET_FG)), - 'bg' : lambda s, v: seq(v)+s+seq(RESET_BG), + "fg": lambda s, v: "{}{}{}".format(seq(v), s, seq(RESET_FG)), + "bg": lambda s, v: seq(v) + s + seq(RESET_BG), } # type: Mapping[Text, Callable[[Text, int], Text]] # TODO unused, remove this in next major release -xforms = {} # type: MutableMapping[Text, Union[Callable[[Text], Text], Callable[[Text, int], Text]]] +xforms = ( + {} +) # type: MutableMapping[Text, Union[Callable[[Text], Text], Callable[[Text, int], Text]]] xforms.update(one_arg_xforms) xforms.update(two_arg_xforms) diff --git a/pyproject.toml b/pyproject.toml index 9a27585..40837b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,9 +13,6 @@ exclude = ''' | stubs )/ | bootstrap.py - | curtsies/formatstring.py - | curtsies/formatstringarray.py | docs/conf.py - ) ''' From f4fada910b094129e3bd88323b3093fc7532c692 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 17 Jan 2021 11:25:56 +0100 Subject: [PATCH 148/302] Fix issues found by codespell --- notes/notesfromdarius.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/notes/notesfromdarius.txt b/notes/notesfromdarius.txt index dd5a5eb..a921366 100644 --- a/notes/notesfromdarius.txt +++ b/notes/notesfromdarius.txt @@ -78,7 +78,7 @@ MOre thoughts on Curtsies on May 28 -I dont' know why, but it's nice for right consuming other input i guess - though I don't do a good enough job +I don't know why, but it's nice for right consuming other input i guess - though I don't do a good enough job the structure is the BaseFmtStrs that are immutable strings with properties like blue, and then FmtStrs are lists of those @@ -185,7 +185,7 @@ keys_in_buffer? chars_available -sigints that happen in this curtsies cdoe to generate the events, but if we not in that, they dont' have to - specifically for bpython +sigints that happen in this curtsies cdoe to generate the events, but if we not in that, they don't have to - specifically for bpython in cbreak ctrl-c still causes a handler to happen, so maybe I don't need custon ones at all, just let bpython do thatk From 94ac720568bf793be6987227bd76e7be3ecd938c Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 22 Jan 2021 15:31:30 +0100 Subject: [PATCH 149/302] setup.cfg: don't build universal wheels since Python 2 is no longer supported --- setup.cfg | 2 -- 1 file changed, 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 4455747..dc046a1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,3 @@ -[bdist_wheel] -universal = 1 [mypy] warn_return_any = True warn_unused_configs = True From 3f1648e2b2bae027e64d6ea9d25645aa59499207 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 22 Jan 2021 16:06:19 +0100 Subject: [PATCH 150/302] Set supported Python versions in setup.py --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 7f985c7..7fd7c9d 100644 --- a/setup.py +++ b/setup.py @@ -28,6 +28,7 @@ def long_description(): author_email="thomasballinger@gmail.com", license="MIT", packages=["curtsies"], + python_requires=">=3.6", install_requires=[ "blessings>=1.5", "wcwidth>=0.1.4", From 91d7043850d2f6fce178672123c68f3776367e3a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 22 Jan 2021 16:07:41 +0100 Subject: [PATCH 151/302] Rename readme.md to make setuptools happy --- MANIFEST.in | 1 - readme.md => README.md | 0 setup.py | 2 +- 3 files changed, 1 insertion(+), 2 deletions(-) rename readme.md => README.md (100%) diff --git a/MANIFEST.in b/MANIFEST.in index a7452a3..d7a857b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,3 @@ include LICENSE include tests/*.py include examples/*.py -include readme.md diff --git a/readme.md b/README.md similarity index 100% rename from readme.md rename to README.md diff --git a/setup.py b/setup.py index 7fd7c9d..fdabdf7 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ def version(): def long_description(): - with open("readme.md", encoding="utf-8") as f: + with open("README.md", encoding="utf-8") as f: return f.read() From 6b0a8a6b4c6ca9418d3133cb5d147fc284b6e849 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 22 Jan 2021 16:29:59 +0100 Subject: [PATCH 152/302] Publish to PyPI using Github Actions --- .github/workflows/publish-twine.yaml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/publish-twine.yaml diff --git a/.github/workflows/publish-twine.yaml b/.github/workflows/publish-twine.yaml new file mode 100644 index 0000000..09c4cf5 --- /dev/null +++ b/.github/workflows/publish-twine.yaml @@ -0,0 +1,26 @@ +name: Upload Python Package + +on: + release: + types: [created] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools twine "blessings>=1.5" "wcwidth>=0.1.4" + - name: Build and publish + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + python setup.py sdist + twine upload dist/* From 43eeee46182a6b6cdadff9285bc911cac02031b7 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 23 Jan 2021 16:27:16 +0100 Subject: [PATCH 153/302] Switch to cwcwidth --- .github/workflows/build.yaml | 2 +- curtsies/formatstring.py | 2 +- docs/FmtStr.rst | 2 +- setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 14699c4..7426efb 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -22,7 +22,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install "blessings>=1.5" "wcwidth>=0.1.4" pyte pytest + pip install "blessings>=1.5" cwcwidth pyte pytest - name: Build with Python ${{ matrix.python-version }} run: | python setup.py build diff --git a/curtsies/formatstring.py b/curtsies/formatstring.py index 1da1536..7477ab6 100644 --- a/curtsies/formatstring.py +++ b/curtsies/formatstring.py @@ -41,7 +41,7 @@ import itertools import re import sys -from wcwidth import wcswidth +from cwcwidth import wcswidth from .escseqparse import parse, remove_ansi from .termformatconstants import ( diff --git a/docs/FmtStr.rst b/docs/FmtStr.rst index e029ac8..5464c2f 100644 --- a/docs/FmtStr.rst +++ b/docs/FmtStr.rst @@ -218,7 +218,7 @@ To access this information, :py:class:`~curtsies.FmtStr` objects have a :py:clas >>> len(combined), combined.width, combined.s (2, 1, u'a\u0324') -As shown above, `full width characters `_ can take up two columns, and `combining characters `_ may be combined with the previous character to form a single grapheme. Curtsies uses a `Python implementation of wcwidth `_ to do this calculation. +As shown above, `full width characters `_ can take up two columns, and `combining characters `_ may be combined with the previous character to form a single grapheme. Curtsies uses `Python bindings of wcwidth `_ to do this calculation. FmtStr - API Docs ================= diff --git a/setup.py b/setup.py index fdabdf7..ffe6796 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ def long_description(): python_requires=">=3.6", install_requires=[ "blessings>=1.5", - "wcwidth>=0.1.4", + "cwcwidth", ], tests_require=[ "pyte", From e1676d36bda672f62508c4ffcb458aa04c77d37e Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 24 Jan 2021 15:06:25 +0100 Subject: [PATCH 154/302] Use wcwidth where it makes sense --- curtsies/formatstring.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/curtsies/formatstring.py b/curtsies/formatstring.py index 7477ab6..327c65d 100644 --- a/curtsies/formatstring.py +++ b/curtsies/formatstring.py @@ -41,7 +41,7 @@ import itertools import re import sys -from cwcwidth import wcswidth +from cwcwidth import wcswidth, wcwidth from .escseqparse import parse, remove_ansi from .termformatconstants import ( @@ -235,7 +235,7 @@ def __init__(self, chunk): self.internal_width = 0 # width of chunks.s[:self.internal_offset] divides = [0] for c in self.chunk.s: - divides.append(divides[-1] + wcswidth(c)) + divides.append(divides[-1] + wcwidth(c)) self.divides = divides def reinit(self, chunk): @@ -790,7 +790,7 @@ def width_aware_slice(s, start, end, replacement_char=" "): """ divides = [0] for c in s: - divides.append(divides[-1] + wcswidth(c)) + divides.append(divides[-1] + wcwidth(c)) new_chunk_chars = [] for char, char_start, char_end in zip(s, divides[:-1], divides[1:]): From 683bc066dbf5d9397b737c6c052bebe3745fc201 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 24 Jan 2021 15:07:10 +0100 Subject: [PATCH 155/302] Specify length instead of splicing --- curtsies/formatstring.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/curtsies/formatstring.py b/curtsies/formatstring.py index 327c65d..7d0880d 100644 --- a/curtsies/formatstring.py +++ b/curtsies/formatstring.py @@ -557,7 +557,7 @@ def width_at_offset(self, n): # type: (int) -> int """Returns the horizontal position of character n of the string""" # TODO make more efficient? - width = wcswidth(self.s[:n]) + width = wcswidth(self.s, n) assert width != -1 return width From 7393fa7e620b2a21818dc65e3ab37d8fe45da0a5 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 24 Jan 2021 15:41:39 +0100 Subject: [PATCH 156/302] Fix shape check --- tests/test_fmtstr.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_fmtstr.py b/tests/test_fmtstr.py index 28ef608..386a915 100644 --- a/tests/test_fmtstr.py +++ b/tests/test_fmtstr.py @@ -582,8 +582,8 @@ def assertFSArraysEqual(self, a, b): self.assertIsInstance(a, FSArray) self.assertIsInstance(b, FSArray) self.assertEqual( - (a.width, b.height), - (a.width, b.height), + (a.width, a.height), + (b.width, b.height), f"fsarray dimensions do not match: {a.shape} {b.shape}", ) for i, (a_row, b_row) in enumerate(zip(a, b)): From 742a4caf412d609e09a37612fef7cba01a36c6d2 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 24 Jan 2021 15:55:26 +0100 Subject: [PATCH 157/302] Add a TODO --- tests/test_fmtstr.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_fmtstr.py b/tests/test_fmtstr.py index 386a915..d3fe754 100644 --- a/tests/test_fmtstr.py +++ b/tests/test_fmtstr.py @@ -558,6 +558,7 @@ def test_no_hanging_space(self): def test_assignment_working(self): t = FSArray(10, 10) t[2, 2] = "a" + # TODO: is this supposed to check something? t[2, 2] == "a" def test_normalize_slice(self): From 93d9c5d66ff69e8840e46bcf38366c1d9f110a82 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 24 Jan 2021 16:29:04 +0100 Subject: [PATCH 158/302] Re-add helper functions to assert equality of FSArray instances --- curtsies/formatstringarray.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/curtsies/formatstringarray.py b/curtsies/formatstringarray.py index f70a87f..335247c 100644 --- a/curtsies/formatstringarray.py +++ b/curtsies/formatstringarray.py @@ -297,6 +297,37 @@ def simple_format(x): return "\n".join(actualize(l) for l in x) +def assertFSArraysEqual(a, b): + # type: (FSArray, FSArray) -> None + assert isinstance(a, FSArray) + assert isinstance(b, FSArray) + assert ( + a.width == b.width and a.hight == b.hight + ), f"fsarray dimensions do not match: {a.shape} {b.shape}" + for i, (a_row, b_row) in enumerate(zip(a, b)): + assert a_row == b_row, "FSArrays differ first on line {}:\n{}".format( + i, FSArray.diff(a, b) + ) + + +def assertFSArraysEqualIgnoringFormatting(a, b): + # type: (FSArray, FSArray) -> None + """Also accepts arrays of strings""" + assert len(a) == len(b), "fsarray heights do not match: %s %s \n%s \n%s" % ( + len(a), + len(b), + simple_format(a), + simple_format(b), + ) + for i, (a_row, b_row) in enumerate(zip(a, b)): + a_row = a_row.s if isinstance(a_row, FmtStr) else a_row + b_row = b_row.s if isinstance(b_row, FmtStr) else b_row + assert a_row == b_row, "FSArrays differ first on line %s:\n%s" % ( + i, + FSArray.diff(a, b, ignore_formatting=True), + ) + + if __name__ == "__main__": a = FSArray(3, 14, bg="blue") a[0:2, 5:11] = cast( From a96bf4e052aec2a60e7d174ed7fb2793334db578 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 24 Jan 2021 16:37:20 +0100 Subject: [PATCH 159/302] Bump version to 0.3.5 --- CHANGELOG.md | 5 +++-- curtsies/__init__.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8ef0e2..54b1fbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,11 @@ # Changelog -## [Unreleased] +## [0.3.5] - 2021-01-24 - Drop supported for Python 2, 3.4 and 3.5. - Migrate to pytest. Thanks to Paolo Stivanin - Add new exmples. Thanks to rybarczykj -- mprove error messages. Thanks to Etienne Richart +- Improve error messages. Thanks to Etienne Richart +- Replace wcwidth with cwcwidth ## [0.3.4] - 2020-07-15 - Prevent crash when embedding in situations including the lldb debugger. Thanks Nathan Lanza! diff --git a/curtsies/__init__.py b/curtsies/__init__.py index 621474e..cc4514b 100644 --- a/curtsies/__init__.py +++ b/curtsies/__init__.py @@ -1,5 +1,5 @@ """Terminal-formatted strings""" -__version__ = "0.3.4" +__version__ = "0.3.5" from .window import FullscreenWindow, CursorAwareWindow from .input import Input From 6b513ae54de0c9a27fb9d02187bd68e89fdcab73 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 24 Jan 2021 16:39:05 +0100 Subject: [PATCH 160/302] Fix typo --- curtsies/formatstringarray.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/curtsies/formatstringarray.py b/curtsies/formatstringarray.py index 335247c..fb62abc 100644 --- a/curtsies/formatstringarray.py +++ b/curtsies/formatstringarray.py @@ -302,7 +302,7 @@ def assertFSArraysEqual(a, b): assert isinstance(a, FSArray) assert isinstance(b, FSArray) assert ( - a.width == b.width and a.hight == b.hight + a.width == b.width and a.height == b.height ), f"fsarray dimensions do not match: {a.shape} {b.shape}" for i, (a_row, b_row) in enumerate(zip(a, b)): assert a_row == b_row, "FSArrays differ first on line {}:\n{}".format( From 20694b2246ec0dd3490a821f3d98aeefbe4f6ec5 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 28 Jan 2021 22:49:24 +0100 Subject: [PATCH 161/302] Update PyPI action --- .github/workflows/publish-twine.yaml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/publish-twine.yaml b/.github/workflows/publish-twine.yaml index 09c4cf5..2cf0914 100644 --- a/.github/workflows/publish-twine.yaml +++ b/.github/workflows/publish-twine.yaml @@ -16,11 +16,12 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install setuptools twine "blessings>=1.5" "wcwidth>=0.1.4" - - name: Build and publish - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + pip install setuptools "blessings>=1.5" cwcwidth + - name: Build sdist run: | python setup.py sdist - twine upload dist/* + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@master + with: + user: __token__ + password: ${{ secrets.PYPI_PASSWORD }} From b14ae9f635709d5776b7216663f2c53c7e28e9c2 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 29 Jan 2021 20:20:17 +0100 Subject: [PATCH 162/302] Fix argument type for termios.tcsetattr --- curtsies/termhelpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/curtsies/termhelpers.py b/curtsies/termhelpers.py index a1d95f7..bd138de 100644 --- a/curtsies/termhelpers.py +++ b/curtsies/termhelpers.py @@ -6,7 +6,7 @@ from typing import IO, Type, List, Union, Optional from types import TracebackType -_Attr = List[Union[int, List[bytes]]] +_Attr = List[Union[int, List[Union[bytes, int]]]] class Nonblocking: From 87d2257f713f3ba310501fb5d5cc6366dc693941 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 29 Jan 2021 20:45:50 +0100 Subject: [PATCH 163/302] Use f-string --- curtsies/termformatconstants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/curtsies/termformatconstants.py b/curtsies/termformatconstants.py index 155e3f9..5c44402 100644 --- a/curtsies/termformatconstants.py +++ b/curtsies/termformatconstants.py @@ -18,4 +18,4 @@ def seq(num): # type: (int) -> str - return "[%sm" % num + return f"[{num}m" From 36c8613d4aaf36d6fb5ab50f5f31db19dc65e505 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 29 Jan 2021 20:52:02 +0100 Subject: [PATCH 164/302] Remove some unnecessary lists --- curtsies/termformatconstants.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/curtsies/termformatconstants.py b/curtsies/termformatconstants.py index 5c44402..4892f22 100644 --- a/curtsies/termformatconstants.py +++ b/curtsies/termformatconstants.py @@ -2,18 +2,22 @@ from typing import Mapping -# fmt: off -colors = 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'gray' -FG_COLORS = dict(list(zip(colors, list(range(30, 38))))) # type: Mapping[str, int] -BG_COLORS = dict(list(zip(colors, list(range(40, 48))))) # type: Mapping[str, int] -STYLES = dict(list(zip(('bold', 'dark', 'underline', 'blink', 'invert'), [1,2,4,5,7]))) # type: Mapping[str, int] -FG_NUMBER_TO_COLOR = dict(zip(FG_COLORS.values(), FG_COLORS.keys())) # type: Mapping[int, str] -BG_NUMBER_TO_COLOR = dict(zip(BG_COLORS.values(), BG_COLORS.keys())) # type: Mapping[int, str] +colors = "black", "red", "green", "yellow", "blue", "magenta", "cyan", "gray" +FG_COLORS = dict(zip(colors, range(30, 38))) # type: Mapping[str, int] +BG_COLORS = dict(zip(colors, range(40, 48))) # type: Mapping[str, int] +STYLES = dict( + zip(("bold", "dark", "underline", "blink", "invert"), (1, 2, 4, 5, 7)) +) # type: Mapping[str, int] +FG_NUMBER_TO_COLOR = dict( + zip(FG_COLORS.values(), FG_COLORS.keys()) +) # type: Mapping[int, str] +BG_NUMBER_TO_COLOR = dict( + zip(BG_COLORS.values(), BG_COLORS.keys()) +) # type: Mapping[int, str] NUMBER_TO_STYLE = dict(zip(STYLES.values(), STYLES.keys())) RESET_ALL = 0 RESET_FG = 39 RESET_BG = 49 -# fmt: on def seq(num): From db530d50748944ab035075a56f771d6f490a3145 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 29 Jan 2021 20:56:34 +0100 Subject: [PATCH 165/302] Remove useless assignment --- curtsies/input.py | 1 - 1 file changed, 1 deletion(-) diff --git a/curtsies/input.py b/curtsies/input.py index c302d8e..48efe28 100644 --- a/curtsies/input.py +++ b/curtsies/input.py @@ -212,7 +212,6 @@ def find_key(): full=len(self.unprocessed_bytes) == 0, ) if e is not None: - current_bytes = [] return e if current_bytes: # incomplete keys shouldn't happen raise ValueError("Couldn't identify key sequence: %r" % current_bytes) From eacd0d46341388bf6f6378c16807d9786bf4fc78 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 29 Jan 2021 21:18:56 +0100 Subject: [PATCH 166/302] Reformat and remove unused import --- curtsies/escseqparse.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/curtsies/escseqparse.py b/curtsies/escseqparse.py index 7455089..7e8053a 100644 --- a/curtsies/escseqparse.py +++ b/curtsies/escseqparse.py @@ -20,7 +20,6 @@ Dict, Any, Optional, - NewType, ) import re @@ -138,15 +137,21 @@ def token_type(info): # Ref: https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_codes values = cast(List[int], info["numbers"]) if len(info["numbers"]) else [0] tokens = [] # type: List[Dict[str, Union[Text, bool, None]]] - # fmt: off for value in values: - if value in FG_NUMBER_TO_COLOR: tokens.append({'fg':FG_NUMBER_TO_COLOR[value]}) - if value in BG_NUMBER_TO_COLOR: tokens.append({'bg':BG_NUMBER_TO_COLOR[value]}) - if value in NUMBER_TO_STYLE: tokens.append({NUMBER_TO_STYLE[value]:True}) - if value == RESET_ALL: tokens.append(dict({k: None for k in STYLES}, **{'fg':None, 'bg':None})) - if value == RESET_FG: tokens.append({'fg':None}) - if value == RESET_BG: tokens.append({'bg':None}) - # fmt: on + if value in FG_NUMBER_TO_COLOR: + tokens.append({"fg": FG_NUMBER_TO_COLOR[value]}) + if value in BG_NUMBER_TO_COLOR: + tokens.append({"bg": BG_NUMBER_TO_COLOR[value]}) + if value in NUMBER_TO_STYLE: + tokens.append({NUMBER_TO_STYLE[value]: True}) + if value == RESET_ALL: + tokens.append( + dict({k: None for k in STYLES}, **{"fg": None, "bg": None}) + ) + if value == RESET_FG: + tokens.append({"fg": None}) + if value == RESET_BG: + tokens.append({"bg": None}) if tokens: return tokens From 189ac58fc91ea9b32415ab616af517f7d21282c5 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 29 Jan 2021 21:21:50 +0100 Subject: [PATCH 167/302] Replace list comprehension with generator --- curtsies/formatstring.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/curtsies/formatstring.py b/curtsies/formatstring.py index 7d0880d..a331b62 100644 --- a/curtsies/formatstring.py +++ b/curtsies/formatstring.py @@ -431,7 +431,7 @@ def splice(self, new_str, start, end=None): new_components.extend(new_fs.chunks) inserted = True - return FmtStr(*[s for s in new_components if s.s]) + return FmtStr(*(s for s in new_components if s.s)) def append(self, string): # type: (Union[Text, FmtStr]) -> FmtStr @@ -441,7 +441,7 @@ def copy_with_new_atts(self, **attributes): # type: (**Union[bool, int]) -> FmtStr """Returns a new FmtStr with the same content but new formatting""" - result = FmtStr(*[Chunk(bfs.s, bfs.atts.extend(attributes)) for bfs in self.chunks]) # type: ignore + result = FmtStr(*(Chunk(bfs.s, bfs.atts.extend(attributes)) for bfs in self.chunks)) # type: ignore return result def join(self, iterable): @@ -596,7 +596,7 @@ def __radd__(self, other): def __mul__(self, other): # type: (int) -> FmtStr if isinstance(other, int): - return sum([self for _ in range(other)], FmtStr()) + return sum((self for _ in range(other)), FmtStr()) raise TypeError("Can't multiply those") # TODO ensure empty FmtStr isn't a problem @@ -622,7 +622,7 @@ def new_with_atts_removed(self, *attributes): # type: (*Text) -> FmtStr """Returns a new FmtStr with the same content but some attributes removed""" - result = FmtStr(*[Chunk(bfs.s, bfs.atts.remove(*attributes)) for bfs in self.chunks]) # type: ignore + result = FmtStr(*(Chunk(bfs.s, bfs.atts.remove(*attributes)) for bfs in self.chunks)) # type: ignore return result @no_type_check From 4bebee8d2649d3cd6b0fed1a8c1a704e993cb2b5 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 29 Jan 2021 21:34:57 +0100 Subject: [PATCH 168/302] Simplifciations --- curtsies/formatstring.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/curtsies/formatstring.py b/curtsies/formatstring.py index a331b62..65c8010 100644 --- a/curtsies/formatstring.py +++ b/curtsies/formatstring.py @@ -121,12 +121,10 @@ class Chunk: def __init__(self, string, atts=None): # type: (Text, Mapping[str, Union[int, bool]]) -> None - if atts is None: - atts = {} if not isinstance(string, str): raise ValueError("unicode string required, got %r" % string) self._s = string # type: Text - self._atts = FrozenDict(atts) + self._atts = FrozenDict(atts if atts else {}) @property def s(self): From 3deb1cbd5a646141e7b7e30f31af143d95e4cea8 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 29 Jan 2021 21:44:22 +0100 Subject: [PATCH 169/302] Remove unused index --- curtsies/formatstring.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/curtsies/formatstring.py b/curtsies/formatstring.py index 65c8010..df70566 100644 --- a/curtsies/formatstring.py +++ b/curtsies/formatstring.py @@ -447,7 +447,7 @@ def join(self, iterable): """Joins an iterable yielding strings or FmtStrs with self as separator""" before = [] # type: List[Chunk] chunks = [] # type: List[Chunk] - for i, s in enumerate(iterable): + for s in iterable: chunks.extend(before) before = self.chunks if isinstance(s, FmtStr): From 97d8c6074a4c7b814d9da41a4aadae18ec0a63fb Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 11 Feb 2021 00:29:54 +0100 Subject: [PATCH 170/302] GA: also test on macos-latest --- .github/workflows/build.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 7426efb..5d212dc 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -9,9 +9,10 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} strategy: matrix: + os: [ubuntu-latest, macos-latest] python-version: [3.6, 3.7, 3.8, 3.9, pypy3] steps: - uses: actions/checkout@v2 From fe85cdfd49430a1ee3f54d85df41d43e5b86e303 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 11 Feb 2021 00:34:37 +0100 Subject: [PATCH 171/302] wcwidth stubs are no longer required --- stubs/wcwidth.pyi | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 stubs/wcwidth.pyi diff --git a/stubs/wcwidth.pyi b/stubs/wcwidth.pyi deleted file mode 100644 index c1decf2..0000000 --- a/stubs/wcwidth.pyi +++ /dev/null @@ -1,3 +0,0 @@ -def wcswidth(pwcs, n=None): - # type: (str, int) -> int - pass From 4034dd293ad0621c84133e790a5e60f6343cb371 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 11 Feb 2021 00:47:04 +0100 Subject: [PATCH 172/302] Re-arrange imports --- curtsies/events.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/curtsies/events.py b/curtsies/events.py index 2380eb2..65c8f6d 100644 --- a/curtsies/events.py +++ b/curtsies/events.py @@ -1,11 +1,11 @@ """Events for keystrokes and other input events""" -import sys -import encodings import codecs +import encodings +import sys +from typing import Text, Optional, List, Union from .termhelpers import Termmode - -from typing import Text, Optional, List, Union +from .curtsieskeys import CURTSIES_NAMES as special_curtsies_names chr_byte = lambda i: chr(i).encode("latin-1") chr_uni = chr @@ -23,8 +23,6 @@ for i in range(0x00, 0x1B): # Overwrite the control keys with better labels CURTSIES_NAMES[chr_byte(i + 0x80)] = "" % chr(i + 0x40) -from .curtsieskeys import CURTSIES_NAMES as special_curtsies_names - CURTSIES_NAMES.update(special_curtsies_names) CURSES_NAMES = {} From 2c91b099361a1c2d7b821b731a2582c21efe0d8c Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 11 Feb 2021 00:47:19 +0100 Subject: [PATCH 173/302] Avoid temporary list allocations --- curtsies/events.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/curtsies/events.py b/curtsies/events.py index 65c8f6d..907108c 100644 --- a/curtsies/events.py +++ b/curtsies/events.py @@ -1,6 +1,7 @@ """Events for keystrokes and other input events""" import codecs import encodings +import itertools import sys from typing import Text, Optional, List, Union @@ -80,7 +81,7 @@ KEYMAP_PREFIXES.add(k[:i]) MAX_KEYPRESS_SIZE = max( - len(seq) for seq in (list(CURSES_NAMES.keys()) + list(CURTSIES_NAMES.keys())) + len(seq) for seq in itertools.chain(CURSES_NAMES.keys(), CURTSIES_NAMES.keys()) ) From c8c5791b0b5db4fc499670ea55370a3cd987b7c5 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 11 Feb 2021 00:52:15 +0100 Subject: [PATCH 174/302] Directly initialize dict --- curtsies/events.py | 88 ++++++++++++++++++++++------------------------ 1 file changed, 42 insertions(+), 46 deletions(-) diff --git a/curtsies/events.py b/curtsies/events.py index 907108c..1993399 100644 --- a/curtsies/events.py +++ b/curtsies/events.py @@ -26,52 +26,48 @@ CURTSIES_NAMES.update(special_curtsies_names) -CURSES_NAMES = {} -CURSES_NAMES[b"\x1bOP"] = "KEY_F(1)" -CURSES_NAMES[b"\x1bOQ"] = "KEY_F(2)" -CURSES_NAMES[b"\x1bOR"] = "KEY_F(3)" -CURSES_NAMES[b"\x1bOS"] = "KEY_F(4)" -CURSES_NAMES[b"\x1b[15~"] = "KEY_F(5)" -CURSES_NAMES[b"\x1b[17~"] = "KEY_F(6)" -CURSES_NAMES[b"\x1b[18~"] = "KEY_F(7)" -CURSES_NAMES[b"\x1b[19~"] = "KEY_F(8)" -CURSES_NAMES[b"\x1b[20~"] = "KEY_F(9)" -CURSES_NAMES[b"\x1b[21~"] = "KEY_F(10)" -CURSES_NAMES[b"\x1b[23~"] = "KEY_F(11)" -CURSES_NAMES[b"\x1b[24~"] = "KEY_F(12)" - -# see bpython #626 -CURSES_NAMES[b"\x1b[11~"] = "KEY_F(1)" -CURSES_NAMES[b"\x1b[12~"] = "KEY_F(2)" -CURSES_NAMES[b"\x1b[13~"] = "KEY_F(3)" -CURSES_NAMES[b"\x1b[14~"] = "KEY_F(4)" - -CURSES_NAMES[b"\x1b[A"] = "KEY_UP" -CURSES_NAMES[b"\x1b[B"] = "KEY_DOWN" -CURSES_NAMES[b"\x1b[C"] = "KEY_RIGHT" -CURSES_NAMES[b"\x1b[D"] = "KEY_LEFT" -CURSES_NAMES[b"\x1b[F"] = "KEY_END" # https://github.com/bpython/bpython/issues/490 -CURSES_NAMES[b"\x1b[H"] = "KEY_HOME" # https://github.com/bpython/bpython/issues/490 -CURSES_NAMES[b"\x08"] = "KEY_BACKSPACE" -CURSES_NAMES[b"\x1b[Z"] = "KEY_BTAB" - -# see curtsies #78 - taken from https://github.com/jquast/blessed/blob/e9ad7b85dfcbbba49010ab8c13e3a5920d81b010/blessed/keyboard.py#L409 -# fmt: off -CURSES_NAMES[b'\x1b[1~'] = 'KEY_FIND' # find -CURSES_NAMES[b'\x1b[2~'] = 'KEY_IC' # insert (0) -CURSES_NAMES[b'\x1b[3~'] = 'KEY_DC' # delete (.), "Execute" -CURSES_NAMES[b'\x1b[4~'] = 'KEY_SELECT' # select -CURSES_NAMES[b'\x1b[5~'] = 'KEY_PPAGE' # pgup (9) -CURSES_NAMES[b'\x1b[6~'] = 'KEY_NPAGE' # pgdown (3) -CURSES_NAMES[b'\x1b[7~'] = 'KEY_HOME' # home -CURSES_NAMES[b'\x1b[8~'] = 'KEY_END' # end -CURSES_NAMES[b'\x1b[OA'] = 'KEY_UP' # up (8) -CURSES_NAMES[b'\x1b[OB'] = 'KEY_DOWN' # down (2) -CURSES_NAMES[b'\x1b[OC'] = 'KEY_RIGHT' # right (6) -CURSES_NAMES[b'\x1b[OD'] = 'KEY_LEFT' # left (4) -CURSES_NAMES[b'\x1b[OF'] = 'KEY_END' # end (1) -CURSES_NAMES[b'\x1b[OH'] = 'KEY_HOME' # home (7) -# fmt: on +CURSES_NAMES = { + b"\x1bOP": "KEY_F(1)", + b"\x1bOQ": "KEY_F(2)", + b"\x1bOR": "KEY_F(3)", + b"\x1bOS": "KEY_F(4)", + b"\x1b[15~": "KEY_F(5)", + b"\x1b[17~": "KEY_F(6)", + b"\x1b[18~": "KEY_F(7)", + b"\x1b[19~": "KEY_F(8)", + b"\x1b[20~": "KEY_F(9)", + b"\x1b[21~": "KEY_F(10)", + b"\x1b[23~": "KEY_F(11)", + b"\x1b[24~": "KEY_F(12)", + # see bpython #626 + b"\x1b[11~": "KEY_F(1)", + b"\x1b[12~": "KEY_F(2)", + b"\x1b[13~": "KEY_F(3)", + b"\x1b[14~": "KEY_F(4)", + b"\x1b[A": "KEY_UP", + b"\x1b[B": "KEY_DOWN", + b"\x1b[C": "KEY_RIGHT", + b"\x1b[D": "KEY_LEFT", + b"\x1b[F": "KEY_END", # https://github.com/bpython/bpython/issues/490 + b"\x1b[H": "KEY_HOME", # https://github.com/bpython/bpython/issues/490 + b"\x08": "KEY_BACKSPACE", + b"\x1b[Z": "KEY_BTAB", + # see curtsies #78 - taken from https://github.com/jquast/blessed/blob/e9ad7b85dfcbbba49010ab8c13e3a5920d81b010/blessed/keyboard.py#L409 + b"\x1b[1~": "KEY_FIND", # find + b"\x1b[2~": "KEY_IC", # insert (0) + b"\x1b[3~": "KEY_DC", # delete (.), "Execute" + b"\x1b[4~": "KEY_SELECT", # select + b"\x1b[5~": "KEY_PPAGE", # pgup (9) + b"\x1b[6~": "KEY_NPAGE", # pgdown (3) + b"\x1b[7~": "KEY_HOME", # home + b"\x1b[8~": "KEY_END", # end + b"\x1b[OA": "KEY_UP", # up (8) + b"\x1b[OB": "KEY_DOWN", # down (2) + b"\x1b[OC": "KEY_RIGHT", # right (6) + b"\x1b[OD": "KEY_LEFT", # left (4) + b"\x1b[OF": "KEY_END", # end (1) + b"\x1b[OH": "KEY_HOME", # home (7) +} KEYMAP_PREFIXES = set() for table in (CURSES_NAMES, CURTSIES_NAMES): From d21563ec14d217d4a9dacc2a35f60172eee35f82 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 11 Feb 2021 00:58:13 +0100 Subject: [PATCH 175/302] Replace Text with str --- curtsies/escseqparse.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/curtsies/escseqparse.py b/curtsies/escseqparse.py index 7e8053a..2072173 100644 --- a/curtsies/escseqparse.py +++ b/curtsies/escseqparse.py @@ -10,7 +10,6 @@ """ from typing import ( - Text, List, Mapping, Union, @@ -35,16 +34,14 @@ ) -Token = Dict[str, Union[Text, List[int]]] +Token = Dict[str, Union[str, List[int]]] -def remove_ansi(s): - # type: (Text) -> Text +def remove_ansi(s: str) -> str: return re.sub(r"(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]", "", s) -def parse(s): - # type: (Text) -> List[Union[Text, Dict[str, Union[str, bool, None]]]] +def parse(s: str) -> List[Union[str, Dict[str, Union[str, bool, None]]]]: r""" Returns a list of strings or format dictionaries to describe the strings. @@ -55,7 +52,7 @@ def parse(s): >>> parse("\x1b[33m[\x1b[39m\x1b[33m]\x1b[39m\x1b[33m[\x1b[39m\x1b[33m]\x1b[39m\x1b[33m[\x1b[39m\x1b[33m]\x1b[39m\x1b[33m[\x1b[39m") [{'fg': 'yellow'}, '[', {'fg': None}, {'fg': 'yellow'}, ']', {'fg': None}, {'fg': 'yellow'}, '[', {'fg': None}, {'fg': 'yellow'}, ']', {'fg': None}, {'fg': 'yellow'}, '[', {'fg': None}, {'fg': 'yellow'}, ']', {'fg': None}, {'fg': 'yellow'}, '[', {'fg': None}] """ - stuff = [] # type: List[Union[Text, Dict[str, Union[str, bool, None]]]] + stuff = [] # type: List[Union[str, Dict[str, Union[str, bool, None]]]] rest = s while True: front, token, rest = peel_off_esc_code(rest) @@ -76,8 +73,7 @@ def parse(s): return stuff -def peel_off_esc_code(s): - # type: (Text) -> Tuple[Text, Optional[Token], Text] +def peel_off_esc_code(s: str) -> Tuple[str, Optional[Token], str]: r"""Returns processed text, the next token, and unprocessed text >>> front, d, rest = peel_off_esc_code('somestuff') @@ -129,14 +125,12 @@ def peel_off_esc_code(s): return s, None, "" -def token_type(info): - # type: (Token) -> Optional[List[Dict[Text, Union[Text, bool, None]]]] - +def token_type(info: Token) -> Optional[List[Dict[str, Union[str, bool, None]]]]: if info["command"] == "m": # The default action for ESC[m is to act like ESC[0m # Ref: https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_codes values = cast(List[int], info["numbers"]) if len(info["numbers"]) else [0] - tokens = [] # type: List[Dict[str, Union[Text, bool, None]]] + tokens = [] # type: List[Dict[str, Union[str, bool, None]]] for value in values: if value in FG_NUMBER_TO_COLOR: tokens.append({"fg": FG_NUMBER_TO_COLOR[value]}) From e8dccf4a70a07f771f182feec8bf0a4b7cbe3bff Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 11 Feb 2021 01:14:38 +0100 Subject: [PATCH 176/302] Save a call to signal.getsignal --- curtsies/input.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/curtsies/input.py b/curtsies/input.py index 48efe28..fafee66 100644 --- a/curtsies/input.py +++ b/curtsies/input.py @@ -31,17 +31,18 @@ def is_main_thread(): class ReplacedSigIntHandler: - def __init__(self, handler): - # type: (Callable) -> None + def __init__(self, handler: Callable) -> None: self.handler = handler - def __enter__(self): - # type: () -> None - self.orig_sigint_handler = signal.getsignal(signal.SIGINT) - signal.signal(signal.SIGINT, self.handler) + def __enter__(self) -> None: + self.orig_sigint_handler = signal.signal(signal.SIGINT, self.handler) - def __exit__(self, type=None, value=None, traceback=None): - # type: (Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]) -> None + def __exit__( + self, + type: Optional[Type[BaseException]] = None, + value: Optional[BaseException] = None, + traceback: Optional[TracebackType] = None, + ) -> None: signal.signal(signal.SIGINT, self.orig_sigint_handler) From 0e313e10a0a3e601175b0e2fbc6ca56a9ae148e0 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 12 Feb 2021 21:21:54 +0100 Subject: [PATCH 177/302] Use declarative build config --- bootstrap.py | 382 ------------------------------------------------- pyproject.toml | 6 + setup.cfg | 30 ++++ setup.py | 34 ----- 4 files changed, 36 insertions(+), 416 deletions(-) delete mode 100644 bootstrap.py diff --git a/bootstrap.py b/bootstrap.py deleted file mode 100644 index 9dc2c87..0000000 --- a/bootstrap.py +++ /dev/null @@ -1,382 +0,0 @@ -#!python -"""Bootstrap setuptools installation - -If you want to use setuptools in your package's setup.py, just include this -file in the same directory with it, and add this to the top of your setup.py:: - - from ez_setup import use_setuptools - use_setuptools() - -If you want to require a specific version of setuptools, set a download -mirror, or use an alternate download directory, you can do so by supplying -the appropriate options to ``use_setuptools()``. - -This file can also be run as a script to install or upgrade setuptools. -""" -import os -import shutil -import sys -import tempfile -import tarfile -import optparse -import subprocess -import platform - -from distutils import log - -try: - from site import USER_SITE -except ImportError: - USER_SITE = None - -DEFAULT_VERSION = "1.4.2" -DEFAULT_URL = "https://pypi.python.org/packages/source/s/setuptools/" - -def _python_cmd(*args): - args = (sys.executable,) + args - return subprocess.call(args) == 0 - -def _check_call_py24(cmd, *args, **kwargs): - res = subprocess.call(cmd, *args, **kwargs) - class CalledProcessError(Exception): - pass - if not res == 0: - msg = "Command '%s' return non-zero exit status %d" % (cmd, res) - raise CalledProcessError(msg) -vars(subprocess).setdefault('check_call', _check_call_py24) - -def _install(tarball, install_args=()): - # extracting the tarball - tmpdir = tempfile.mkdtemp() - log.warn('Extracting in %s', tmpdir) - old_wd = os.getcwd() - try: - os.chdir(tmpdir) - tar = tarfile.open(tarball) - _extractall(tar) - tar.close() - - # going in the directory - subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) - os.chdir(subdir) - log.warn('Now working in %s', subdir) - - # installing - log.warn('Installing Setuptools') - if not _python_cmd('setup.py', 'install', *install_args): - log.warn('Something went wrong during the installation.') - log.warn('See the error message above.') - # exitcode will be 2 - return 2 - finally: - os.chdir(old_wd) - shutil.rmtree(tmpdir) - - -def _build_egg(egg, tarball, to_dir): - # extracting the tarball - tmpdir = tempfile.mkdtemp() - log.warn('Extracting in %s', tmpdir) - old_wd = os.getcwd() - try: - os.chdir(tmpdir) - tar = tarfile.open(tarball) - _extractall(tar) - tar.close() - - # going in the directory - subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) - os.chdir(subdir) - log.warn('Now working in %s', subdir) - - # building an egg - log.warn('Building a Setuptools egg in %s', to_dir) - _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir) - - finally: - os.chdir(old_wd) - shutil.rmtree(tmpdir) - # returning the result - log.warn(egg) - if not os.path.exists(egg): - raise IOError('Could not build the egg.') - - -def _do_download(version, download_base, to_dir, download_delay): - egg = os.path.join(to_dir, 'setuptools-%s-py%d.%d.egg' - % (version, sys.version_info[0], sys.version_info[1])) - if not os.path.exists(egg): - tarball = download_setuptools(version, download_base, - to_dir, download_delay) - _build_egg(egg, tarball, to_dir) - sys.path.insert(0, egg) - - # Remove previously-imported pkg_resources if present (see - # https://bitbucket.org/pypa/setuptools/pull-request/7/ for details). - if 'pkg_resources' in sys.modules: - del sys.modules['pkg_resources'] - - import setuptools - setuptools.bootstrap_install_from = egg - - -def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, - to_dir=os.curdir, download_delay=15): - # making sure we use the absolute path - to_dir = os.path.abspath(to_dir) - was_imported = 'pkg_resources' in sys.modules or \ - 'setuptools' in sys.modules - try: - import pkg_resources - except ImportError: - return _do_download(version, download_base, to_dir, download_delay) - try: - pkg_resources.require("setuptools>=" + version) - return - except pkg_resources.VersionConflict: - e = sys.exc_info()[1] - if was_imported: - sys.stderr.write( - "The required version of setuptools (>=%s) is not available,\n" - "and can't be installed while this script is running. Please\n" - "install a more recent version first, using\n" - "'easy_install -U setuptools'." - "\n\n(Currently using %r)\n" % (version, e.args[0])) - sys.exit(2) - else: - del pkg_resources, sys.modules['pkg_resources'] # reload ok - return _do_download(version, download_base, to_dir, - download_delay) - except pkg_resources.DistributionNotFound: - return _do_download(version, download_base, to_dir, - download_delay) - -def _clean_check(cmd, target): - """ - Run the command to download target. If the command fails, clean up before - re-raising the error. - """ - try: - subprocess.check_call(cmd) - except subprocess.CalledProcessError: - if os.access(target, os.F_OK): - os.unlink(target) - raise - -def download_file_powershell(url, target): - """ - Download the file at url to target using Powershell (which will validate - trust). Raise an exception if the command cannot complete. - """ - target = os.path.abspath(target) - cmd = [ - 'powershell', - '-Command', - "(new-object System.Net.WebClient).DownloadFile(%(url)r, %(target)r)" % vars(), - ] - _clean_check(cmd, target) - -def has_powershell(): - if platform.system() != 'Windows': - return False - cmd = ['powershell', '-Command', 'echo test'] - devnull = open(os.path.devnull, 'wb') - try: - try: - subprocess.check_call(cmd, stdout=devnull, stderr=devnull) - except: - return False - finally: - devnull.close() - return True - -download_file_powershell.viable = has_powershell - -def download_file_curl(url, target): - cmd = ['curl', url, '--silent', '--output', target] - _clean_check(cmd, target) - -def has_curl(): - cmd = ['curl', '--version'] - devnull = open(os.path.devnull, 'wb') - try: - try: - subprocess.check_call(cmd, stdout=devnull, stderr=devnull) - except: - return False - finally: - devnull.close() - return True - -download_file_curl.viable = has_curl - -def download_file_wget(url, target): - cmd = ['wget', url, '--quiet', '--output-document', target] - _clean_check(cmd, target) - -def has_wget(): - cmd = ['wget', '--version'] - devnull = open(os.path.devnull, 'wb') - try: - try: - subprocess.check_call(cmd, stdout=devnull, stderr=devnull) - except: - return False - finally: - devnull.close() - return True - -download_file_wget.viable = has_wget - -def download_file_insecure(url, target): - """ - Use Python to download the file, even though it cannot authenticate the - connection. - """ - try: - from urllib.request import urlopen - except ImportError: - from urllib2 import urlopen - src = dst = None - try: - src = urlopen(url) - # Read/write all in one block, so we don't create a corrupt file - # if the download is interrupted. - data = src.read() - dst = open(target, "wb") - dst.write(data) - finally: - if src: - src.close() - if dst: - dst.close() - -download_file_insecure.viable = lambda: True - -def get_best_downloader(): - downloaders = [ - download_file_powershell, - download_file_curl, - download_file_wget, - download_file_insecure, - ] - - for dl in downloaders: - if dl.viable(): - return dl - -def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, - to_dir=os.curdir, delay=15, - downloader_factory=get_best_downloader): - """Download setuptools from a specified location and return its filename - - `version` should be a valid setuptools version number that is available - as an egg for download under the `download_base` URL (which should end - with a '/'). `to_dir` is the directory where the egg will be downloaded. - `delay` is the number of seconds to pause before an actual download - attempt. - - ``downloader_factory`` should be a function taking no arguments and - returning a function for downloading a URL to a target. - """ - # making sure we use the absolute path - to_dir = os.path.abspath(to_dir) - tgz_name = "setuptools-%s.tar.gz" % version - url = download_base + tgz_name - saveto = os.path.join(to_dir, tgz_name) - if not os.path.exists(saveto): # Avoid repeated downloads - log.warn("Downloading %s", url) - downloader = downloader_factory() - downloader(url, saveto) - return os.path.realpath(saveto) - - -def _extractall(self, path=".", members=None): - """Extract all members from the archive to the current working - directory and set owner, modification time and permissions on - directories afterwards. `path' specifies a different directory - to extract to. `members' is optional and must be a subset of the - list returned by getmembers(). - """ - import copy - import operator - from tarfile import ExtractError - directories = [] - - if members is None: - members = self - - for tarinfo in members: - if tarinfo.isdir(): - # Extract directories with a safe mode. - directories.append(tarinfo) - tarinfo = copy.copy(tarinfo) - tarinfo.mode = 448 # decimal for oct 0700 - self.extract(tarinfo, path) - - # Reverse sort directories. - if sys.version_info < (2, 4): - def sorter(dir1, dir2): - return cmp(dir1.name, dir2.name) - directories.sort(sorter) - directories.reverse() - else: - directories.sort(key=operator.attrgetter('name'), reverse=True) - - # Set correct owner, mtime and filemode on directories. - for tarinfo in directories: - dirpath = os.path.join(path, tarinfo.name) - try: - self.chown(tarinfo, dirpath) - self.utime(tarinfo, dirpath) - self.chmod(tarinfo, dirpath) - except ExtractError: - e = sys.exc_info()[1] - if self.errorlevel > 1: - raise - else: - self._dbg(1, "tarfile: %s" % e) - - -def _build_install_args(options): - """ - Build the arguments to 'python setup.py install' on the setuptools package - """ - install_args = [] - if options.user_install: - if sys.version_info < (2, 6): - log.warn("--user requires Python 2.6 or later") - raise SystemExit(1) - install_args.append('--user') - return install_args - -def _parse_args(): - """ - Parse the command line for options - """ - parser = optparse.OptionParser() - parser.add_option( - '--user', dest='user_install', action='store_true', default=False, - help='install in user site package (requires Python 2.6 or later)') - parser.add_option( - '--download-base', dest='download_base', metavar="URL", - default=DEFAULT_URL, - help='alternative URL from where to download the setuptools package') - parser.add_option( - '--insecure', dest='downloader_factory', action='store_const', - const=lambda: download_file_insecure, default=get_best_downloader, - help='Use internal, non-validating downloader' - ) - options, args = parser.parse_args() - # positional arguments are ignored - return options - -def main(version=DEFAULT_VERSION): - """Install or upgrade setuptools and EasyInstall""" - options = _parse_args() - tarball = download_setuptools(download_base=options.download_base, - downloader_factory=options.downloader_factory) - return _install(tarball, _build_install_args(options)) - -if __name__ == '__main__': - sys.exit(main()) diff --git a/pyproject.toml b/pyproject.toml index 40837b5..249899a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,9 @@ +[build-system] +requires = [ + "setuptools >= 43", + "wheel", +] + [tool.black] line-length = 88 target_version = ["py36"] diff --git a/setup.cfg b/setup.cfg index dc046a1..e7652e6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,33 @@ +[metadata] +name = curtsies +description = Curses-like terminal wrapper, with colored strings! +long_description = file: README.md, +long_description_content_type = text/markdown +url = https://github.com/bpython/curtsies +author = Thomas Ballinger +author_email = thomasballinger@gmail.com +license = MIT +license_file = LICENSE +classifiers = + Development Status :: 3 - Alpha + Environment :: Console + Intended Audience :: Developers + License :: OSI Approved :: MIT License + Operating System :: POSIX + Programming Language :: Python + Programming Language :: Python :: 3 + +[options] +python_requires = >=3.6 +zip_safe = False +packages = curtsies +install_requires = + blessings>=1.5 + cwcwidth +tests_require = + pyte + pytest + [mypy] warn_return_any = True warn_unused_configs = True diff --git a/setup.py b/setup.py index ffe6796..0a2f524 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,6 @@ from setuptools import setup import ast import os -import io def version(): @@ -12,39 +11,6 @@ def version(): return ast.parse(line).body[0].value.s -def long_description(): - with open("README.md", encoding="utf-8") as f: - return f.read() - - setup( - name="curtsies", version=version(), - description="Curses-like terminal wrapper, with colored strings!", - long_description=long_description(), - long_description_content_type="text/markdown", - url="https://github.com/bpython/curtsies", - author="Thomas Ballinger", - author_email="thomasballinger@gmail.com", - license="MIT", - packages=["curtsies"], - python_requires=">=3.6", - install_requires=[ - "blessings>=1.5", - "cwcwidth", - ], - tests_require=[ - "pyte", - "pytest", - ], - classifiers=[ - "Development Status :: 3 - Alpha", - "Environment :: Console", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: POSIX", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - ], - zip_safe=False, ) From 8dc289bd1da1fe0ef63f835c51edd78fe9f7c7d2 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 12 Feb 2021 22:15:46 +0100 Subject: [PATCH 178/302] Use super --- curtsies/window.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/curtsies/window.py b/curtsies/window.py index 8d224c8..7e6c42c 100644 --- a/curtsies/window.py +++ b/curtsies/window.py @@ -175,18 +175,18 @@ def __init__(self, out_stream=None, hide_cursor=True): out_stream (file): Defaults to sys.__stdout__ hide_cursor (bool): Hides cursor while in context """ - BaseWindow.__init__(self, out_stream=out_stream, hide_cursor=hide_cursor) + super().__init__(out_stream=out_stream, hide_cursor=hide_cursor) self.fullscreen_ctx = self.t.fullscreen() def __enter__(self): # type: () -> FullscreenWindow self.fullscreen_ctx.__enter__() - return BaseWindow.__enter__(self) + return super().__enter__() def __exit__(self, type, value, traceback): # type: (Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]) -> None self.fullscreen_ctx.__exit__(type, value, traceback) - BaseWindow.__exit__(self, type, value, traceback) + super().__exit__(type, value, traceback) def render_to_terminal(self, array, cursor_pos=(0, 0)): # type: (Union[FSArray, List[FmtStr]], Tuple[int, int]) -> None @@ -283,7 +283,7 @@ def __init__( bytes inadvertently read in get_cursor_position(). If not provided, a ValueError will be raised when this occurs. """ - BaseWindow.__init__(self, out_stream=out_stream, hide_cursor=hide_cursor) + super().__init__(out_stream=out_stream, hide_cursor=hide_cursor) if in_stream is None: in_stream = sys.__stdin__ self.in_stream = in_stream @@ -305,7 +305,7 @@ def __enter__(self): self.top_usable_row, _ = self.get_cursor_position() self._orig_top_usable_row = self.top_usable_row logger.debug("initial top_usable_row: %d" % self.top_usable_row) - return BaseWindow.__enter__(self) + return super().__enter__() def __exit__(self, type, value, traceback): # type: (Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]) -> None @@ -317,7 +317,7 @@ def __exit__(self, type, value, traceback): self.write(self.t.clear_eos) self.write(self.t.clear_eol) self.cbreak.__exit__(type, value, traceback) - BaseWindow.__exit__(self, type, value, traceback) + super().__exit__(type, value, traceback) def get_cursor_position(self): # type: () -> Tuple[int, int] From 322c3a5034f59af1dc32e4509836c3cc7fd3530f Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 12 Feb 2021 22:16:44 +0100 Subject: [PATCH 179/302] Remove Python 2 fallback code --- curtsies/window.py | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/curtsies/window.py b/curtsies/window.py index 7e6c42c..5f51770 100644 --- a/curtsies/window.py +++ b/curtsies/window.py @@ -130,23 +130,9 @@ def array_from_text_rc(cls, msg, rows, columns): i += 1 return arr - def fmtstr_to_stdout_xform(self): - # type: () -> Callable[[FmtStr], Text] - if sys.version_info[0] == 2: - if hasattr(self.out_stream, "encoding"): - encoding = self.out_stream.encoding - else: - encoding = locale.getpreferredencoding() - - def for_stdout(s): - # type: (FmtStr) -> Text - return unicode(s).encode(encoding, "replace") - - else: - - def for_stdout(s): - # type: (FmtStr) -> Text - return str(s) + def fmtstr_to_stdout_xform(self) -> Callable[[FmtStr], str]: + def for_stdout(s: FmtStr) -> str: + return str(s) return for_stdout From 50b1fcc5c1442ea4387b74673c1b7e0d7c1b5413 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 12 Feb 2021 22:17:25 +0100 Subject: [PATCH 180/302] Replace Text with str And also fix some type annotations. --- curtsies/configfile_keynames.py | 3 +- curtsies/events.py | 60 ++++------ curtsies/formatstring.py | 205 ++++++++++++-------------------- curtsies/formatstringarray.py | 61 ++++------ curtsies/input.py | 94 +++++++-------- curtsies/termformatconstants.py | 3 +- curtsies/termhelpers.py | 42 ++++--- curtsies/window.py | 115 +++++++++--------- 8 files changed, 251 insertions(+), 332 deletions(-) diff --git a/curtsies/configfile_keynames.py b/curtsies/configfile_keynames.py index 9966609..6d649e0 100644 --- a/curtsies/configfile_keynames.py +++ b/curtsies/configfile_keynames.py @@ -14,8 +14,7 @@ class KeyMap: """Maps config file key syntax to Curtsies names""" - def __getitem__(self, key): - # type: (str) -> Tuple[str, ...] + def __getitem__(self, key: str) -> Tuple[str, ...]: if not key: # Unbound key return () elif key in SPECIALS: diff --git a/curtsies/events.py b/curtsies/events.py index 1993399..5c531da 100644 --- a/curtsies/events.py +++ b/curtsies/events.py @@ -3,7 +3,7 @@ import encodings import itertools import sys -from typing import Text, Optional, List, Union +from typing import Optional, List, Union from .termhelpers import Termmode from .curtsieskeys import CURTSIES_NAMES as special_curtsies_names @@ -94,14 +94,14 @@ class ScheduledEvent(Event): Custom events that occur at a specific time in the future should be subclassed from ScheduledEvent.""" - def __init__(self, when): - # type: (float) -> None + def __init__(self, when: float) -> None: self.when = when class WindowChangeEvent(Event): - def __init__(self, rows, columns, cursor_dy=None): - # type: (int, int, int) -> None + def __init__( + self, rows: int, columns: int, cursor_dy: Optional[int] = None + ) -> None: self.rows = rows self.columns = columns self.cursor_dy = cursor_dy @@ -109,8 +109,7 @@ def __init__(self, rows, columns, cursor_dy=None): x = width = property(lambda self: self.columns) y = height = property(lambda self: self.rows) - def __repr__(self): - # type: () -> str + def __repr__(self) -> str: return "" % ( self.rows, self.columns, @@ -118,21 +117,18 @@ def __repr__(self): ) @property - def name(self): - # type: () -> str + def name(self) -> str: return "" class SigIntEvent(Event): """Event signifying a SIGINT""" - def __repr__(self): - # type: () -> str + def __repr__(self) -> str: return "" @property - def name(self): - # type: () -> str + def name(self) -> str: return repr(self) @@ -142,22 +138,18 @@ class PasteEvent(Event): The events attribute contains a list of keypress event strings. """ - def __init__(self): - # type: () -> None + def __init__(self) -> None: self.events = [] # type: List[Union[Event, str]] - def __repr__(self): - # type: () -> str + def __repr__(self) -> str: return "" % self.events @property - def name(self): - # type: () -> str + def name(self) -> str: return repr(self) -def decodable(seq, encoding): - # type: (bytes, str) -> bool +def decodable(seq: bytes, encoding: str) -> bool: try: u = seq.decode(encoding) except UnicodeDecodeError: @@ -166,8 +158,9 @@ def decodable(seq, encoding): return True -def get_key(bytes_, encoding, keynames="curtsies", full=False): - # type: (List[bytes], str, str, bool) -> Optional[Text] +def get_key( + bytes_: List[bytes], encoding: str, keynames: str = "curtsies", full: bool = False +) -> Optional[str]: """Return key pressed from bytes_ or None Return a key name or None meaning it's an incomplete sequence of bytes @@ -208,8 +201,7 @@ def get_key(bytes_, encoding, keynames="curtsies", full=False): if len(seq) > MAX_KEYPRESS_SIZE: raise ValueError("unable to decode bytes %r" % seq) - def key_name(): - # type: () -> Union[Text] + def key_name() -> str: if keynames == "curses": # may not be here (and still not decodable) curses names incomplete if seq in CURSES_NAMES: @@ -256,8 +248,7 @@ def key_name(): assert False, "should have raised an unicode decode error" -def could_be_unfinished_char(seq, encoding): - # type: (bytes, Text) -> bool +def could_be_unfinished_char(seq: bytes, encoding: str) -> bool: """Whether seq bytes might create a char in encoding if more bytes were added""" if decodable(seq, encoding): return False # any sensible encoding surely doesn't require lookahead (right?) @@ -271,8 +262,7 @@ def could_be_unfinished_char(seq, encoding): return True # We don't know, it could be -def could_be_unfinished_utf8(seq): - # type: (bytes) -> bool +def could_be_unfinished_utf8(seq: bytes) -> bool: # http://en.wikipedia.org/wiki/UTF-8#Description # fmt: off @@ -286,8 +276,7 @@ def could_be_unfinished_utf8(seq): # fmt: on -def pp_event(seq): - # type: (Text) -> Union[Text, bytes] +def pp_event(seq: Union[Event, str]) -> Union[str, bytes]: """Returns pretty representation of an Event or keypress""" if isinstance(seq, Event): @@ -309,13 +298,11 @@ def pp_event(seq): return repr(seq).lstrip("u")[1:-1] -def curtsies_name(seq): - # type: (bytes) -> Union[Text, bytes] +def curtsies_name(seq: bytes) -> Union[str, bytes]: return CURTSIES_NAMES.get(seq, seq) -def try_keys(): - # type: () -> None +def try_keys() -> None: print( "press a bunch of keys (not at the same time, but you can hit them pretty quickly)" ) @@ -325,8 +312,7 @@ def try_keys(): import os from .termhelpers import Cbreak - def ask_what_they_pressed(seq, Normal): - # type: (bytes, Termmode) -> None + def ask_what_they_pressed(seq: bytes, Normal: Termmode) -> None: print("Unidentified character sequence!") with Normal: while True: diff --git a/curtsies/formatstring.py b/curtsies/formatstring.py index df70566..f5fc902 100644 --- a/curtsies/formatstring.py +++ b/curtsies/formatstring.py @@ -21,7 +21,6 @@ from typing import ( Iterator, - Text, Tuple, List, Union, @@ -32,7 +31,6 @@ MutableMapping, no_type_check, Type, - cast, Callable, Iterable, ) @@ -62,17 +60,17 @@ "underline": lambda s: seq(STYLES["underline"]) + s + seq(RESET_ALL), "blink": lambda s: seq(STYLES["blink"]) + s + seq(RESET_ALL), "invert": lambda s: seq(STYLES["invert"]) + s + seq(RESET_ALL), -} # type: Mapping[Text, Callable[[Text], Text]] +} # type: Mapping[str, Callable[[str], str]] two_arg_xforms = { "fg": lambda s, v: "{}{}{}".format(seq(v), s, seq(RESET_FG)), "bg": lambda s, v: seq(v) + s + seq(RESET_BG), -} # type: Mapping[Text, Callable[[Text, int], Text]] +} # type: Mapping[str, Callable[[str, int], str]] # TODO unused, remove this in next major release xforms = ( {} -) # type: MutableMapping[Text, Union[Callable[[Text], Text], Callable[[Text, int], Text]]] +) # type: MutableMapping[str, Union[Callable[[str], str], Callable[[str, int], str]]] xforms.update(one_arg_xforms) xforms.update(two_arg_xforms) @@ -88,17 +86,14 @@ def __setitem__(self, key, value): def update(self, *args, **kwds): raise Exception("Cannot change value.") - def extend(self, dictlike): - # type: (Mapping[str, Union[int, bool]]) -> FrozenDict + def extend(self, dictlike: Mapping[str, Union[int, bool]]) -> FrozenDict: return FrozenDict(itertools.chain(self.items(), dictlike.items())) - def remove(self, *keys): - # type: (*str) -> FrozenDict + def remove(self, *keys: str) -> FrozenDict: return FrozenDict((k, v) for k, v in self.items() if k not in keys) -def stable_format_dict(d): - # type: (Mapping) -> str +def stable_format_dict(d: Mapping) -> str: """A sorted, python2/3 stable formatting of a dictionary. Does not work for dicts with unicode strings as values.""" @@ -119,31 +114,28 @@ class Chunk: Subject to change, not part of the API""" - def __init__(self, string, atts=None): - # type: (Text, Mapping[str, Union[int, bool]]) -> None + def __init__( + self, string: str, atts: Optional[Mapping[str, Union[int, bool]]] = None + ): if not isinstance(string, str): raise ValueError("unicode string required, got %r" % string) - self._s = string # type: Text + self._s = string # type: str self._atts = FrozenDict(atts if atts else {}) @property - def s(self): - # type: () -> Text + def s(self) -> str: return self._s @property - def atts(self): - # type: () -> Mapping[str, Union[int, bool]] + def atts(self) -> Mapping[str, Union[int, bool]]: "Attributes, e.g. {'fg': 34, 'bold': True} where 34 is the escape code for ..." return self._atts - def __len__(self): - # type: () -> int + def __len__(self) -> int: return len(self._s) @property - def width(self): - # type: () -> int + def width(self) -> int: width = wcswidth(self._s) if len(self._s) > 0 and width < 1: raise ValueError("Can't calculate width of string %r" % self._s) @@ -151,8 +143,7 @@ def width(self): # TODO cache this @property - def color_str(self): - # type: () -> Text + def color_str(self) -> str: "Return an escape-coded string to write to the terminal." s = self.s for k, v in sorted(self.atts.items()): @@ -168,37 +159,31 @@ def color_str(self): s = two_arg_xforms[k](s, v) return s - def __str__(self): - # type: () -> Text + def __str__(self) -> str: value = self.color_str if isinstance(value, bytes): return value.decode("utf8", "replace") return value - def __eq__(self, other): - # type: (Chunk, Any) -> bool + def __eq__(self, other: Any) -> bool: if not isinstance(other, Chunk): return False return self.s == other.s and self.atts == other.atts - def __hash__(self): - # type: () -> int + def __hash__(self) -> int: return hash((self.s, self.atts)) - def __repr__(self): - # type: () -> str + def __repr__(self) -> str: return "Chunk({s}{sep}{atts})".format( s=repr(self.s), sep=", " if self.atts else "", atts=stable_format_dict(self.atts) if self.atts else "", ) - def repr_part(self): - # type: () -> str + def repr_part(self) -> str: """FmtStr repr is build by concatenating these.""" - def pp_att(att): - # type: (str) -> str + def pp_att(att: str) -> str: if att == "fg": return FG_NUMBER_TO_COLOR[self.atts[att]] elif att == "bg": @@ -213,8 +198,7 @@ def pp_att(att): + ")" * len(atts_out) ) - def splitter(self): - # type: () -> ChunkSplitter + def splitter(self) -> ChunkSplitter: """ Returns a view of this Chunk from which new Chunks can be requested. """ @@ -226,8 +210,7 @@ class ChunkSplitter: View of a Chunk for breaking it into smaller Chunks. """ - def __init__(self, chunk): - # type: (Chunk) -> None + def __init__(self, chunk: Chunk) -> None: self.chunk = chunk self.internal_offset = 0 # index into chunk.s self.internal_width = 0 # width of chunks.s[:self.internal_offset] @@ -236,14 +219,12 @@ def __init__(self, chunk): divides.append(divides[-1] + wcwidth(c)) self.divides = divides - def reinit(self, chunk): - # type: (Chunk) -> None + def reinit(self, chunk: Chunk) -> None: """Reuse an existing Splitter instance for speed.""" # TODO benchmark to prove this is worthwhile self.__init__(chunk) # type: ignore - def request(self, max_width): - # type: (int) -> Optional[Tuple[int, Chunk]] + def request(self, max_width: int) -> Optional[Tuple[int, Chunk]]: """Requests a sub-chunk of max_width or shorter. Returns None if no chunks left.""" if max_width < 1: raise ValueError("requires positive integer max_width") @@ -303,22 +284,20 @@ def request(self, max_width): class FmtStr: """A string whose substrings carry attributes.""" - def __init__(self, *components): - # type: (*Chunk) -> None + def __init__(self, *components: Chunk) -> None: # These assertions below could be useful for debugging, but slow things down considerably # assert all([len(x) > 0 for x in components]) # self.chunks = [x for x in components if len(x) > 0] self.chunks = list(components) # caching these leads to a significant speedup - self._unicode = None # type: Optional[Text] + self._unicode = None # type: Optional[str] self._len = None # type: Optional[int] - self._s = None # type: Optional[Text] + self._s = None # type: Optional[str] self._width = None # type: Optional[int] @classmethod - def from_str(cls, s): - # type: (Union[Text]) -> FmtStr + def from_str(cls, s: str) -> FmtStr: r""" Return a FmtStr representing input. @@ -353,8 +332,7 @@ def from_str(cls, s): else: return FmtStr(Chunk(s)) - def copy_with_new_str(self, new_str): - # type: (Text) -> FmtStr + def copy_with_new_str(self, new_str: str) -> FmtStr: """Copies the current FmtStr's attributes while changing its string.""" # What to do when there are multiple Chunks with conflicting atts? old_atts = { @@ -362,13 +340,13 @@ def copy_with_new_str(self, new_str): } return FmtStr(Chunk(new_str, old_atts)) - def setitem(self, startindex, fs): - # type: (int, Union[Text, FmtStr]) -> FmtStr + def setitem(self, startindex: int, fs: Union[str, FmtStr]) -> FmtStr: """Shim for easily converting old __setitem__ calls""" return self.setslice_with_length(startindex, startindex + 1, fs, len(self)) - def setslice_with_length(self, startindex, endindex, fs, length): - # type: (int, int, Union[Text, FmtStr], int) -> FmtStr + def setslice_with_length( + self, startindex: int, endindex: int, fs: Union[str, FmtStr], length: int + ) -> FmtStr: """Shim for easily converting old __setitem__ calls""" if len(self) < startindex: fs = " " * (startindex - len(self)) + fs @@ -382,8 +360,9 @@ def setslice_with_length(self, startindex, endindex, fs, length): ) return result - def splice(self, new_str, start, end=None): - # type: (Union[Text, FmtStr], int, int) -> FmtStr + def splice( + self, new_str: Union[str, FmtStr], start: int, end: Optional[int] = None + ) -> FmtStr: """Returns a new FmtStr with the input string spliced into the the original FmtStr at start and end. If end is provided, new_str will replace the substring self.s[start:end-1]. @@ -431,19 +410,16 @@ def splice(self, new_str, start, end=None): return FmtStr(*(s for s in new_components if s.s)) - def append(self, string): - # type: (Union[Text, FmtStr]) -> FmtStr + def append(self, string: Union[str, FmtStr]) -> FmtStr: return self.splice(string, len(self.s)) - def copy_with_new_atts(self, **attributes): - # type: (**Union[bool, int]) -> FmtStr + def copy_with_new_atts(self, **attributes: Union[bool, int]) -> FmtStr: """Returns a new FmtStr with the same content but new formatting""" result = FmtStr(*(Chunk(bfs.s, bfs.atts.extend(attributes)) for bfs in self.chunks)) # type: ignore return result - def join(self, iterable): - # type: (Iterable[Union[Text, FmtStr]]) -> FmtStr + def join(self, iterable: Iterable[Union[str, FmtStr]]) -> FmtStr: """Joins an iterable yielding strings or FmtStrs with self as separator""" before = [] # type: List[Chunk] chunks = [] # type: List[Chunk] @@ -459,8 +435,12 @@ def join(self, iterable): return FmtStr(*chunks) # TODO make this split work like str.split - def split(self, sep=None, maxsplit=None, regex=False): - # type: (Text, int, bool) -> List[FmtStr] + def split( + self, + sep: Optional[str] = None, + maxsplit: Optional[int] = None, + regex: bool = False, + ) -> List[FmtStr]: """Split based on separator, optionally using a regex. Capture groups are ignored in regex, the whole pattern is matched @@ -481,8 +461,7 @@ def split(self, sep=None, maxsplit=None, regex=False): ) ] - def splitlines(self, keepends=False): - # type: (bool) -> List[FmtStr] + def splitlines(self, keepends: bool = False) -> List[FmtStr]: """Return a list of lines, split on newline characters, include line boundaries, if keepends is true.""" lines = self.split("\n") @@ -494,8 +473,7 @@ def splitlines(self, keepends=False): # proxying to the string via __getattr__ is insufficient # because we shouldn't drop foreground or formatting info - def ljust(self, width, fillchar=None): - # type: (int, Text) -> FmtStr + def ljust(self, width: int, fillchar: Optional[str] = None) -> FmtStr: """S.ljust(width[, fillchar]) -> string If a fillchar is provided, less formatting information will be preserved @@ -510,8 +488,7 @@ def ljust(self, width, fillchar=None): uniform = self.new_with_atts_removed("bg") return uniform + fmtstr(to_add, **self.shared_atts) if to_add else uniform - def rjust(self, width, fillchar=None): - # type: (int, Text) -> FmtStr + def rjust(self, width: int, fillchar: Optional[str] = None) -> FmtStr: """S.rjust(width[, fillchar]) -> string If a fillchar is provided, less formatting information will be preserved @@ -526,15 +503,13 @@ def rjust(self, width, fillchar=None): uniform = self.new_with_atts_removed("bg") return fmtstr(to_add, **self.shared_atts) + uniform if to_add else uniform - def __str__(self): - # type: () -> Text + def __str__(self) -> str: if self._unicode is not None: return self._unicode self._unicode = "".join(str(fs) for fs in self.chunks) return self._unicode - def __len__(self): - # type: () -> int + def __len__(self) -> int: if self._len is not None: return self._len value = sum(len(fs) for fs in self.chunks) @@ -542,8 +517,7 @@ def __len__(self): return value @property - def width(self): - # type: () -> int + def width(self) -> int: """The number of columns it would take to display this string.""" if self._width is not None: return self._width @@ -551,30 +525,25 @@ def width(self): self._width = value return value - def width_at_offset(self, n): - # type: (int) -> int + def width_at_offset(self, n: int) -> int: """Returns the horizontal position of character n of the string""" # TODO make more efficient? width = wcswidth(self.s, n) assert width != -1 return width - def __repr__(self): - # type: () -> str + def __repr__(self) -> str: return "+".join(fs.repr_part() for fs in self.chunks) - def __eq__(self, other): - # type: (Any) -> bool + def __eq__(self, other: Any) -> bool: if isinstance(other, (str, bytes, FmtStr)): return str(self) == str(other) return False - def __hash__(self): - # type: () -> int + def __hash__(self) -> int: return hash(str(self)) - def __add__(self, other): - # type: (Union[FmtStr, Text]) -> FmtStr + def __add__(self, other: Union[FmtStr, str]) -> FmtStr: if isinstance(other, FmtStr): return FmtStr(*(self.chunks + other.chunks)) elif isinstance(other, (bytes, str)): @@ -582,8 +551,7 @@ def __add__(self, other): else: raise TypeError(f"Can't add {self!r} and {other!r}") - def __radd__(self, other): - # type: (Union[FmtStr, Text]) -> FmtStr + def __radd__(self, other: Union[FmtStr, str]) -> FmtStr: if isinstance(other, FmtStr): return FmtStr(*(x for x in (other.chunks + self.chunks))) elif isinstance(other, (bytes, str)): @@ -591,8 +559,7 @@ def __radd__(self, other): else: raise TypeError("Can't add those") - def __mul__(self, other): - # type: (int) -> FmtStr + def __mul__(self, other: int) -> FmtStr: if isinstance(other, int): return sum((self for _ in range(other)), FmtStr()) raise TypeError("Can't multiply those") @@ -600,8 +567,7 @@ def __mul__(self, other): # TODO ensure empty FmtStr isn't a problem @property - def shared_atts(self): - # type: () -> Mapping[str, Union[int, bool]] + def shared_atts(self) -> Mapping[str, Union[int, bool]]: """Gets atts shared among all nonzero length component Chunks""" # TODO cache this, could get ugly for large FmtStrs atts = {} @@ -616,8 +582,7 @@ def shared_atts(self): atts[att] = first.atts[att] return atts - def new_with_atts_removed(self, *attributes): - # type: (*Text) -> FmtStr + def new_with_atts_removed(self, *attributes: str) -> FmtStr: """Returns a new FmtStr with the same content but some attributes removed""" result = FmtStr(*(Chunk(bfs.s, bfs.atts.remove(*attributes)) for bfs in self.chunks)) # type: ignore @@ -642,8 +607,7 @@ def func_help(*args, **kwargs): return func_help @property - def divides(self): - # type: () -> List[int] + def divides(self) -> List[int]: """List of indices of divisions between the constituent chunks.""" acc = [0] for s in self.chunks: @@ -651,15 +615,13 @@ def divides(self): return acc @property - def s(self): - # type: () -> Text + def s(self) -> str: if self._s is not None: return self._s self._s = "".join(fs.s for fs in self.chunks) return self._s - def __getitem__(self, index): - # type: (Union[int, slice]) -> FmtStr + def __getitem__(self, index: Union[int, slice]) -> FmtStr: index = normalize_slice(len(self), index) counter = 0 parts = [] @@ -679,8 +641,7 @@ def __getitem__(self, index): break return FmtStr(*parts) if parts else fmtstr("") - def width_aware_slice(self, index): - # type: (Union[int, slice]) -> FmtStr + def width_aware_slice(self, index: Union[int, slice]) -> FmtStr: """Slice based on the number of columns it would take to display the substring.""" if wcswidth(self.s) == -1: raise ValueError("bad values for width aware slicing") @@ -703,8 +664,7 @@ def width_aware_slice(self, index): break return FmtStr(*parts) if parts else fmtstr("") - def width_aware_splitlines(self, columns): - # type: (int) -> Iterator[FmtStr] + def width_aware_splitlines(self, columns: int) -> Iterator[FmtStr]: """Split into lines, pushing doublewidth characters at the end of a line to the next line. When a double-width character is pushed to the next line, a space is added to pad out the line. @@ -715,8 +675,7 @@ def width_aware_splitlines(self, columns): raise ValueError("bad values for width aware slicing") return self._width_aware_splitlines(columns) - def _width_aware_splitlines(self, columns): - # type: (int) -> Iterator[FmtStr] + def _width_aware_splitlines(self, columns: int) -> Iterator[FmtStr]: splitter = self.chunks[0].splitter() chunks_of_line = [] width_of_line = 0 @@ -738,8 +697,7 @@ def _width_aware_splitlines(self, columns): if chunks_of_line: yield FmtStr(*chunks_of_line) - def _getitem_normalized(self, index): - # type: (Union[int, slice]) -> FmtStr + def _getitem_normalized(self, index: Union[int, slice]) -> FmtStr: """Builds the more compact fmtstrs by using fromstr( of the control sequences)""" index = normalize_slice(len(self), index) counter = 0 @@ -754,17 +712,14 @@ def _getitem_normalized(self, index): break return fmtstr(output) - def __setitem__(self, index, value): - # type: (int, Any) -> None + def __setitem__(self, index: int, value: Any) -> None: raise Exception("No!") - def copy(self): - # type: () -> FmtStr + def copy(self) -> FmtStr: return FmtStr(*self.chunks) -def interval_overlap(a, b, x, y): - # type: (int, int, int, int) -> int +def interval_overlap(a: int, b: int, x: int, y: int) -> int: """Returns by how much two intervals overlap assumed that a <= b and x <= y""" @@ -780,8 +735,7 @@ def interval_overlap(a, b, x, y): assert False -def width_aware_slice(s, start, end, replacement_char=" "): - # type: (Text, int, int, Text) -> Text +def width_aware_slice(s: str, start: int, end: int, replacement_char: str = " ") -> str: """ >>> width_aware_slice(u'a\uff25iou', 0, 2)[1] == u' ' True @@ -805,8 +759,7 @@ def width_aware_slice(s, start, end, replacement_char=" "): return "".join(new_chunk_chars) -def linesplit(string, columns): - # type: (Union[Text, FmtStr], int) -> List[FmtStr] +def linesplit(string: Union[str, FmtStr], columns: int) -> List[FmtStr]: """Returns a list of lines, split on the last possible space of each line. Split spaces will be removed. Whitespaces will be normalized to one space. @@ -851,8 +804,7 @@ def linesplit(string, columns): return lines -def normalize_slice(length, index): - # type: (int, Union[int, slice]) -> slice +def normalize_slice(length: int, index: Union[int, slice]) -> slice: "Fill in the Nones in a slice." is_int = False if isinstance(index, int): @@ -874,8 +826,10 @@ def normalize_slice(length, index): return index -def parse_args(args, kwargs): - # type: (Tuple[Union[bytes, str], ...], MutableMapping[str, Union[int, bool, str]]) -> Mapping[str, Union[int, bool]] +def parse_args( + args: Tuple[Union[bytes, str], ...], + kwargs: MutableMapping[str, Union[int, bool, str]], +) -> Mapping[str, Union[int, bool]]: """Returns a kwargs dictionary by turning args into kwargs""" if "style" in kwargs: args += (cast(str, kwargs["style"]),) @@ -912,8 +866,7 @@ def parse_args(args, kwargs): return cast(MutableMapping[str, Union[int, bool]], kwargs) -def fmtstr(string, *args, **kwargs): - # type: (Union[Text, FmtStr], *Any, **Any) -> FmtStr +def fmtstr(string: Union[str, FmtStr], *args: Any, **kwargs: Any) -> FmtStr: """ Convenience function for creating a FmtStr diff --git a/curtsies/formatstringarray.py b/curtsies/formatstringarray.py index fb62abc..0f5ca3e 100644 --- a/curtsies/formatstringarray.py +++ b/curtsies/formatstringarray.py @@ -31,7 +31,6 @@ from typing import ( Any, Union, - Text, List, Sequence, overload, @@ -46,13 +45,11 @@ # TODO check that strings used in arrays don't have tabs or spaces in them! -def slicesize(s): - # type: (slice) -> int +def slicesize(s: slice) -> int: return int((s.stop - s.start) / (s.step if s.step else 1)) -def fsarray(strings, *args, **kwargs): - # type: (List[Union[FmtStr, Text]], *Any, **Any) -> FSArray +def fsarray(strings: List[Union[FmtStr, str]], *args: Any, **kwargs: Any) -> FSArray: """fsarray(list_of_FmtStrs_or_strings, width=None) -> FSArray Returns a new FSArray of width of the maximum size of the provided @@ -86,8 +83,9 @@ class FSArray(Sequence): Internally represented by a list of FmtStrs of identical size.""" # TODO add constructor that takes fmtstrs instead of dims - def __init__(self, num_rows, num_columns, *args, **kwargs): - # type: (int, int, *Any, **Any) -> None + def __init__( + self, num_rows: int, num_columns: int, *args: Any, **kwargs: Any + ) -> None: self.saved_args, self.saved_kwargs = args, kwargs self.rows = [ fmtstr("", *args, **kwargs) for _ in range(num_rows) @@ -95,22 +93,22 @@ def __init__(self, num_rows, num_columns, *args, **kwargs): self.num_columns = num_columns @overload - def __getitem__(self, slicetuple): - # type: (int) -> FmtStr + def __getitem__(self, slicetuple: int) -> FmtStr: pass @overload - def __getitem__(self, slicetuple): - # type: (slice) -> List[FmtStr] + def __getitem__(self, slicetuple: slice) -> List[FmtStr]: pass @overload - def __getitem__(self, slicetuple): - # type: (Tuple[Union[slice, int], Union[slice, int]]) -> List[FmtStr] + def __getitem__( + self, slicetuple: Tuple[Union[slice, int], Union[slice, int]] + ) -> List[FmtStr]: pass - def __getitem__(self, slicetuple): - # type: (Union[int, slice, Tuple[Union[int, slice], Union[int, slice]]]) -> Union[FmtStr, List[FmtStr]] + def __getitem__( + self, slicetuple: Union[int, slice, Tuple[Union[int, slice], Union[int, slice]]] + ) -> Union[FmtStr, List[FmtStr]]: if isinstance(slicetuple, int): if slicetuple < 0: slicetuple = len(self.rows) - slicetuple @@ -129,25 +127,21 @@ def __getitem__(self, slicetuple): # TODO clean up slices return [fs[colslice] for fs in self.rows[rowslice]] - def __len__(self): - # type: () -> int + def __len__(self) -> int: return len(self.rows) @property - def shape(self): - # type: () -> Tuple[int, int] + def shape(self) -> Tuple[int, int]: """Tuple of (len(rows, len(num_columns)) numpy-style shape""" return len(self.rows), self.num_columns @property - def height(self): - # type: () -> int + def height(self) -> int: """The number of rows""" return len(self.rows) @property - def width(self): - # type: () -> int + def width(self) -> int: """The number of columns""" return self.num_columns @@ -237,23 +231,19 @@ def __setitem__(self, slicetuple, value): + self.rows[rowslice.stop :] ) - def dumb_display(self): - # type: () -> None + def dumb_display(self) -> None: """Prints each row followed by a newline without regard for the terminal window size""" for line in self.rows: print(line) @classmethod - def diff(cls, a, b, ignore_formatting=False): - # type: (FSArray, FSArray, bool) -> Text + def diff(cls, a: FSArray, b: FSArray, ignore_formatting: bool = False) -> str: """Returns two FSArrays with differences underlined""" - def underline(x): - # type: (Text) -> Text + def underline(x: str) -> str: return f"\x1b[4m{x}\x1b[0m" - def blink(x): - # type: (Text) -> Text + def blink(x: str) -> str: return f"\x1b[5m{x}\x1b[0m" a_rows = [] @@ -292,13 +282,11 @@ def blink(x): return hdiff -def simple_format(x): - # type: (Union[FSArray, List[FmtStr]]) -> Text +def simple_format(x: Union[FSArray, List[FmtStr]]) -> str: return "\n".join(actualize(l) for l in x) -def assertFSArraysEqual(a, b): - # type: (FSArray, FSArray) -> None +def assertFSArraysEqual(a: FSArray, b: FSArray) -> None: assert isinstance(a, FSArray) assert isinstance(b, FSArray) assert ( @@ -310,8 +298,7 @@ def assertFSArraysEqual(a, b): ) -def assertFSArraysEqualIgnoringFormatting(a, b): - # type: (FSArray, FSArray) -> None +def assertFSArraysEqualIgnoringFormatting(a: FSArray, b: FSArray) -> None: """Also accepts arrays of strings""" assert len(a) == len(b), "fsarray heights do not match: %s %s \n%s \n%s" % ( len(a), diff --git a/curtsies/input.py b/curtsies/input.py index fafee66..bd89c59 100644 --- a/curtsies/input.py +++ b/curtsies/input.py @@ -1,22 +1,21 @@ import locale +import logging import os -import signal import select +import signal import sys import termios import threading import time import tty -import logging - logger = logging.getLogger(__name__) from .termhelpers import Nonblocking from . import events -from typing import Callable, Type, TextIO, Optional, List, Union, Text, cast, Tuple, Any +from typing import Callable, Type, TextIO, Optional, List, Union, cast, Tuple, Any from types import TracebackType, FrameType READ_SIZE = 1024 @@ -25,8 +24,7 @@ # the paste logic that reads more data as needed might not work. -def is_main_thread(): - # type: () -> bool +def is_main_thread() -> bool: return threading.current_thread() == threading.main_thread() @@ -51,14 +49,13 @@ class Input: def __init__( self, - in_stream=None, - keynames="curtsies", - paste_threshold=events.MAX_KEYPRESS_SIZE + 1, - sigint_event=False, - signint_callback_provider=None, - disable_terminal_start_stop=False, - ): - # type: (TextIO, str, int, bool, None, bool) -> None + in_stream: Optional[TextIO] = None, + keynames: str = "curtsies", + paste_threshold: int = events.MAX_KEYPRESS_SIZE + 1, + sigint_event: bool = False, + signint_callback_provider: Optional[Any] = None, + disable_terminal_start_stop: bool = False, + ) -> None: """Returns an Input instance. Args: @@ -87,19 +84,17 @@ def __init__( self.sigints = [] # type: List[events.SigIntEvent] self.readers = [] # type: List[int] - self.queued_interrupting_events = [] # type: List[Union[events.Event, Text]] + self.queued_interrupting_events = [] # type: List[Union[events.Event, str]] self.queued_events = [] # type: List[events.Event] self.queued_scheduled_events = ( [] ) # type: List[Tuple[float, events.ScheduledEvent]] # prospective: this could be useful for an external select loop - def fileno(self): - # type: () -> int + def fileno(self) -> int: return self.in_stream.fileno() - def __enter__(self): - # type: () -> Input + def __enter__(self) -> Input: self.original_stty = termios.tcgetattr(self.in_stream) tty.setcbreak(self.in_stream, termios.TCSANOW) @@ -122,8 +117,12 @@ def __enter__(self): signal.signal(signal.SIGINT, self.sigint_handler) return self - def __exit__(self, type=None, value=None, traceback=None): - # type: (Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]) -> None + def __exit__( + self, + type: Optional[Type[BaseException]] = None, + value: Optional[BaseException] = None, + traceback: Optional[TracebackType] = None, + ) -> None: if ( self.sigint_event and is_main_thread() @@ -132,20 +131,18 @@ def __exit__(self, type=None, value=None, traceback=None): signal.signal(signal.SIGINT, self.orig_sigint_handler) termios.tcsetattr(self.in_stream, termios.TCSANOW, self.original_stty) - def sigint_handler(self, signum, frame): - # type: (Union[signal.Signals, int], FrameType) -> None + def sigint_handler( + self, signum: Union[signal.Signals, int], frame: FrameType + ) -> None: self.sigints.append(events.SigIntEvent()) - def __iter__(self): - # type: () -> Input + def __iter__(self) -> Input: return self - def __next__(self): - # type: () -> Union[None, Text, events.Event] + def __next__(self) -> Union[None, str, events.Event]: return self.send(None) - def unget_bytes(self, string): - # type: (bytes) -> None + def unget_bytes(self, string: bytes) -> None: """Adds bytes to be internal buffer to be read This method is for reporting bytes from an in_stream read @@ -153,8 +150,9 @@ def unget_bytes(self, string): self.unprocessed_bytes.extend(string[i : i + 1] for i in range(len(string))) - def _wait_for_read_ready_or_timeout(self, timeout): - # type: (Union[float, int, None]) -> Tuple[bool, Optional[Union[events.Event, Text]]] + def _wait_for_read_ready_or_timeout( + self, timeout: Union[float, int, None] + ) -> Tuple[bool, Optional[Union[events.Event, str]]]: """Returns tuple of whether stdin is ready to read and an event. If an event is returned, that event is more pressing than reading @@ -189,8 +187,9 @@ def _wait_for_read_ready_or_timeout(self, timeout): if remaining_timeout is not None: remaining_timeout = max(remaining_timeout - (time.time() - t0), 0) - def send(self, timeout=None): - # type: (Union[float, int, None]) -> Union[None, Text, events.Event] + def send( + self, timeout: Optional[Union[float, int, None]] = None + ) -> Union[None, str, events.Event]: """Returns an event or None if no events occur before timeout.""" if self.sigint_event and is_main_thread(): with ReplacedSigIntHandler(self.sigint_handler): @@ -198,10 +197,8 @@ def send(self, timeout=None): else: return self._send(timeout) - def _send(self, timeout): - # type: (Union[float, int, None]) -> Union[None, Text, events.Event] - def find_key(): - # type: () -> Optional[Text] + def _send(self, timeout: Union[float, int, None]) -> Union[None, str, events.Event]: + def find_key() -> Optional[str]: """Returns keypress identified by adding unprocessed bytes or None""" current_bytes = [] while self.unprocessed_bytes: @@ -301,34 +298,31 @@ def _nonblocking_read(self): else: return 0 - def event_trigger(self, event_type): - # type: (Type[events.Event]) -> Callable + def event_trigger(self, event_type: Type[events.Event]) -> Callable: """Returns a callback that creates events. Returned callback function will add an event of type event_type to a queue which will be checked the next time an event is requested.""" - def callback(**kwargs): - # type: (**Any) -> None + def callback(**kwargs: Any) -> None: self.queued_events.append(event_type(**kwargs)) # type: ignore return callback - def scheduled_event_trigger(self, event_type): - # type: (Type[events.ScheduledEvent]) -> Callable + def scheduled_event_trigger( + self, event_type: Type[events.ScheduledEvent] + ) -> Callable: """Returns a callback that schedules events for the future. Returned callback function will add an event of type event_type to a queue which will be checked the next time an event is requested.""" - def callback(when): - # type: (float) -> None + def callback(when: float) -> None: self.queued_scheduled_events.append((when, event_type(when=when))) return callback - def threadsafe_event_trigger(self, event_type): - # type: (Type[events.Event]) -> Callable + def threadsafe_event_trigger(self, event_type: Type[events.Event]) -> Callable: """Returns a callback to creates events, interrupting current event requests. Returned callback function will create an event of type event_type @@ -338,8 +332,7 @@ def threadsafe_event_trigger(self, event_type): readfd, writefd = os.pipe() self.readers.append(readfd) - def callback(**kwargs): - # type: (**Any) -> None + def callback(**kwargs: Any) -> None: # TODO use a threadsafe queue for this self.queued_interrupting_events.append(event_type(**kwargs)) # type: ignore logger.debug( @@ -350,8 +343,7 @@ def callback(**kwargs): return callback -def getpreferredencoding(): - # type: () -> str +def getpreferredencoding() -> str: return locale.getpreferredencoding() or sys.getdefaultencoding() diff --git a/curtsies/termformatconstants.py b/curtsies/termformatconstants.py index 4892f22..fa61c30 100644 --- a/curtsies/termformatconstants.py +++ b/curtsies/termformatconstants.py @@ -20,6 +20,5 @@ RESET_BG = 49 -def seq(num): - # type: (int) -> str +def seq(num: int) -> str: return f"[{num}m" diff --git a/curtsies/termhelpers.py b/curtsies/termhelpers.py index bd138de..95ebdc0 100644 --- a/curtsies/termhelpers.py +++ b/curtsies/termhelpers.py @@ -14,48 +14,54 @@ class Nonblocking: A context manager for making an input stream nonblocking. """ - def __init__(self, stream): - # type: (IO) -> None + def __init__(self, stream: IO) -> None: self.stream = stream self.fd = self.stream.fileno() - def __enter__(self): - # type: () -> None + def __enter__(self) -> None: self.orig_fl = fcntl.fcntl(self.fd, fcntl.F_GETFL) fcntl.fcntl(self.fd, fcntl.F_SETFL, self.orig_fl | os.O_NONBLOCK) - def __exit__(self, type=None, value=None, traceback=None): - # type: (Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]) -> None + def __exit__( + self, + type: Optional[Type[BaseException]] = None, + value: Optional[BaseException] = None, + traceback: Optional[TracebackType] = None, + ) -> None: fcntl.fcntl(self.fd, fcntl.F_SETFL, self.orig_fl) class Cbreak: - def __init__(self, stream): - # type: (IO) -> None + def __init__(self, stream: IO) -> None: self.stream = stream - def __enter__(self): - # type: () -> Termmode + def __enter__(self) -> Termmode: self.original_stty = termios.tcgetattr(self.stream) tty.setcbreak(self.stream, termios.TCSANOW) return Termmode(self.stream, self.original_stty) - def __exit__(self, type=None, value=None, traceback=None): - # type: (Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]) -> None + def __exit__( + self, + type: Optional[Type[BaseException]] = None, + value: Optional[BaseException] = None, + traceback: Optional[TracebackType] = None, + ) -> None: termios.tcsetattr(self.stream, termios.TCSANOW, self.original_stty) class Termmode: - def __init__(self, stream, attrs): - # type: (IO, _Attr) -> None + def __init__(self, stream: IO, attrs: _Attr) -> None: self.stream = stream self.attrs = attrs - def __enter__(self): - # type: () -> None + def __enter__(self) -> None: self.original_stty = termios.tcgetattr(self.stream) termios.tcsetattr(self.stream, termios.TCSANOW, self.attrs) - def __exit__(self, type=None, value=None, traceback=None): - # type: (Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]) -> None + def __exit__( + self, + type: Optional[Type[BaseException]] = None, + value: Optional[BaseException] = None, + traceback: Optional[TracebackType] = None, + ) -> None: termios.tcsetattr(self.stream, termios.TCSANOW, self.original_stty) diff --git a/curtsies/window.py b/curtsies/window.py index 5f51770..289c435 100644 --- a/curtsies/window.py +++ b/curtsies/window.py @@ -4,7 +4,6 @@ from typing import ( Optional, - Text, IO, Dict, Generic, @@ -12,7 +11,6 @@ Type, Tuple, Callable, - Any, ByteString, cast, TextIO, @@ -42,8 +40,9 @@ class BaseWindow: - def __init__(self, out_stream=None, hide_cursor=True): - # type: (IO, bool) -> None + def __init__( + self, out_stream: Optional[IO] = None, hide_cursor: bool = True + ) -> None: logger.debug("-------initializing Window object %r------" % self) if out_stream is None: out_stream = sys.__stdout__ @@ -54,70 +53,66 @@ def __init__(self, out_stream=None, hide_cursor=True): self._last_rendered_width = None # type: Optional[int] self._last_rendered_height = None # type: Optional[int] - def scroll_down(self): - # type: () -> None + def scroll_down(self) -> None: logger.debug("sending scroll down message w/ cursor on bottom line") # since scroll-down only moves the screen if cursor is at bottom with self.t.location(x=0, y=1000000): self.write(SCROLL_DOWN) # TODO will blessings do this? - def write(self, msg): - # type: (Text) -> None + def write(self, msg: str) -> None: self.out_stream.write(msg) self.out_stream.flush() - def __enter__(self): - # type: (T) -> T + def __enter__(self: T) -> T: logger.debug("running BaseWindow.__enter__") if self.hide_cursor: self.write(self.t.hide_cursor) return self - def __exit__(self, type, value, traceback): - # type: (Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]) -> None + def __exit__( + self, + type: Optional[Type[BaseException]] = None, + value: Optional[BaseException] = None, + traceback: Optional[TracebackType] = None, + ) -> None: logger.debug("running BaseWindow.__exit__") if self.hide_cursor: self.write(self.t.normal_cursor) - def on_terminal_size_change(self, height, width): - # type: (int, int) -> None + def on_terminal_size_change(self, height: int, width: int) -> None: # Changing the terminal size breaks the cache, because it # is unknown how the window size change affected scrolling / the cursor self._last_lines_by_row = {} self._last_rendered_width = width self._last_rendered_height = height - def render_to_terminal(self, array, cursor_pos=(0, 0)): - # type: (Union[FSArray, List[FmtStr]], Tuple[int, int]) -> Optional[int] + def render_to_terminal( + self, array: Union[FSArray, List[FmtStr]], cursor_pos: Tuple[int, int] = (0, 0) + ) -> Optional[int]: raise NotImplementedError - def get_term_hw(self): - # type: () -> Tuple[int, int] + def get_term_hw(self) -> Tuple[int, int]: """Returns current terminal height and width""" return self.t.height, self.t.width @property - def width(self): - # type: () -> int + def width(self) -> int: "The current width of the terminal window" return self.t.width @property - def height(self): - # type: () -> int + def height(self) -> int: "The current width of the terminal window" return self.t.height - def array_from_text(self, msg): - # type: (Text) -> FSArray + def array_from_text(self, msg: str) -> FSArray: """Returns a FSArray of the size of the window containing msg""" rows, columns = self.t.height, self.t.width return self.array_from_text_rc(msg, rows, columns) @classmethod - def array_from_text_rc(cls, msg, rows, columns): - # type: (Text, int, int) -> FSArray + def array_from_text_rc(cls, msg: str, rows: int, columns: int) -> FSArray: arr = FSArray(0, columns) i = 0 for c in msg: @@ -153,8 +148,9 @@ class FullscreenWindow(BaseWindow): its out_stream; cached writes will be inaccurate. """ - def __init__(self, out_stream=None, hide_cursor=True): - # type: (IO, bool) -> None + def __init__( + self, out_stream: Optional[IO] = None, hide_cursor: bool = True + ) -> None: """Constructs a FullscreenWindow Args: @@ -164,18 +160,22 @@ def __init__(self, out_stream=None, hide_cursor=True): super().__init__(out_stream=out_stream, hide_cursor=hide_cursor) self.fullscreen_ctx = self.t.fullscreen() - def __enter__(self): - # type: () -> FullscreenWindow + def __enter__(self) -> FullscreenWindow: self.fullscreen_ctx.__enter__() return super().__enter__() - def __exit__(self, type, value, traceback): - # type: (Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]) -> None + def __exit__( + self, + type: Optional[Type[BaseException]] = None, + value: Optional[BaseException] = None, + traceback: Optional[TracebackType] = None, + ) -> None: self.fullscreen_ctx.__exit__(type, value, traceback) super().__exit__(type, value, traceback) - def render_to_terminal(self, array, cursor_pos=(0, 0)): - # type: (Union[FSArray, List[FmtStr]], Tuple[int, int]) -> None + def render_to_terminal( + self, array: Union[FSArray, List[FmtStr]], cursor_pos: Tuple[int, int] = (0, 0) + ) -> None: """Renders array to terminal and places (0-indexed) cursor Args: @@ -250,13 +250,12 @@ class CursorAwareWindow(BaseWindow): def __init__( self, - out_stream=None, - in_stream=None, - keep_last_line=False, - hide_cursor=True, - extra_bytes_callback=None, + out_stream: Optional[IO] = None, + in_stream: Optional[IO] = None, + keep_last_line: bool = False, + hide_cursor: bool = True, + extra_bytes_callback: Optional[Callable[[ByteString], None]] = None, ): - # type: (IO, IO, bool, bool, Callable[[ByteString], None]) -> None """Constructs a CursorAwareWindow Args: @@ -285,16 +284,19 @@ def __init__( # in the cursor query code of cursor diff self.in_get_cursor_diff = False - def __enter__(self): - # type: () -> CursorAwareWindow + def __enter__(self) -> CursorAwareWindow: self.cbreak.__enter__() self.top_usable_row, _ = self.get_cursor_position() self._orig_top_usable_row = self.top_usable_row logger.debug("initial top_usable_row: %d" % self.top_usable_row) return super().__enter__() - def __exit__(self, type, value, traceback): - # type: (Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]) -> None + def __exit__( + self, + type: Optional[Type[BaseException]] = None, + value: Optional[BaseException] = None, + traceback: Optional[TracebackType] = None, + ) -> None: if self.keep_last_line: # just moves cursor down if not on last line self.write(SCROLL_DOWN) @@ -305,8 +307,7 @@ def __exit__(self, type, value, traceback): self.cbreak.__exit__(type, value, traceback) super().__exit__(type, value, traceback) - def get_cursor_position(self): - # type: () -> Tuple[int, int] + def get_cursor_position(self) -> Tuple[int, int]: """Returns the terminal (row, column) of the cursor 0-indexed, like blessings cursor positions""" @@ -316,8 +317,7 @@ def get_cursor_position(self): query_cursor_position = "\x1b[6n" self.write(query_cursor_position) - def retrying_read(): - # type: () -> str + def retrying_read() -> str: while True: try: c = in_stream.read(1) @@ -368,8 +368,7 @@ def retrying_read(): ) return (row - 1, col - 1) - def get_cursor_vertical_diff(self): - # type: () -> int + def get_cursor_vertical_diff(self) -> int: """Returns the how far down the cursor moved since last render. Note: @@ -398,8 +397,7 @@ def get_cursor_vertical_diff(self): if not self.another_sigwinch: return cursor_dy - def _get_cursor_vertical_diff_once(self): - # type: () -> int + def _get_cursor_vertical_diff_once(self) -> int: """Returns the how far down the cursor moved.""" old_top_usable_row = self.top_usable_row row, col = self.get_cursor_position() @@ -423,8 +421,9 @@ def _get_cursor_vertical_diff_once(self): self._last_cursor_row = row return cursor_dy - def render_to_terminal(self, array, cursor_pos=(0, 0)): - # type: (Union[FSArray, List[FmtStr]], Tuple[int, int]) -> int + def render_to_terminal( + self, array: Union[FSArray, List[FmtStr]], cursor_pos: Tuple[int, int] = (0, 0) + ) -> int: """Renders array to terminal, returns the number of lines scrolled offscreen Returns: @@ -509,8 +508,7 @@ def render_to_terminal(self, array, cursor_pos=(0, 0)): return offscreen_scrolls -def demo(): - # type: () -> None +def demo() -> None: handler = logging.FileHandler(filename="display.log") logging.getLogger(__name__).setLevel(logging.DEBUG) logging.getLogger(__name__).addHandler(handler) @@ -520,7 +518,7 @@ def demo(): with input.Input(sys.stdin) as input_generator: rows, columns = w.t.height, w.t.width for c in input_generator: - assert isinstance(c, Text) + assert isinstance(c, str) if c == "": sys.exit() # same as raise SystemExit() elif c == "h": @@ -550,8 +548,7 @@ def demo(): w.render_to_terminal(a) -def main(): - # type: () -> None +def main() -> None: handler = logging.FileHandler(filename="display.log") logging.getLogger(__name__).setLevel(logging.DEBUG) logging.getLogger(__name__).addHandler(handler) From c2db5a0942570885b1b2def88fcf32c5c7d5148f Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 12 Feb 2021 23:10:10 +0100 Subject: [PATCH 181/302] Remove actualize --- curtsies/formatstringarray.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/curtsies/formatstringarray.py b/curtsies/formatstringarray.py index 0f5ca3e..f73ebb9 100644 --- a/curtsies/formatstringarray.py +++ b/curtsies/formatstringarray.py @@ -39,7 +39,6 @@ no_type_check, ) -actualize = str logger = logging.getLogger(__name__) # TODO check that strings used in arrays don't have tabs or spaces in them! @@ -266,11 +265,11 @@ def blink(x: str) -> str: a_char_for_eval = a_char b_char_for_eval = b_char if a_char_for_eval == b_char_for_eval: - a_line += actualize(a_char) - b_line += actualize(b_char) + a_line += str(a_char) + b_line += str(b_char) else: - a_line += underline(blink(actualize(a_char))) - b_line += underline(blink(actualize(b_char))) + a_line += underline(blink(str(a_char))) + b_line += underline(blink(str(b_char))) a_rows.append(a_line) b_rows.append(b_line) hdiff = "\n".join( @@ -283,7 +282,7 @@ def blink(x: str) -> str: def simple_format(x: Union[FSArray, List[FmtStr]]) -> str: - return "\n".join(actualize(l) for l in x) + return "\n".join(str(l) for l in x) def assertFSArraysEqual(a: FSArray, b: FSArray) -> None: From 7ce2b8c5b96f0230dbef8a7ebae1e838506d60c8 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 12 Feb 2021 23:43:24 +0100 Subject: [PATCH 182/302] Fix type annotations --- curtsies/formatstring.py | 54 ++++++++++++++++---------------- curtsies/formatstringarray.py | 58 +++++++++++++++++------------------ curtsies/termhelpers.py | 20 ++++++------ curtsies/window.py | 4 +-- 4 files changed, 68 insertions(+), 68 deletions(-) diff --git a/curtsies/formatstring.py b/curtsies/formatstring.py index f5fc902..1ffbc33 100644 --- a/curtsies/formatstring.py +++ b/curtsies/formatstring.py @@ -86,10 +86,10 @@ def __setitem__(self, key, value): def update(self, *args, **kwds): raise Exception("Cannot change value.") - def extend(self, dictlike: Mapping[str, Union[int, bool]]) -> FrozenDict: + def extend(self, dictlike: Mapping[str, Union[int, bool]]) -> "FrozenDict": return FrozenDict(itertools.chain(self.items(), dictlike.items())) - def remove(self, *keys: str) -> FrozenDict: + def remove(self, *keys: str) -> "FrozenDict": return FrozenDict((k, v) for k, v in self.items() if k not in keys) @@ -198,7 +198,7 @@ def pp_att(att: str) -> str: + ")" * len(atts_out) ) - def splitter(self) -> ChunkSplitter: + def splitter(self) -> "ChunkSplitter": """ Returns a view of this Chunk from which new Chunks can be requested. """ @@ -297,7 +297,7 @@ def __init__(self, *components: Chunk) -> None: self._width = None # type: Optional[int] @classmethod - def from_str(cls, s: str) -> FmtStr: + def from_str(cls, s: str) -> "FmtStr": r""" Return a FmtStr representing input. @@ -332,7 +332,7 @@ def from_str(cls, s: str) -> FmtStr: else: return FmtStr(Chunk(s)) - def copy_with_new_str(self, new_str: str) -> FmtStr: + def copy_with_new_str(self, new_str: str) -> "FmtStr": """Copies the current FmtStr's attributes while changing its string.""" # What to do when there are multiple Chunks with conflicting atts? old_atts = { @@ -340,13 +340,13 @@ def copy_with_new_str(self, new_str: str) -> FmtStr: } return FmtStr(Chunk(new_str, old_atts)) - def setitem(self, startindex: int, fs: Union[str, FmtStr]) -> FmtStr: + def setitem(self, startindex: int, fs: Union[str, "FmtStr"]) -> "FmtStr": """Shim for easily converting old __setitem__ calls""" return self.setslice_with_length(startindex, startindex + 1, fs, len(self)) def setslice_with_length( - self, startindex: int, endindex: int, fs: Union[str, FmtStr], length: int - ) -> FmtStr: + self, startindex: int, endindex: int, fs: Union[str, "FmtStr"], length: int + ) -> "FmtStr": """Shim for easily converting old __setitem__ calls""" if len(self) < startindex: fs = " " * (startindex - len(self)) + fs @@ -361,8 +361,8 @@ def setslice_with_length( return result def splice( - self, new_str: Union[str, FmtStr], start: int, end: Optional[int] = None - ) -> FmtStr: + self, new_str: Union[str, "FmtStr"], start: int, end: Optional[int] = None + ) -> "FmtStr": """Returns a new FmtStr with the input string spliced into the the original FmtStr at start and end. If end is provided, new_str will replace the substring self.s[start:end-1]. @@ -410,16 +410,16 @@ def splice( return FmtStr(*(s for s in new_components if s.s)) - def append(self, string: Union[str, FmtStr]) -> FmtStr: + def append(self, string: Union[str, "FmtStr"]) -> "FmtStr": return self.splice(string, len(self.s)) - def copy_with_new_atts(self, **attributes: Union[bool, int]) -> FmtStr: + def copy_with_new_atts(self, **attributes: Union[bool, int]) -> "FmtStr": """Returns a new FmtStr with the same content but new formatting""" result = FmtStr(*(Chunk(bfs.s, bfs.atts.extend(attributes)) for bfs in self.chunks)) # type: ignore return result - def join(self, iterable: Iterable[Union[str, FmtStr]]) -> FmtStr: + def join(self, iterable: Iterable[Union[str, "FmtStr"]]) -> "FmtStr": """Joins an iterable yielding strings or FmtStrs with self as separator""" before = [] # type: List[Chunk] chunks = [] # type: List[Chunk] @@ -440,7 +440,7 @@ def split( sep: Optional[str] = None, maxsplit: Optional[int] = None, regex: bool = False, - ) -> List[FmtStr]: + ) -> List["FmtStr"]: """Split based on separator, optionally using a regex. Capture groups are ignored in regex, the whole pattern is matched @@ -461,7 +461,7 @@ def split( ) ] - def splitlines(self, keepends: bool = False) -> List[FmtStr]: + def splitlines(self, keepends: bool = False) -> List["FmtStr"]: """Return a list of lines, split on newline characters, include line boundaries, if keepends is true.""" lines = self.split("\n") @@ -473,7 +473,7 @@ def splitlines(self, keepends: bool = False) -> List[FmtStr]: # proxying to the string via __getattr__ is insufficient # because we shouldn't drop foreground or formatting info - def ljust(self, width: int, fillchar: Optional[str] = None) -> FmtStr: + def ljust(self, width: int, fillchar: Optional[str] = None) -> "FmtStr": """S.ljust(width[, fillchar]) -> string If a fillchar is provided, less formatting information will be preserved @@ -488,7 +488,7 @@ def ljust(self, width: int, fillchar: Optional[str] = None) -> FmtStr: uniform = self.new_with_atts_removed("bg") return uniform + fmtstr(to_add, **self.shared_atts) if to_add else uniform - def rjust(self, width: int, fillchar: Optional[str] = None) -> FmtStr: + def rjust(self, width: int, fillchar: Optional[str] = None) -> "FmtStr": """S.rjust(width[, fillchar]) -> string If a fillchar is provided, less formatting information will be preserved @@ -543,7 +543,7 @@ def __eq__(self, other: Any) -> bool: def __hash__(self) -> int: return hash(str(self)) - def __add__(self, other: Union[FmtStr, str]) -> FmtStr: + def __add__(self, other: Union["FmtStr", str]) -> "FmtStr": if isinstance(other, FmtStr): return FmtStr(*(self.chunks + other.chunks)) elif isinstance(other, (bytes, str)): @@ -551,7 +551,7 @@ def __add__(self, other: Union[FmtStr, str]) -> FmtStr: else: raise TypeError(f"Can't add {self!r} and {other!r}") - def __radd__(self, other: Union[FmtStr, str]) -> FmtStr: + def __radd__(self, other: Union["FmtStr", str]) -> "FmtStr": if isinstance(other, FmtStr): return FmtStr(*(x for x in (other.chunks + self.chunks))) elif isinstance(other, (bytes, str)): @@ -559,7 +559,7 @@ def __radd__(self, other: Union[FmtStr, str]) -> FmtStr: else: raise TypeError("Can't add those") - def __mul__(self, other: int) -> FmtStr: + def __mul__(self, other: int) -> "FmtStr": if isinstance(other, int): return sum((self for _ in range(other)), FmtStr()) raise TypeError("Can't multiply those") @@ -582,7 +582,7 @@ def shared_atts(self) -> Mapping[str, Union[int, bool]]: atts[att] = first.atts[att] return atts - def new_with_atts_removed(self, *attributes: str) -> FmtStr: + def new_with_atts_removed(self, *attributes: str) -> "FmtStr": """Returns a new FmtStr with the same content but some attributes removed""" result = FmtStr(*(Chunk(bfs.s, bfs.atts.remove(*attributes)) for bfs in self.chunks)) # type: ignore @@ -621,7 +621,7 @@ def s(self) -> str: self._s = "".join(fs.s for fs in self.chunks) return self._s - def __getitem__(self, index: Union[int, slice]) -> FmtStr: + def __getitem__(self, index: Union[int, slice]) -> "FmtStr": index = normalize_slice(len(self), index) counter = 0 parts = [] @@ -641,7 +641,7 @@ def __getitem__(self, index: Union[int, slice]) -> FmtStr: break return FmtStr(*parts) if parts else fmtstr("") - def width_aware_slice(self, index: Union[int, slice]) -> FmtStr: + def width_aware_slice(self, index: Union[int, slice]) -> "FmtStr": """Slice based on the number of columns it would take to display the substring.""" if wcswidth(self.s) == -1: raise ValueError("bad values for width aware slicing") @@ -664,7 +664,7 @@ def width_aware_slice(self, index: Union[int, slice]) -> FmtStr: break return FmtStr(*parts) if parts else fmtstr("") - def width_aware_splitlines(self, columns: int) -> Iterator[FmtStr]: + def width_aware_splitlines(self, columns: int) -> Iterator["FmtStr"]: """Split into lines, pushing doublewidth characters at the end of a line to the next line. When a double-width character is pushed to the next line, a space is added to pad out the line. @@ -675,7 +675,7 @@ def width_aware_splitlines(self, columns: int) -> Iterator[FmtStr]: raise ValueError("bad values for width aware slicing") return self._width_aware_splitlines(columns) - def _width_aware_splitlines(self, columns: int) -> Iterator[FmtStr]: + def _width_aware_splitlines(self, columns: int) -> Iterator["FmtStr"]: splitter = self.chunks[0].splitter() chunks_of_line = [] width_of_line = 0 @@ -697,7 +697,7 @@ def _width_aware_splitlines(self, columns: int) -> Iterator[FmtStr]: if chunks_of_line: yield FmtStr(*chunks_of_line) - def _getitem_normalized(self, index: Union[int, slice]) -> FmtStr: + def _getitem_normalized(self, index: Union[int, slice]) -> "FmtStr": """Builds the more compact fmtstrs by using fromstr( of the control sequences)""" index = normalize_slice(len(self), index) counter = 0 @@ -715,7 +715,7 @@ def _getitem_normalized(self, index: Union[int, slice]) -> FmtStr: def __setitem__(self, index: int, value: Any) -> None: raise Exception("No!") - def copy(self) -> FmtStr: + def copy(self) -> "FmtStr": return FmtStr(*self.chunks) diff --git a/curtsies/formatstringarray.py b/curtsies/formatstringarray.py index f73ebb9..0a79292 100644 --- a/curtsies/formatstringarray.py +++ b/curtsies/formatstringarray.py @@ -48,34 +48,6 @@ def slicesize(s: slice) -> int: return int((s.stop - s.start) / (s.step if s.step else 1)) -def fsarray(strings: List[Union[FmtStr, str]], *args: Any, **kwargs: Any) -> FSArray: - """fsarray(list_of_FmtStrs_or_strings, width=None) -> FSArray - - Returns a new FSArray of width of the maximum size of the provided - strings, or width provided, and height of the number of strings provided. - If a width is provided, raises a ValueError if any of the strings - are of length greater than this width""" - - strings = list(strings) - if "width" in kwargs: - width = kwargs["width"] - del kwargs["width"] - if strings and any(len(s) > width for s in strings): - raise ValueError(f"Those strings won't fit for width {width}") - else: - width = max(len(s) for s in strings) if strings else 0 - fstrings = [ - s if isinstance(s, FmtStr) else fmtstr(s, *args, **kwargs) for s in strings - ] - arr = FSArray(len(fstrings), width, *args, **kwargs) - rows = [ - fs.setslice_with_length(0, len(s), s, width) - for fs, s in zip(arr.rows, fstrings) - ] - arr.rows = rows - return arr - - class FSArray(Sequence): """A 2D array of colored text. @@ -236,7 +208,7 @@ def dumb_display(self) -> None: print(line) @classmethod - def diff(cls, a: FSArray, b: FSArray, ignore_formatting: bool = False) -> str: + def diff(cls, a: "FSArray", b: "FSArray", ignore_formatting: bool = False) -> str: """Returns two FSArrays with differences underlined""" def underline(x: str) -> str: @@ -281,6 +253,34 @@ def blink(x: str) -> str: return hdiff +def fsarray(strings: List[Union[FmtStr, str]], *args: Any, **kwargs: Any) -> FSArray: + """fsarray(list_of_FmtStrs_or_strings, width=None) -> FSArray + + Returns a new FSArray of width of the maximum size of the provided + strings, or width provided, and height of the number of strings provided. + If a width is provided, raises a ValueError if any of the strings + are of length greater than this width""" + + strings = list(strings) + if "width" in kwargs: + width = kwargs["width"] + del kwargs["width"] + if strings and any(len(s) > width for s in strings): + raise ValueError(f"Those strings won't fit for width {width}") + else: + width = max(len(s) for s in strings) if strings else 0 + fstrings = [ + s if isinstance(s, FmtStr) else fmtstr(s, *args, **kwargs) for s in strings + ] + arr = FSArray(len(fstrings), width, *args, **kwargs) + rows = [ + fs.setslice_with_length(0, len(s), s, width) + for fs, s in zip(arr.rows, fstrings) + ] + arr.rows = rows + return arr + + def simple_format(x: Union[FSArray, List[FmtStr]]) -> str: return "\n".join(str(l) for l in x) diff --git a/curtsies/termhelpers.py b/curtsies/termhelpers.py index 95ebdc0..fa86c7a 100644 --- a/curtsies/termhelpers.py +++ b/curtsies/termhelpers.py @@ -31,14 +31,14 @@ def __exit__( fcntl.fcntl(self.fd, fcntl.F_SETFL, self.orig_fl) -class Cbreak: - def __init__(self, stream: IO) -> None: +class Termmode: + def __init__(self, stream: IO, attrs: _Attr) -> None: self.stream = stream + self.attrs = attrs - def __enter__(self) -> Termmode: + def __enter__(self) -> None: self.original_stty = termios.tcgetattr(self.stream) - tty.setcbreak(self.stream, termios.TCSANOW) - return Termmode(self.stream, self.original_stty) + termios.tcsetattr(self.stream, termios.TCSANOW, self.attrs) def __exit__( self, @@ -49,14 +49,14 @@ def __exit__( termios.tcsetattr(self.stream, termios.TCSANOW, self.original_stty) -class Termmode: - def __init__(self, stream: IO, attrs: _Attr) -> None: +class Cbreak: + def __init__(self, stream: IO) -> None: self.stream = stream - self.attrs = attrs - def __enter__(self) -> None: + def __enter__(self) -> Termmode: self.original_stty = termios.tcgetattr(self.stream) - termios.tcsetattr(self.stream, termios.TCSANOW, self.attrs) + tty.setcbreak(self.stream, termios.TCSANOW) + return Termmode(self.stream, self.original_stty) def __exit__( self, diff --git a/curtsies/window.py b/curtsies/window.py index 289c435..2117824 100644 --- a/curtsies/window.py +++ b/curtsies/window.py @@ -160,7 +160,7 @@ def __init__( super().__init__(out_stream=out_stream, hide_cursor=hide_cursor) self.fullscreen_ctx = self.t.fullscreen() - def __enter__(self) -> FullscreenWindow: + def __enter__(self) -> "FullscreenWindow": self.fullscreen_ctx.__enter__() return super().__enter__() @@ -284,7 +284,7 @@ def __init__( # in the cursor query code of cursor diff self.in_get_cursor_diff = False - def __enter__(self) -> CursorAwareWindow: + def __enter__(self) -> "CursorAwareWindow": self.cbreak.__enter__() self.top_usable_row, _ = self.get_cursor_position() self._orig_top_usable_row = self.top_usable_row From be0065b1b801d1348c7ae4a689a54cd83884ae37 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 13 Feb 2021 08:59:42 +0100 Subject: [PATCH 183/302] Fix typo annotations --- curtsies/input.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/curtsies/input.py b/curtsies/input.py index bd89c59..6f92f35 100644 --- a/curtsies/input.py +++ b/curtsies/input.py @@ -94,7 +94,7 @@ def __init__( def fileno(self) -> int: return self.in_stream.fileno() - def __enter__(self) -> Input: + def __enter__(self) -> "Input": self.original_stty = termios.tcgetattr(self.in_stream) tty.setcbreak(self.in_stream, termios.TCSANOW) @@ -136,7 +136,7 @@ def sigint_handler( ) -> None: self.sigints.append(events.SigIntEvent()) - def __iter__(self) -> Input: + def __iter__(self) -> "Input": return self def __next__(self) -> Union[None, str, events.Event]: From ac8e0ceba5699fb0252a5a4979f26711c69ab2b3 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 13 Feb 2021 14:53:47 +0100 Subject: [PATCH 184/302] Cache color_str property --- .github/workflows/build.yaml | 2 +- .github/workflows/publish-twine.yaml | 2 +- curtsies/formatstring.py | 7 +++++-- setup.cfg | 1 + 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 5d212dc..c670f6d 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -23,7 +23,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install "blessings>=1.5" cwcwidth pyte pytest + pip install setuptools wheel "blessings>=1.5" cwcwidth "backports.cached-property; python_version < '3.9'" pyte pytest - name: Build with Python ${{ matrix.python-version }} run: | python setup.py build diff --git a/.github/workflows/publish-twine.yaml b/.github/workflows/publish-twine.yaml index 2cf0914..7ced2b0 100644 --- a/.github/workflows/publish-twine.yaml +++ b/.github/workflows/publish-twine.yaml @@ -16,7 +16,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install setuptools "blessings>=1.5" cwcwidth + pip install setuptools wheel "blessings>=1.5" cwcwidth "backports.cached-property; python_version < '3.9'" - name: Build sdist run: | python setup.py sdist diff --git a/curtsies/formatstring.py b/curtsies/formatstring.py index 1ffbc33..939970a 100644 --- a/curtsies/formatstring.py +++ b/curtsies/formatstring.py @@ -40,6 +40,10 @@ import re import sys from cwcwidth import wcswidth, wcwidth +try: + from functools import cached_property +except ImportError: + from backports.cached_property import cached_property from .escseqparse import parse, remove_ansi from .termformatconstants import ( @@ -141,8 +145,7 @@ def width(self) -> int: raise ValueError("Can't calculate width of string %r" % self._s) return width - # TODO cache this - @property + @cached_property def color_str(self) -> str: "Return an escape-coded string to write to the terminal." s = self.s diff --git a/setup.cfg b/setup.cfg index e7652e6..77f0da3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,6 +24,7 @@ packages = curtsies install_requires = blessings>=1.5 cwcwidth + backports.cached-property; python_version < "3.9" tests_require = pyte pytest From 3a3269c24b15f419db5f86bb2fe78b57e61017f4 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 13 Feb 2021 14:56:20 +0100 Subject: [PATCH 185/302] Re-arrange imports --- curtsies/formatstring.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/curtsies/formatstring.py b/curtsies/formatstring.py index 939970a..479e96d 100644 --- a/curtsies/formatstring.py +++ b/curtsies/formatstring.py @@ -19,6 +19,10 @@ red('hello') """ +import itertools +import re +import sys +from cwcwidth import wcswidth, wcwidth from typing import ( Iterator, Tuple, @@ -35,15 +39,10 @@ Iterable, ) - -import itertools -import re -import sys -from cwcwidth import wcswidth, wcwidth try: from functools import cached_property except ImportError: - from backports.cached_property import cached_property + from backports.cached_property import cached_property # type: ignore from .escseqparse import parse, remove_ansi from .termformatconstants import ( From 2322b50491d9ff3bd982fa7a83fdff3975535a9c Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 13 Feb 2021 15:04:52 +0100 Subject: [PATCH 186/302] Raise a type error when encountering non-str/dict value --- curtsies/formatstring.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/curtsies/formatstring.py b/curtsies/formatstring.py index 479e96d..266966e 100644 --- a/curtsies/formatstring.py +++ b/curtsies/formatstring.py @@ -42,7 +42,7 @@ try: from functools import cached_property except ImportError: - from backports.cached_property import cached_property # type: ignore + from backports.cached_property import cached_property # type: ignore from .escseqparse import parse, remove_ansi from .termformatconstants import ( @@ -329,7 +329,7 @@ def from_str(cls, s: str) -> "FmtStr": ) chunks.append(Chunk(x, atts=atts)) else: - raise Exception("logic error") + raise TypeError(f"Expected dict or str, not {type(x)}") return FmtStr(*chunks) else: return FmtStr(Chunk(s)) From e8e260c5817b6d4bca55f331a2d4bb235ecb6dba Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 28 Mar 2021 18:14:42 +0200 Subject: [PATCH 187/302] Install py.typed file to announce typing support --- MANIFEST.in | 1 + curtsies/py.typed | 0 setup.cfg | 3 +++ 3 files changed, 4 insertions(+) create mode 100644 curtsies/py.typed diff --git a/MANIFEST.in b/MANIFEST.in index d7a857b..584721a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ include LICENSE include tests/*.py include examples/*.py +include curtsies/py.typed diff --git a/curtsies/py.typed b/curtsies/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/setup.cfg b/setup.cfg index 77f0da3..6f4c2a3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,6 +29,9 @@ tests_require = pyte pytest +[options.package_data] +curtsies = py.typed + [mypy] warn_return_any = True warn_unused_configs = True From e2ba0451435fd50e4abc2b0ea920dd99e6056805 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 15 May 2021 16:37:33 +0200 Subject: [PATCH 188/302] Return NotImplemented instead of raising a TypeError --- curtsies/formatstring.py | 15 ++++++++------- tests/test_fmtstr.py | 1 - 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/curtsies/formatstring.py b/curtsies/formatstring.py index 266966e..9bac98a 100644 --- a/curtsies/formatstring.py +++ b/curtsies/formatstring.py @@ -169,7 +169,7 @@ def __str__(self) -> str: def __eq__(self, other: Any) -> bool: if not isinstance(other, Chunk): - return False + return NotImplemented return self.s == other.s and self.atts == other.atts def __hash__(self) -> int: @@ -540,7 +540,7 @@ def __repr__(self) -> str: def __eq__(self, other: Any) -> bool: if isinstance(other, (str, bytes, FmtStr)): return str(self) == str(other) - return False + return NotImplemented def __hash__(self) -> int: return hash(str(self)) @@ -550,21 +550,22 @@ def __add__(self, other: Union["FmtStr", str]) -> "FmtStr": return FmtStr(*(self.chunks + other.chunks)) elif isinstance(other, (bytes, str)): return FmtStr(*(self.chunks + [Chunk(other)])) - else: - raise TypeError(f"Can't add {self!r} and {other!r}") + + return NotImplemented def __radd__(self, other: Union["FmtStr", str]) -> "FmtStr": if isinstance(other, FmtStr): return FmtStr(*(x for x in (other.chunks + self.chunks))) elif isinstance(other, (bytes, str)): return FmtStr(*(x for x in ([Chunk(other)] + self.chunks))) - else: - raise TypeError("Can't add those") + + return NotImplemented def __mul__(self, other: int) -> "FmtStr": if isinstance(other, int): return sum((self for _ in range(other)), FmtStr()) - raise TypeError("Can't multiply those") + + return NotImplemented # TODO ensure empty FmtStr isn't a problem diff --git a/tests/test_fmtstr.py b/tests/test_fmtstr.py index d3fe754..61a19da 100644 --- a/tests/test_fmtstr.py +++ b/tests/test_fmtstr.py @@ -252,7 +252,6 @@ def test_split_with_spaces(self): ) def test_ljust_rjust(self): - """""" b = fmtstr("ab", "blue", "on_red", "bold") g = fmtstr("cd", "green", "on_red", "bold") s = b + g From 8ecca5fa1a083af60371f27a192fbe9d89b5c517 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 16 May 2021 01:08:33 +0200 Subject: [PATCH 189/302] Fix return type --- curtsies/input.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/curtsies/input.py b/curtsies/input.py index 6f92f35..551f1ae 100644 --- a/curtsies/input.py +++ b/curtsies/input.py @@ -284,8 +284,7 @@ def find_key() -> Optional[str]: assert e is not None return e - def _nonblocking_read(self): - # type: () -> int + def _nonblocking_read(self) -> int: """Returns the number of characters read and adds them to self.unprocessed_bytes""" with Nonblocking(self.in_stream): try: @@ -311,7 +310,7 @@ def callback(**kwargs: Any) -> None: def scheduled_event_trigger( self, event_type: Type[events.ScheduledEvent] - ) -> Callable: + ) -> Callable[[float], None]: """Returns a callback that schedules events for the future. Returned callback function will add an event of type event_type From 17e31a9acca192761305681d1f815749f27ff1c8 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 16 May 2021 15:07:33 +0200 Subject: [PATCH 190/302] Fix version constraint for backports.cached-property (fixes #164) --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 6f4c2a3..444cd23 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,7 +24,7 @@ packages = curtsies install_requires = blessings>=1.5 cwcwidth - backports.cached-property; python_version < "3.9" + backports.cached-property; python_version < "3.8" tests_require = pyte pytest From 4d54189da7c864f0e84076bbc0d83ffd56d7fb7d Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 16 May 2021 15:20:07 +0200 Subject: [PATCH 191/302] Keep imports at the top --- curtsies/input.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/curtsies/input.py b/curtsies/input.py index 551f1ae..75d407e 100644 --- a/curtsies/input.py +++ b/curtsies/input.py @@ -9,15 +9,14 @@ import time import tty -logger = logging.getLogger(__name__) - - from .termhelpers import Nonblocking from . import events from typing import Callable, Type, TextIO, Optional, List, Union, cast, Tuple, Any from types import TracebackType, FrameType + +logger = logging.getLogger(__name__) READ_SIZE = 1024 assert READ_SIZE >= events.MAX_KEYPRESS_SIZE # if a keypress could require more bytes than we read to be identified, From e993b82c024f2b75894fc17c67db974251e82eaa Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 21 May 2021 22:01:19 +0200 Subject: [PATCH 192/302] Fix type of paste_threshold --- curtsies/input.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/curtsies/input.py b/curtsies/input.py index 75d407e..7c590d4 100644 --- a/curtsies/input.py +++ b/curtsies/input.py @@ -50,7 +50,7 @@ def __init__( self, in_stream: Optional[TextIO] = None, keynames: str = "curtsies", - paste_threshold: int = events.MAX_KEYPRESS_SIZE + 1, + paste_threshold: Optional[int] = events.MAX_KEYPRESS_SIZE + 1, sigint_event: bool = False, signint_callback_provider: Optional[Any] = None, disable_terminal_start_stop: bool = False, From 133e593a3455839ba5629a54fadf377ee0d10948 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 24 May 2021 22:01:15 +0200 Subject: [PATCH 193/302] Fix type of callback --- curtsies/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/curtsies/window.py b/curtsies/window.py index 2117824..7f51ec9 100644 --- a/curtsies/window.py +++ b/curtsies/window.py @@ -254,7 +254,7 @@ def __init__( in_stream: Optional[IO] = None, keep_last_line: bool = False, hide_cursor: bool = True, - extra_bytes_callback: Optional[Callable[[ByteString], None]] = None, + extra_bytes_callback: Optional[Callable[[bytes], None]] = None, ): """Constructs a CursorAwareWindow From 01043d2e4ce757c7fca2193d1c33a4f715ddcbd1 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 24 May 2021 22:01:38 +0200 Subject: [PATCH 194/302] Refactor type annotations --- curtsies/input.py | 18 +++++++----------- curtsies/window.py | 16 ++++++++-------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/curtsies/input.py b/curtsies/input.py index 7c590d4..98f81cb 100644 --- a/curtsies/input.py +++ b/curtsies/input.py @@ -73,21 +73,17 @@ def __init__( if in_stream is None: in_stream = sys.__stdin__ self.in_stream = in_stream - self.unprocessed_bytes = ( - [] - ) # type: List[bytes] # leftover from stdin, unprocessed yet + self.unprocessed_bytes: List[bytes] = [] # leftover from stdin, unprocessed yet self.keynames = keynames self.paste_threshold = paste_threshold self.sigint_event = sigint_event self.disable_terminal_start_stop = disable_terminal_start_stop - self.sigints = [] # type: List[events.SigIntEvent] - - self.readers = [] # type: List[int] - self.queued_interrupting_events = [] # type: List[Union[events.Event, str]] - self.queued_events = [] # type: List[events.Event] - self.queued_scheduled_events = ( - [] - ) # type: List[Tuple[float, events.ScheduledEvent]] + self.sigints: List[events.SigIntEvent] = [] + + self.readers: List[int] = [] + self.queued_interrupting_events: List[Union[events.Event, str]] = [] + self.queued_events: List[events.Event] = [] + self.queued_scheduled_events: List[Tuple[float, events.ScheduledEvent]] = [] # prospective: this could be useful for an external select loop def fileno(self) -> int: diff --git a/curtsies/window.py b/curtsies/window.py index 7f51ec9..225836d 100644 --- a/curtsies/window.py +++ b/curtsies/window.py @@ -49,9 +49,9 @@ def __init__( self.t = blessings.Terminal(stream=out_stream, force_styling=True) self.out_stream = out_stream self.hide_cursor = hide_cursor - self._last_lines_by_row = {} # type: Dict[int, Optional[FmtStr]] - self._last_rendered_width = None # type: Optional[int] - self._last_rendered_height = None # type: Optional[int] + self._last_lines_by_row: Dict[int, Optional[FmtStr]] = {} + self._last_rendered_width: Optional[int] = None + self._last_rendered_height: Optional[int] = None def scroll_down(self) -> None: logger.debug("sending scroll down message w/ cursor on bottom line") @@ -272,8 +272,8 @@ def __init__( if in_stream is None: in_stream = sys.__stdin__ self.in_stream = in_stream - self._last_cursor_column = None # type: Optional[int] - self._last_cursor_row = None # type: Optional[int] + self._last_cursor_column: Optional[int] = None + self._last_cursor_row: Optional[int] = None self.keep_last_line = keep_last_line self.cbreak = Cbreak(self.in_stream) self.extra_bytes_callback = extra_bytes_callback @@ -453,7 +453,7 @@ def render_to_terminal( if height != self._last_rendered_height or width != self._last_rendered_width: self.on_terminal_size_change(height, width) - current_lines_by_row = {} # type: Dict[int, Optional[FmtStr]] + current_lines_by_row: Dict[int, Optional[FmtStr]] = {} rows_for_use = list(range(self.top_usable_row, height)) # rows which we have content for and don't require scrolling @@ -522,9 +522,9 @@ def demo() -> None: if c == "": sys.exit() # same as raise SystemExit() elif c == "h": - a = w.array_from_text( + a: Union[List[FmtStr], FSArray] = w.array_from_text( "a for small array" - ) # type: Union[List[FmtStr], FSArray] + ) elif c == "a": a = [fmtstr(c * columns) for _ in range(rows)] elif c == "s": From 1460e0d440d8ba3e9b51de310b6b819247cbf411 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 24 May 2021 22:01:38 +0200 Subject: [PATCH 195/302] Refactor type annotations --- curtsies/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/curtsies/window.py b/curtsies/window.py index 225836d..9c2df3b 100644 --- a/curtsies/window.py +++ b/curtsies/window.py @@ -201,7 +201,7 @@ def render_to_terminal( if height != self._last_rendered_height or width != self._last_rendered_width: self.on_terminal_size_change(height, width) - current_lines_by_row = {} # type: Dict[int, Optional[FmtStr]] + current_lines_by_row: Dict[int, Optional[FmtStr]] = {} rows = list(range(height)) rows_for_use = rows[: len(array)] rest_of_rows = rows[len(array) :] From ad4f74fa6b0d6e4b76a778b0c80b9b32e61c868d Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 24 May 2021 22:05:24 +0200 Subject: [PATCH 196/302] Fix check --- curtsies/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/curtsies/window.py b/curtsies/window.py index 9c2df3b..e79134d 100644 --- a/curtsies/window.py +++ b/curtsies/window.py @@ -351,7 +351,7 @@ def retrying_read() -> str: col = int(m.groupdict()["column"]) extra = m.groupdict()["extra"] if extra: - if self.extra_bytes_callback: + if self.extra_bytes_callback is not None: self.extra_bytes_callback( # TODO how do we know that this works? extra.encode(cast(TextIO, in_stream).encoding) From 9709c51980fc6668f1bd8b9d5e244afe312ab605 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 24 May 2021 22:06:45 +0200 Subject: [PATCH 197/302] Clean up imports --- curtsies/window.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/curtsies/window.py b/curtsies/window.py index e79134d..68c7d9b 100644 --- a/curtsies/window.py +++ b/curtsies/window.py @@ -6,12 +6,10 @@ Optional, IO, Dict, - Generic, TypeVar, Type, Tuple, Callable, - ByteString, cast, TextIO, Union, @@ -19,7 +17,6 @@ ) from types import TracebackType -import locale import logging import re import sys From 7586636797e1d371d062ed1f35d3bab93b7dac18 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 24 May 2021 22:14:31 +0200 Subject: [PATCH 198/302] Refactor type annotations --- curtsies/escseqparse.py | 6 +++--- curtsies/events.py | 4 ++-- curtsies/formatstring.py | 26 ++++++++++++-------------- curtsies/formatstringarray.py | 4 +--- curtsies/input.py | 3 +-- curtsies/termformatconstants.py | 16 ++++++---------- 6 files changed, 25 insertions(+), 34 deletions(-) diff --git a/curtsies/escseqparse.py b/curtsies/escseqparse.py index 2072173..3f4d44f 100644 --- a/curtsies/escseqparse.py +++ b/curtsies/escseqparse.py @@ -52,7 +52,7 @@ def parse(s: str) -> List[Union[str, Dict[str, Union[str, bool, None]]]]: >>> parse("\x1b[33m[\x1b[39m\x1b[33m]\x1b[39m\x1b[33m[\x1b[39m\x1b[33m]\x1b[39m\x1b[33m[\x1b[39m\x1b[33m]\x1b[39m\x1b[33m[\x1b[39m") [{'fg': 'yellow'}, '[', {'fg': None}, {'fg': 'yellow'}, ']', {'fg': None}, {'fg': 'yellow'}, '[', {'fg': None}, {'fg': 'yellow'}, ']', {'fg': None}, {'fg': 'yellow'}, '[', {'fg': None}, {'fg': 'yellow'}, ']', {'fg': None}, {'fg': 'yellow'}, '[', {'fg': None}] """ - stuff = [] # type: List[Union[str, Dict[str, Union[str, bool, None]]]] + stuff: List[Union[str, Dict[str, Union[str, bool, None]]]] = [] rest = s while True: front, token, rest = peel_off_esc_code(rest) @@ -114,7 +114,7 @@ def peel_off_esc_code(s: str) -> Tuple[str, Optional[Token], str]: m = None if m: - d = m.groupdict() # type: Dict[str, Any] + d: Dict[str, Any] = m.groupdict() del d["front"] del d["rest"] if "numbers" in d and all(d["numbers"].split(";")): @@ -130,7 +130,7 @@ def token_type(info: Token) -> Optional[List[Dict[str, Union[str, bool, None]]]] # The default action for ESC[m is to act like ESC[0m # Ref: https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_codes values = cast(List[int], info["numbers"]) if len(info["numbers"]) else [0] - tokens = [] # type: List[Dict[str, Union[str, bool, None]]] + tokens: List[Dict[str, Union[str, bool, None]]] = [] for value in values: if value in FG_NUMBER_TO_COLOR: tokens.append({"fg": FG_NUMBER_TO_COLOR[value]}) diff --git a/curtsies/events.py b/curtsies/events.py index 5c531da..d8df974 100644 --- a/curtsies/events.py +++ b/curtsies/events.py @@ -139,7 +139,7 @@ class PasteEvent(Event): """ def __init__(self) -> None: - self.events = [] # type: List[Union[Event, str]] + self.events: List[Union[Event, str]] = [] def __repr__(self) -> str: return "" % self.events @@ -285,7 +285,7 @@ def pp_event(seq: Union[Event, str]) -> Union[str, bytes]: # Get the original sequence back if seq is a pretty name already rev_curses = {v: k for k, v in CURSES_NAMES.items()} rev_curtsies = {v: k for k, v in CURTSIES_NAMES.items()} - bytes_seq = None # type: Optional[bytes] + bytes_seq: Optional[bytes] = None if seq in rev_curses: bytes_seq = rev_curses[seq] elif seq in rev_curtsies: diff --git a/curtsies/formatstring.py b/curtsies/formatstring.py index 9bac98a..e947bd2 100644 --- a/curtsies/formatstring.py +++ b/curtsies/formatstring.py @@ -57,23 +57,21 @@ seq, ) -one_arg_xforms = { +one_arg_xforms: Mapping[str, Callable[[str], str]] = { "bold": lambda s: seq(STYLES["bold"]) + s + seq(RESET_ALL), "dark": lambda s: seq(STYLES["dark"]) + s + seq(RESET_ALL), "underline": lambda s: seq(STYLES["underline"]) + s + seq(RESET_ALL), "blink": lambda s: seq(STYLES["blink"]) + s + seq(RESET_ALL), "invert": lambda s: seq(STYLES["invert"]) + s + seq(RESET_ALL), -} # type: Mapping[str, Callable[[str], str]] +} -two_arg_xforms = { +two_arg_xforms: Mapping[str, Callable[[str, int], str]] = { "fg": lambda s, v: "{}{}{}".format(seq(v), s, seq(RESET_FG)), "bg": lambda s, v: seq(v) + s + seq(RESET_BG), -} # type: Mapping[str, Callable[[str, int], str]] +} # TODO unused, remove this in next major release -xforms = ( - {} -) # type: MutableMapping[str, Union[Callable[[str], str], Callable[[str, int], str]]] +xforms: MutableMapping[str, Union[Callable[[str], str], Callable[[str, int], str]]] = {} xforms.update(one_arg_xforms) xforms.update(two_arg_xforms) @@ -122,7 +120,7 @@ def __init__( ): if not isinstance(string, str): raise ValueError("unicode string required, got %r" % string) - self._s = string # type: str + self._s = string self._atts = FrozenDict(atts if atts else {}) @property @@ -293,10 +291,10 @@ def __init__(self, *components: Chunk) -> None: self.chunks = list(components) # caching these leads to a significant speedup - self._unicode = None # type: Optional[str] - self._len = None # type: Optional[int] - self._s = None # type: Optional[str] - self._width = None # type: Optional[int] + self._unicode: Optional[str] = None + self._len: Optional[int] = None + self._s: Optional[str] = None + self._width: Optional[int] = None @classmethod def from_str(cls, s: str) -> "FmtStr": @@ -423,8 +421,8 @@ def copy_with_new_atts(self, **attributes: Union[bool, int]) -> "FmtStr": def join(self, iterable: Iterable[Union[str, "FmtStr"]]) -> "FmtStr": """Joins an iterable yielding strings or FmtStrs with self as separator""" - before = [] # type: List[Chunk] - chunks = [] # type: List[Chunk] + before: List[Chunk] = [] + chunks: List[Chunk] = [] for s in iterable: chunks.extend(before) before = self.chunks diff --git a/curtsies/formatstringarray.py b/curtsies/formatstringarray.py index 0a79292..75de71c 100644 --- a/curtsies/formatstringarray.py +++ b/curtsies/formatstringarray.py @@ -58,9 +58,7 @@ def __init__( self, num_rows: int, num_columns: int, *args: Any, **kwargs: Any ) -> None: self.saved_args, self.saved_kwargs = args, kwargs - self.rows = [ - fmtstr("", *args, **kwargs) for _ in range(num_rows) - ] # type: List[FmtStr] + self.rows: List[FmtStr] = [fmtstr("", *args, **kwargs) for _ in range(num_rows)] self.num_columns = num_columns @overload diff --git a/curtsies/input.py b/curtsies/input.py index 98f81cb..ac9f37e 100644 --- a/curtsies/input.py +++ b/curtsies/input.py @@ -341,8 +341,7 @@ def getpreferredencoding() -> str: return locale.getpreferredencoding() or sys.getdefaultencoding() -def main(): - # type: () -> None +def main() -> None: with Input() as input_generator: print(repr(input_generator.send(2))) print(repr(input_generator.send(1))) diff --git a/curtsies/termformatconstants.py b/curtsies/termformatconstants.py index fa61c30..3779aab 100644 --- a/curtsies/termformatconstants.py +++ b/curtsies/termformatconstants.py @@ -3,17 +3,13 @@ from typing import Mapping colors = "black", "red", "green", "yellow", "blue", "magenta", "cyan", "gray" -FG_COLORS = dict(zip(colors, range(30, 38))) # type: Mapping[str, int] -BG_COLORS = dict(zip(colors, range(40, 48))) # type: Mapping[str, int] -STYLES = dict( +FG_COLORS: Mapping[str, int] = dict(zip(colors, range(30, 38))) +BG_COLORS: Mapping[str, int] = dict(zip(colors, range(40, 48))) +STYLES: Mapping[str, int] = dict( zip(("bold", "dark", "underline", "blink", "invert"), (1, 2, 4, 5, 7)) -) # type: Mapping[str, int] -FG_NUMBER_TO_COLOR = dict( - zip(FG_COLORS.values(), FG_COLORS.keys()) -) # type: Mapping[int, str] -BG_NUMBER_TO_COLOR = dict( - zip(BG_COLORS.values(), BG_COLORS.keys()) -) # type: Mapping[int, str] +) +FG_NUMBER_TO_COLOR: Mapping[int, str] = dict(zip(FG_COLORS.values(), FG_COLORS.keys())) +BG_NUMBER_TO_COLOR: Mapping[int, str] = dict(zip(BG_COLORS.values(), BG_COLORS.keys())) NUMBER_TO_STYLE = dict(zip(STYLES.values(), STYLES.keys())) RESET_ALL = 0 RESET_FG = 39 From 512efe4bae918c10325a5dc7f66cc8a18f91d43e Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 24 May 2021 22:48:25 +0200 Subject: [PATCH 199/302] Clean up --- curtsies/formatstringarray.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/curtsies/formatstringarray.py b/curtsies/formatstringarray.py index 75de71c..93c7604 100644 --- a/curtsies/formatstringarray.py +++ b/curtsies/formatstringarray.py @@ -21,6 +21,7 @@ ['i'] """ +import itertools import sys import logging @@ -217,7 +218,7 @@ def blink(x: str) -> str: a_rows = [] b_rows = [] - max_width = max([len(row) for row in a] + [len(row) for row in b]) + max_width = max(len(row) for row in itertools.chain(a, b)) a_lengths = [] b_lengths = [] for a_row, b_row in zip(a, b): @@ -242,13 +243,12 @@ def blink(x: str) -> str: b_line += underline(blink(str(b_char))) a_rows.append(a_line) b_rows.append(b_line) - hdiff = "\n".join( - a_line + " %3d | %3d " % (a_len, b_len) + b_line + return "\n".join( + f"{a_line} {a_len:3d} | {b_len:3d} {b_line}" for a_line, b_line, a_len, b_len in zip( a_rows, b_rows, a_lengths, b_lengths ) ) - return hdiff def fsarray(strings: List[Union[FmtStr, str]], *args: Any, **kwargs: Any) -> FSArray: From e96e5d94a048be9a56bf34b15f47ca06a6f7a6b9 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Sat, 25 Sep 2021 10:30:40 -0700 Subject: [PATCH 200/302] Removed unused argument This was never used, so I don't anticipate much breakage add in 77912b54 by me, whoops --- curtsies/input.py | 1 - 1 file changed, 1 deletion(-) diff --git a/curtsies/input.py b/curtsies/input.py index ac9f37e..70ffa09 100644 --- a/curtsies/input.py +++ b/curtsies/input.py @@ -52,7 +52,6 @@ def __init__( keynames: str = "curtsies", paste_threshold: Optional[int] = events.MAX_KEYPRESS_SIZE + 1, sigint_event: bool = False, - signint_callback_provider: Optional[Any] = None, disable_terminal_start_stop: bool = False, ) -> None: """Returns an Input instance. From 17367a5ad86341f1a35c062a47eac0b3a25d55d5 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Sun, 26 Sep 2021 12:46:50 -0700 Subject: [PATCH 201/302] remove old notes --- notes/notesfromdarius.txt | 195 -------------------------------------- notes/todo.txt | 10 -- 2 files changed, 205 deletions(-) delete mode 100644 notes/notesfromdarius.txt delete mode 100644 notes/todo.txt diff --git a/notes/notesfromdarius.txt b/notes/notesfromdarius.txt deleted file mode 100644 index a921366..0000000 --- a/notes/notesfromdarius.txt +++ /dev/null @@ -1,195 +0,0 @@ -insert should prob. be "splice" - is it an exposed api? check bpython, if not then change this. - -fmtstr is kind of like racket graphics primitives - change the world, use immutable building blocks - look into "racket worlds" - -* look at "how to design programs!!!" Maybe try to get a beginner that's super interested to do this? - -read this: http://www.cs.utexas.edu/~wcook/Drafts/2009/essay.pdf - -* play with smalltalk: make a bouncing ball etc, changing the code while it's bouncing, - stop it etc. - - (erlang was also cool like that, maybe look at it again) - -played at all with sounds fairly diff, cool in that syate very explicitly very cool - - i assume you've seen how objc- does it? - -sum-of a and b paramname arg paramname arg - -I was guessing it would use font, but I was wrong - -or font or something - -check out Darius's help - for editor <-> repl interaction without the repl - -I going to get some food - -re you saying the emssages would be things like "do A" - - -this could also be pass this message - -right now it's not realtime it blocks on getting an event, and has no timer events or - butit'd be nice to change - -I thoguht the simpler model was perfect for a repl - wait until user does something - but code can do anything -which way? - -I guess I don't want tto deal with threads unless each thread is an event driven loop <- I just don't have this yet - -right now it's a single thread of execution - -UI -UI -UI -UI -block on input -got input -it was a return key - so run code -run code -run code -oh code wants to read on stdin - so this is a greenet, suspend execution and - -the realization I had was that in bptuhon-curtiess I 'm also emulating the terminal - I'm responsible for colelcting that the user hit a key at *any* time, not just during input - -so it really feels like an evented system? - at least queues and messaging and things - -My problem was I was trying to be like vanilla python - but it was a terminal to collect input in the meantime - - - - - - - - - - - - - - - -MOre thoughts on Curtsies on May 28 - - - - - - -I don't know why, but it's nice for right consuming other input i guess - though I don't do a good enough job - -the structure is the BaseFmtStrs that are immutable strings with properties like blue, and then FmtStrs are lists of those - -frozen dict - add hash - -originally fmtstr and basefmtstrs were mutable and to keep myself honest I wasnted - -could you only have i - -deeplly immutable - E -not obvious what this does -then it's a boolean thing, so presence is enough -should be documentation of that - -self.atts is a dictionary of {fg:blue, bg:red, bold:Trueo - -I've not been working on this much for ahwile, but I could get excited about it - it's messy now - -I'm actually paying lea A. (hs'er) to make a logo! So if I ha - -Also if bpython starts to use this as ath main thing, I'lll need to work on cleanign it up. - -I guess so I don't have release things set up - I could give you github commit access for this and just put it on another branch for now? - -git pull upstream - - -the readme is the only docs right now - -mostly tests - -I wrote the whole thing tried to do it clearly, then had performance problems and went - -just python slice bookkeeping - -it's poorly named, it doesn't accomplish that - it's just - -asdf[1:] -normalize_slice(4, slice(1, None, None)) -> slice(1, 4, 1) - -It shouldn't be called normalize, all it does is get rid of the None's in a slice object -and replace them with the real numbers based on the length of the things we're slicing - - -right we read from in_buffer to chars - - -chars in_buffer - - ESC [ \ 1 2 - - - - - -you're not worrying about paste that's fine, i just had speed concerns - -it's a hack - you have no - -"we can't keep up - so speed up, user program!" - - -paste_mode means to things probably -init(paste_mode=True) means allow pastes to be detected, not that w'eer ecurrently in a - - -meaning that sequence isn't recognizd or isn't valid - -this depends on every input having - -if ab is passed it assumes it must have already been called with a - - - ther'e no speed concertn, we could assert that too - -char is always exactly what we pass to get_key( - -so we have the event for that - -we maybe should, but we don't clear chars - - -C and chars + self.in_buffer, but one of C and chars is empty / None - -tha'ts not used and just getting in the way - -fake_input - -just for a demo or something - -t's not used for anything - - -better code - -it's used to crawl the fs for importable python modules in bpython - -cleaner probably is what you've got with a timeout, where you just get_event(timeout=0), then do your stuff, then - -keys_in_buffer? - -(in_buffer or non_blocking_read) - -chars_available - - -sigints that happen in this curtsies cdoe to generate the events, but if we not in that, they don't have to - specifically for bpython - - -in cbreak ctrl-c still causes a handler to happen, so maybe I don't need custon ones at all, just let bpython do thatk - -take sigwinch out of bit loop? - - diff --git a/notes/todo.txt b/notes/todo.txt deleted file mode 100644 index e7fd192..0000000 --- a/notes/todo.txt +++ /dev/null @@ -1,10 +0,0 @@ -temporary todos not yet in GitHub issues, which is the official place for this kind of thing - -soon ----- - -* bugfix release bpython to use 0.0.32 and not higher - * make a bugfix 0.0.* branch -* release a new version of curtsies - * do a bugfix release of bpython first -* more tests! Even ones that just cover surface area! Everything py2/3 different that's testable From 163c9bc6f7737bde5f4a886d8fe9a581a8aeac56 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Sun, 26 Sep 2021 12:39:32 -0700 Subject: [PATCH 202/302] Fix sigint on Python 3.5+ --- curtsies/input.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/curtsies/input.py b/curtsies/input.py index 70ffa09..189573c 100644 --- a/curtsies/input.py +++ b/curtsies/input.py @@ -109,6 +109,12 @@ def __enter__(self) -> "Input": if self.sigint_event and is_main_thread(): self.orig_sigint_handler = signal.getsignal(signal.SIGINT) signal.signal(signal.SIGINT, self.sigint_handler) + + self.wakeup_read_fd, wfd = os.pipe() + os.set_blocking(wfd, False) + if sys.version_info[0] == 3 and sys.version_info[1] > 4: + signal.set_wakeup_fd(wfd, warn_on_full_buffer=False) + return self def __exit__( @@ -123,6 +129,8 @@ def __exit__( and self.orig_sigint_handler is not None ): signal.signal(signal.SIGINT, self.orig_sigint_handler) + if sys.version_info[0] == 3 and sys.version_info[1] > 4: + signal.set_wakeup_fd(-1) termios.tcsetattr(self.in_stream, termios.TCSANOW, self.original_stty) def sigint_handler( @@ -158,13 +166,25 @@ def _wait_for_read_ready_or_timeout( while True: try: (rs, _, _) = select.select( - [self.in_stream.fileno()] + self.readers, [], [], remaining_timeout + [ + self.in_stream.fileno(), + self.wakeup_read_fd, + ] + + self.readers, + [], + [], + remaining_timeout, ) if not rs: return False, None r = rs[0] # if there's more than one, get it in the next loop if r == self.in_stream.fileno(): return True, None + elif r == self.wakeup_read_fd: + # In Python >=3.5 select won't raise this signal handler + signal_number = ord(os.read(r, 1)) + if signal_number == signal.SIGINT: + raise InterruptedError() else: os.read(r, 1024) if self.queued_interrupting_events: @@ -217,7 +237,7 @@ def find_key() -> Optional[str]: return self.queued_interrupting_events.pop(0) if self.queued_scheduled_events: - self.queued_scheduled_events.sort() # TODO use a data structure that inserts sorted + self.queued_scheduled_events.sort() when, _ = self.queued_scheduled_events[0] if when < time.time(): logger.debug( From 085f67fc338ea02f3e0284208c0a264495b00df6 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Sun, 26 Sep 2021 12:53:25 -0700 Subject: [PATCH 203/302] bump version to 0.3.6 --- curtsies/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/curtsies/__init__.py b/curtsies/__init__.py index cc4514b..320e833 100644 --- a/curtsies/__init__.py +++ b/curtsies/__init__.py @@ -1,5 +1,5 @@ """Terminal-formatted strings""" -__version__ = "0.3.5" +__version__ = "0.3.6" from .window import FullscreenWindow, CursorAwareWindow from .input import Input From 85c7926952d365b842e4fa910ee77ce4f0610428 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Mon, 27 Sep 2021 06:04:46 -0700 Subject: [PATCH 204/302] don't use nonexistent argument on 3.5 and 3.6 --- curtsies/input.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/curtsies/input.py b/curtsies/input.py index 189573c..373eac9 100644 --- a/curtsies/input.py +++ b/curtsies/input.py @@ -112,7 +112,9 @@ def __enter__(self) -> "Input": self.wakeup_read_fd, wfd = os.pipe() os.set_blocking(wfd, False) - if sys.version_info[0] == 3 and sys.version_info[1] > 4: + if sys.version_info[0] == 3 and 5 <= sys.version_info[1] < 7: + signal.set_wakeup_fd(wfd) + elif sys.version_info[0] == 3 and 7 <= sys.version_info[1]: signal.set_wakeup_fd(wfd, warn_on_full_buffer=False) return self From 68f749a28fb330346538337b784a4b4369583043 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Mon, 27 Sep 2021 06:07:11 -0700 Subject: [PATCH 205/302] bump version to 0.3.7 --- CHANGELOG.md | 3 +++ curtsies/__init__.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54b1fbb..01f1c28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## [0.3.7] - 2021-09-27 +- Fixed ctrl-c not being reported until another key was pressed in Python 3.5+ + ## [0.3.5] - 2021-01-24 - Drop supported for Python 2, 3.4 and 3.5. - Migrate to pytest. Thanks to Paolo Stivanin diff --git a/curtsies/__init__.py b/curtsies/__init__.py index 320e833..0bfa827 100644 --- a/curtsies/__init__.py +++ b/curtsies/__init__.py @@ -1,5 +1,5 @@ """Terminal-formatted strings""" -__version__ = "0.3.6" +__version__ = "0.3.7" from .window import FullscreenWindow, CursorAwareWindow from .input import Input From 96bfb3eee6524246d9f4718ad2f78d832814f483 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Wed, 6 Oct 2021 20:02:43 -0700 Subject: [PATCH 206/302] run tests in Python 3.10 --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index c670f6d..2b409ec 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -13,7 +13,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest] - python-version: [3.6, 3.7, 3.8, 3.9, pypy3] + python-version: [3.6, 3.7, 3.8, 3.9, "3.10", pypy3] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} From 4e8ea4651ef40219adfe384f29c22cf5bdfb59bf Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Thu, 7 Oct 2021 11:35:31 -0700 Subject: [PATCH 207/302] Make types more allowing and run mypy in CI --- .github/workflows/lint.yaml | 13 +++++++++++++ CHANGELOG.md | 3 +++ curtsies/formatstring.py | 8 ++++---- curtsies/input.py | 6 ++++-- 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 173c0d9..94c504a 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -26,3 +26,16 @@ jobs: with: skip: '*.po' ignore_words_list: te,ot + + mypy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install mypy + pip install "blessings>=1.5" cwcwidth "backports.cached-property; python_version < '3.9'" pyte + - name: Check with mypy + run: python -m mypy diff --git a/CHANGELOG.md b/CHANGELOG.md index 01f1c28..953eaba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## [0.3.8] - 2021-09-27 +- Change typing of `event_trigger(event_type)` to allow a function that returns None + ## [0.3.7] - 2021-09-27 - Fixed ctrl-c not being reported until another key was pressed in Python 3.5+ diff --git a/curtsies/formatstring.py b/curtsies/formatstring.py index e947bd2..462371b 100644 --- a/curtsies/formatstring.py +++ b/curtsies/formatstring.py @@ -137,7 +137,7 @@ def __len__(self) -> int: @property def width(self) -> int: - width = wcswidth(self._s) + width = wcswidth(self._s, None) if len(self._s) > 0 and width < 1: raise ValueError("Can't calculate width of string %r" % self._s) return width @@ -240,7 +240,7 @@ def request(self, max_width: int) -> Optional[Tuple[int, Chunk]]: replacement_char = " " while True: - w = wcswidth(s[i]) + w = wcswidth(s[i], None) # If adding a character puts us over the requested width, return what we've got so far if width + w > max_width: @@ -644,7 +644,7 @@ def __getitem__(self, index: Union[int, slice]) -> "FmtStr": def width_aware_slice(self, index: Union[int, slice]) -> "FmtStr": """Slice based on the number of columns it would take to display the substring.""" - if wcswidth(self.s) == -1: + if wcswidth(self.s, None) == -1: raise ValueError("bad values for width aware slicing") index = normalize_slice(self.width, index) counter = 0 @@ -672,7 +672,7 @@ def width_aware_splitlines(self, columns: int) -> Iterator["FmtStr"]: """ if columns < 2: raise ValueError("Column width %s is too narrow." % columns) - if wcswidth(self.s) == -1: + if wcswidth(self.s, None) == -1: raise ValueError("bad values for width aware slicing") return self._width_aware_splitlines(columns) diff --git a/curtsies/input.py b/curtsies/input.py index 373eac9..e278473 100644 --- a/curtsies/input.py +++ b/curtsies/input.py @@ -81,7 +81,7 @@ def __init__( self.readers: List[int] = [] self.queued_interrupting_events: List[Union[events.Event, str]] = [] - self.queued_events: List[events.Event] = [] + self.queued_events: List[Union[events.Event, None]] = [] self.queued_scheduled_events: List[Tuple[float, events.ScheduledEvent]] = [] # prospective: this could be useful for an external select loop @@ -313,7 +313,9 @@ def _nonblocking_read(self) -> int: else: return 0 - def event_trigger(self, event_type: Type[events.Event]) -> Callable: + def event_trigger( + self, event_type: Union[Type[events.Event], Callable[..., None]] + ) -> Callable: """Returns a callback that creates events. Returned callback function will add an event of type event_type From 7b7a8b7138c43c30914981fede6430aa4e189f7e Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Thu, 7 Oct 2021 11:42:48 -0700 Subject: [PATCH 208/302] bump version to 0.3.8 --- CHANGELOG.md | 2 +- curtsies/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 953eaba..6536fff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [0.3.8] - 2021-09-27 +## [0.3.8] - 2021-10-07 - Change typing of `event_trigger(event_type)` to allow a function that returns None ## [0.3.7] - 2021-09-27 diff --git a/curtsies/__init__.py b/curtsies/__init__.py index 0bfa827..49d7d3c 100644 --- a/curtsies/__init__.py +++ b/curtsies/__init__.py @@ -1,5 +1,5 @@ """Terminal-formatted strings""" -__version__ = "0.3.7" +__version__ = "0.3.8" from .window import FullscreenWindow, CursorAwareWindow from .input import Input From 428e9a0f1ec84e53f6f4317d6c47166bda4f5f73 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Thu, 7 Oct 2021 11:50:00 -0700 Subject: [PATCH 209/302] fix other type --- curtsies/input.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/curtsies/input.py b/curtsies/input.py index e278473..544975c 100644 --- a/curtsies/input.py +++ b/curtsies/input.py @@ -339,7 +339,9 @@ def callback(when: float) -> None: return callback - def threadsafe_event_trigger(self, event_type: Type[events.Event]) -> Callable: + def threadsafe_event_trigger( + self, event_type: Union[Type[events.Event], Callable[..., None]] + ) -> Callable: """Returns a callback to creates events, interrupting current event requests. Returned callback function will create an event of type event_type From 26772e560547781ee641fb815a35b2a0b2d0732e Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Thu, 7 Oct 2021 11:50:32 -0700 Subject: [PATCH 210/302] bump version to 0.3.9 --- CHANGELOG.md | 2 +- curtsies/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6536fff..21fe28b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [0.3.8] - 2021-10-07 +## [0.3.9] - 2021-10-07 - Change typing of `event_trigger(event_type)` to allow a function that returns None ## [0.3.7] - 2021-09-27 diff --git a/curtsies/__init__.py b/curtsies/__init__.py index 49d7d3c..a9ff159 100644 --- a/curtsies/__init__.py +++ b/curtsies/__init__.py @@ -1,5 +1,5 @@ """Terminal-formatted strings""" -__version__ = "0.3.8" +__version__ = "0.3.9" from .window import FullscreenWindow, CursorAwareWindow from .input import Input From 9a78f93bf6f818001959a8edfb0d625cbc83482e Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Fri, 8 Oct 2021 19:39:01 -0700 Subject: [PATCH 211/302] more typing for bpython --- curtsies/events.py | 2 +- curtsies/input.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/curtsies/events.py b/curtsies/events.py index d8df974..8be5313 100644 --- a/curtsies/events.py +++ b/curtsies/events.py @@ -139,7 +139,7 @@ class PasteEvent(Event): """ def __init__(self) -> None: - self.events: List[Union[Event, str]] = [] + self.events: List[str] = [] def __repr__(self) -> str: return "" % self.events diff --git a/curtsies/input.py b/curtsies/input.py index 544975c..26e3cf3 100644 --- a/curtsies/input.py +++ b/curtsies/input.py @@ -204,7 +204,7 @@ def _wait_for_read_ready_or_timeout( remaining_timeout = max(remaining_timeout - (time.time() - t0), 0) def send( - self, timeout: Optional[Union[float, int, None]] = None + self, timeout: Optional[Union[float, None]] = None ) -> Union[None, str, events.Event]: """Returns an event or None if no events occur before timeout.""" if self.sigint_event and is_main_thread(): @@ -315,7 +315,7 @@ def _nonblocking_read(self) -> int: def event_trigger( self, event_type: Union[Type[events.Event], Callable[..., None]] - ) -> Callable: + ) -> Callable[..., None]: """Returns a callback that creates events. Returned callback function will add an event of type event_type @@ -341,7 +341,7 @@ def callback(when: float) -> None: def threadsafe_event_trigger( self, event_type: Union[Type[events.Event], Callable[..., None]] - ) -> Callable: + ) -> Callable[..., None]: """Returns a callback to creates events, interrupting current event requests. Returned callback function will create an event of type event_type From a6019a45eb9a9897f68b1a404245076cbd74492c Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Fri, 8 Oct 2021 19:39:34 -0700 Subject: [PATCH 212/302] bump version to 0.3.10 --- CHANGELOG.md | 4 ++++ curtsies/__init__.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21fe28b..af50d9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [0.3.10] - 2021-10-08 +- Typing: more specify return types for event triggers +- Typing: don't allow Event instances in PasteEvent contents + ## [0.3.9] - 2021-10-07 - Change typing of `event_trigger(event_type)` to allow a function that returns None diff --git a/curtsies/__init__.py b/curtsies/__init__.py index a9ff159..688b053 100644 --- a/curtsies/__init__.py +++ b/curtsies/__init__.py @@ -1,5 +1,5 @@ """Terminal-formatted strings""" -__version__ = "0.3.9" +__version__ = "0.3.10" from .window import FullscreenWindow, CursorAwareWindow from .input import Input From 307aa877dffe28f5760b88f67d4e865134d949c3 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Tue, 19 Oct 2021 09:51:20 -0700 Subject: [PATCH 213/302] more care taken with wakeup_read_fd on non-main threads --- curtsies/input.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/curtsies/input.py b/curtsies/input.py index 26e3cf3..ae1b1b0 100644 --- a/curtsies/input.py +++ b/curtsies/input.py @@ -78,6 +78,7 @@ def __init__( self.sigint_event = sigint_event self.disable_terminal_start_stop = disable_terminal_start_stop self.sigints: List[events.SigIntEvent] = [] + self.wakeup_read_fd: Optional[int] = None self.readers: List[int] = [] self.queued_interrupting_events: List[Union[events.Event, str]] = [] @@ -110,12 +111,14 @@ def __enter__(self) -> "Input": self.orig_sigint_handler = signal.getsignal(signal.SIGINT) signal.signal(signal.SIGINT, self.sigint_handler) - self.wakeup_read_fd, wfd = os.pipe() - os.set_blocking(wfd, False) - if sys.version_info[0] == 3 and 5 <= sys.version_info[1] < 7: - signal.set_wakeup_fd(wfd) - elif sys.version_info[0] == 3 and 7 <= sys.version_info[1]: - signal.set_wakeup_fd(wfd, warn_on_full_buffer=False) + # Non-main threads don't receive signals + if threading.current_thread() is threading.main_thread(): + self.wakeup_read_fd, wfd = os.pipe() + os.set_blocking(wfd, False) + if sys.version_info[0] == 3 and 5 <= sys.version_info[1] < 7: + signal.set_wakeup_fd(wfd) + elif sys.version_info[0] == 3 and 7 <= sys.version_info[1]: + signal.set_wakeup_fd(wfd, warn_on_full_buffer=False) return self @@ -168,10 +171,8 @@ def _wait_for_read_ready_or_timeout( while True: try: (rs, _, _) = select.select( - [ - self.in_stream.fileno(), - self.wakeup_read_fd, - ] + [self.in_stream.fileno()] + + ([] if self.wakeup_read_fd is None else [self.wakeup_read_fd]) + self.readers, [], [], From 56a0ad1199d346a059635982aa87ca07be17e14a Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Sat, 20 Nov 2021 19:48:59 -0800 Subject: [PATCH 214/302] Fix #169 - add del for st --- curtsies/curtsieskeys.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/curtsies/curtsieskeys.py b/curtsies/curtsieskeys.py index 7bd89df..be583c1 100644 --- a/curtsies/curtsieskeys.py +++ b/curtsies/curtsieskeys.py @@ -92,6 +92,10 @@ b"\x1b[3~": '', # delete (.), "Execute" b"\x1b[3;5~": '', + # st (simple terminal) see issue #169 + b"\x1b[4h": '', + b"\x1b[P": '', + # not fixing for back compat. # (b"\x1b[4~": u'