From 3a0aeecfcc733cba462fe6ec85d7810e1f1f87bd Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Mon, 30 Oct 2023 19:57:43 -0400 Subject: [PATCH 1/9] Implement ESCDELAY environment value Closes #158 --- blessed/keyboard.py | 15 +++++++++++++++ blessed/terminal.py | 26 +++++++++++++++++--------- tests/test_core.py | 13 +++++++++++++ tests/test_keyboard.py | 29 +++++++++++++++++++++++++++++ 4 files changed, 74 insertions(+), 9 deletions(-) diff --git a/blessed/keyboard.py b/blessed/keyboard.py index 31cc98c6..3e6d2a22 100644 --- a/blessed/keyboard.py +++ b/blessed/keyboard.py @@ -1,6 +1,7 @@ """Sub-module providing 'keyboard awareness'.""" # std imports +import os import re import time import platform @@ -448,4 +449,18 @@ def _read_until(term, pattern, timeout): ('KEY_BEGIN', curses.KEY_BEG), ) +#: Default delay, in seconds, of Escape key detection in +#: :meth:`Terminal.inkey`.` curses has a default delay of 1000ms (1 second) for +#: escape sequences. This is too long for modern applications, so we set it to +#: 350ms, or 0.35 seconds. It is still a bit conservative but does well with +#: for telnet or ssh servers. +DEFAULT_ESCDELAY = 0.35 +if os.environ.get('ESCDELAY'): + try: + DEFAULT_ESCDELAY = int(os.environ['ESCDELAY']) / 1000.0 + except ValueError: + # invalid values of 'ESCDELAY' are ignored + pass + + __all__ = ('Keystroke', 'get_keyboard_codes', 'get_keyboard_sequences',) diff --git a/blessed/terminal.py b/blessed/terminal.py index 76214e9d..e01f6c8c 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -23,7 +23,8 @@ resolve_sequence, get_keyboard_codes, get_leading_prefixes, - get_keyboard_sequences) + get_keyboard_sequences, + DEFAULT_ESCDELAY) from .sequences import Termcap, Sequence, SequenceTextWrapper from .colorspace import RGB_256TABLE from .formatters import (COLORS, @@ -1425,7 +1426,7 @@ def keypad(self): self.stream.write(self.rmkx) self.stream.flush() - def inkey(self, timeout=None, esc_delay=0.35): + def inkey(self, timeout=None, esc_delay=DEFAULT_ESCDELAY): """ Read and return the next keyboard event within given timeout. @@ -1434,12 +1435,20 @@ def inkey(self, timeout=None, esc_delay=0.35): :arg float timeout: Number of seconds to wait for a keystroke before returning. When ``None`` (default), this method may block indefinitely. - :arg float esc_delay: To distinguish between the keystroke of - ``KEY_ESCAPE``, and sequences beginning with escape, the parameter - ``esc_delay`` specifies the amount of time after receiving escape - (``chr(27)``) to seek for the completion of an application key - before returning a :class:`~.Keystroke` instance for - ``KEY_ESCAPE``. + :arg float esc_delay: Time in seconds to block after Escape key + is received to await another key sequence beginning with + escape such as *KEY_LEFT*, sequence ``'\x1b[D'``], before returning a + :class:`~.Keystroke` instance for ``KEY_ESCAPE``. + + Users may also override this for all blessed and curses applications + with environment value of ESCDELAY_ as an integer in milliseconds. + You may also override user preference as an argument to this function, + as the delay value is in seconds. + + It could be set to low value such as 10, modern pipelines typically + transmit a keyboard input sequence without framing and this can often + be safely set at very low values! + :rtype: :class:`~.Keystroke`. :returns: :class:`~.Keystroke`, which may be empty (``u''``) if ``timeout`` is specified and keystroke is not received. @@ -1458,7 +1467,6 @@ def inkey(self, timeout=None, esc_delay=0.35): resolve = functools.partial(resolve_sequence, mapper=self._keymap, codes=self._keycodes) - stime = time.time() # re-buffer previously received keystrokes, diff --git a/tests/test_core.py b/tests/test_core.py index d5039dc1..0818917d 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -91,9 +91,13 @@ def child(): child() +@pytest.fixture(scope="session", autouse=True) @pytest.mark.skipif(IS_WINDOWS, reason="requires more than 1 tty") def test_number_of_colors_without_tty(): """``number_of_colors`` should return 0 when there's no tty.""" + if 'COLORTERM' in os.environ: + del os.environ['COLORTERM'] + @as_subprocess def child_256_nostyle(): t = TestTerminal(stream=six.StringIO()) @@ -118,6 +122,15 @@ def child_0_forcestyle(): force_styling=True) assert (t.number_of_colors == 0) + @as_subprocess + def child_24bit_forcestyle_with_colorterm(): + os.environ['COLORTERM'] = 'truecolor' + t = TestTerminal(kind='vt220', stream=six.StringIO(), + force_styling=True) + assert (t.number_of_colors == 1 << 24) + + + child_0_forcestyle() child_8_forcestyle() child_256_forcestyle() diff --git a/tests/test_keyboard.py b/tests/test_keyboard.py index 6622f4c7..b9baecec 100644 --- a/tests/test_keyboard.py +++ b/tests/test_keyboard.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """Tests for keyboard support.""" # std imports +import os import sys import platform import tempfile @@ -363,3 +364,31 @@ def child(kind): # pylint: disable=too-many-statements assert resolve(u"\x1bOS").name == "KEY_F4" child('xterm') + +UNDER_PY34 = sys.version_info[0:2] < (3, 4) + +@pytest.fixture(scope="session", autouse=True) +@pytest.mark.skipif(UNDER_PY34, reason="importlib was renamed in py34") +def test_ESCDELAY_unset(): + if 'ESCDELAY' in os.environ: + del os.environ['ESCDELAY'] + import importlib, blessed.keyboard + importlib.reload(blessed.keyboard) + assert blessed.keyboard.DEFAULT_ESCDELAY == 0.35 + +@pytest.fixture(scope="session", autouse=True) +@pytest.mark.skipif(UNDER_PY34, reason="importlib was renamed in py34") +def test_ESCDELAY_bad_value(): + os.environ['ESCDELAY'] = 'XYZ123!' + import importlib, blessed.keyboard + importlib.reload(blessed.keyboard) + assert blessed.keyboard.DEFAULT_ESCDELAY == 0.35 + +@pytest.fixture(scope="session", autouse=True) +@pytest.mark.skipif(UNDER_PY34, reason="importlib was renamed in py34") +def test_ESCDELAY_bad_value(): + os.environ['ESCDELAY'] = '10' + import importlib, blessed.keyboard + importlib.reload(blessed.keyboard) + assert blessed.keyboard.DEFAULT_ESCDELAY == 0.01 + From 030d195dc9feae3b674ad71233d52b813396eace Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Mon, 30 Oct 2023 20:03:20 -0400 Subject: [PATCH 2/9] gramfix g# with '#' will be ignored, and an empty message aborts the commit. --- blessed/keyboard.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blessed/keyboard.py b/blessed/keyboard.py index 3e6d2a22..f121108e 100644 --- a/blessed/keyboard.py +++ b/blessed/keyboard.py @@ -452,8 +452,8 @@ def _read_until(term, pattern, timeout): #: Default delay, in seconds, of Escape key detection in #: :meth:`Terminal.inkey`.` curses has a default delay of 1000ms (1 second) for #: escape sequences. This is too long for modern applications, so we set it to -#: 350ms, or 0.35 seconds. It is still a bit conservative but does well with -#: for telnet or ssh servers. +#: 350ms, or 0.35 seconds. It is still a bit conservative, for remote telnet or +#: ssh servers, for example. DEFAULT_ESCDELAY = 0.35 if os.environ.get('ESCDELAY'): try: From f92018b95537faa6d2da055c2903e44d93d128a4 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Mon, 30 Oct 2023 20:07:52 -0400 Subject: [PATCH 3/9] docfix --- blessed/terminal.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/blessed/terminal.py b/blessed/terminal.py index e01f6c8c..12b1f8b2 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -1440,15 +1440,11 @@ def inkey(self, timeout=None, esc_delay=DEFAULT_ESCDELAY): escape such as *KEY_LEFT*, sequence ``'\x1b[D'``], before returning a :class:`~.Keystroke` instance for ``KEY_ESCAPE``. - Users may also override this for all blessed and curses applications - with environment value of ESCDELAY_ as an integer in milliseconds. - You may also override user preference as an argument to this function, - as the delay value is in seconds. - - It could be set to low value such as 10, modern pipelines typically - transmit a keyboard input sequence without framing and this can often - be safely set at very low values! - + Users may override the default value of ``esc_delay`` in seconds, + using environment value of ``ESCDELAY`` as milliseconds, see + `ncurses(3)`_ section labeled *ESCDELAY* for details. Setting + the value as an argument to this function will override any + such preference. :rtype: :class:`~.Keystroke`. :returns: :class:`~.Keystroke`, which may be empty (``u''``) if ``timeout`` is specified and keystroke is not received. @@ -1463,6 +1459,8 @@ def inkey(self, timeout=None, esc_delay=DEFAULT_ESCDELAY): `_. Decreasing the time resolution will reduce this to 10 ms, while increasing it, which is rarely done, will have a perceptable impact on the behavior. + + _`ncurses(3)`: https://www.man7.org/linux/man-pages/man3/ncurses.3x.html """ resolve = functools.partial(resolve_sequence, mapper=self._keymap, From e56e13dea8655f795de4dca6262c533c28316f7a Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Mon, 30 Oct 2023 20:36:58 -0400 Subject: [PATCH 4/9] don't use reload, add _reinit_escdelay() this should help with dubious test coverage results, i wasn't using pytest fixture right anyway --- blessed/keyboard.py | 15 +++++++++------ tests/test_keyboard.py | 43 +++++++++++++++++++++--------------------- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/blessed/keyboard.py b/blessed/keyboard.py index f121108e..4c682d05 100644 --- a/blessed/keyboard.py +++ b/blessed/keyboard.py @@ -455,12 +455,15 @@ def _read_until(term, pattern, timeout): #: 350ms, or 0.35 seconds. It is still a bit conservative, for remote telnet or #: ssh servers, for example. DEFAULT_ESCDELAY = 0.35 -if os.environ.get('ESCDELAY'): - try: - DEFAULT_ESCDELAY = int(os.environ['ESCDELAY']) / 1000.0 - except ValueError: - # invalid values of 'ESCDELAY' are ignored - pass +def _reinit_escdelay(): + global DEFAULT_ESCDELAY + if os.environ.get('ESCDELAY'): + try: + DEFAULT_ESCDELAY = int(os.environ['ESCDELAY']) / 1000.0 + except ValueError: + # invalid values of 'ESCDELAY' are ignored + pass +_reinit_escdelay() __all__ = ('Keystroke', 'get_keyboard_codes', 'get_keyboard_sequences',) diff --git a/tests/test_keyboard.py b/tests/test_keyboard.py index b9baecec..2c6d56d7 100644 --- a/tests/test_keyboard.py +++ b/tests/test_keyboard.py @@ -365,30 +365,29 @@ def child(kind): # pylint: disable=too-many-statements child('xterm') -UNDER_PY34 = sys.version_info[0:2] < (3, 4) - -@pytest.fixture(scope="session", autouse=True) -@pytest.mark.skipif(UNDER_PY34, reason="importlib was renamed in py34") -def test_ESCDELAY_unset(): +def test_ESCDELAY_unset_unchanged(): + """Unset ESCDELAY leaves DEFAULT_ESCDELAY unchanged in _reinit_escdelay().""" if 'ESCDELAY' in os.environ: del os.environ['ESCDELAY'] - import importlib, blessed.keyboard - importlib.reload(blessed.keyboard) - assert blessed.keyboard.DEFAULT_ESCDELAY == 0.35 + import blessed.keyboard + prev_value = blessed.keyboard.DEFAULT_ESCDELAY + blessed.keyboard._reinit_escdelay() + assert blessed.keyboard.DEFAULT_ESCDELAY == prev_value -@pytest.fixture(scope="session", autouse=True) -@pytest.mark.skipif(UNDER_PY34, reason="importlib was renamed in py34") -def test_ESCDELAY_bad_value(): +def test_ESCDELAY_bad_value_unchanged(): + """Invalid ESCDELAY leaves DEFAULT_ESCDELAY unchanged in _reinit_escdelay().""" os.environ['ESCDELAY'] = 'XYZ123!' - import importlib, blessed.keyboard - importlib.reload(blessed.keyboard) - assert blessed.keyboard.DEFAULT_ESCDELAY == 0.35 - -@pytest.fixture(scope="session", autouse=True) -@pytest.mark.skipif(UNDER_PY34, reason="importlib was renamed in py34") -def test_ESCDELAY_bad_value(): - os.environ['ESCDELAY'] = '10' - import importlib, blessed.keyboard - importlib.reload(blessed.keyboard) - assert blessed.keyboard.DEFAULT_ESCDELAY == 0.01 + import blessed.keyboard + prev_value = blessed.keyboard.DEFAULT_ESCDELAY + blessed.keyboard._reinit_escdelay() + assert blessed.keyboard.DEFAULT_ESCDELAY == prev_value + del os.environ['ESCDELAY'] + +def test_ESCDELAY_10ms(): + """Verify ESCDELAY modifies DEFAULT_ESCDELAY in _reinit_escdelay().""" + os.environ['ESCDELAY'] = '1234' + import blessed.keyboard + blessed.keyboard._reinit_escdelay() + assert blessed.keyboard.DEFAULT_ESCDELAY == 1.234 + del os.environ['ESCDELAY'] From 3544edba72959b7aaae1568f4fa59337537899ef Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Mon, 30 Oct 2023 21:05:20 -0400 Subject: [PATCH 5/9] pylint disable globals --- blessed/keyboard.py | 2 ++ tests/test_core.py | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/blessed/keyboard.py b/blessed/keyboard.py index 4c682d05..7734a69b 100644 --- a/blessed/keyboard.py +++ b/blessed/keyboard.py @@ -456,6 +456,8 @@ def _read_until(term, pattern, timeout): #: ssh servers, for example. DEFAULT_ESCDELAY = 0.35 def _reinit_escdelay(): + # pylint: disable=W0603 Using the global statement, this is necessary to + # allow test coverage without complex module reload global DEFAULT_ESCDELAY if os.environ.get('ESCDELAY'): try: diff --git a/tests/test_core.py b/tests/test_core.py index 0818917d..ad83c850 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -129,8 +129,6 @@ def child_24bit_forcestyle_with_colorterm(): force_styling=True) assert (t.number_of_colors == 1 << 24) - - child_0_forcestyle() child_8_forcestyle() child_256_forcestyle() From d237cdb2730cf0a8080f74befa94a8c24e0c56f4 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Mon, 30 Oct 2023 21:09:04 -0400 Subject: [PATCH 6/9] nit --- tests/test_keyboard.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_keyboard.py b/tests/test_keyboard.py index 2c6d56d7..189c6ac4 100644 --- a/tests/test_keyboard.py +++ b/tests/test_keyboard.py @@ -390,4 +390,3 @@ def test_ESCDELAY_10ms(): blessed.keyboard._reinit_escdelay() assert blessed.keyboard.DEFAULT_ESCDELAY == 1.234 del os.environ['ESCDELAY'] - From 8123628c82614a089f81063c0f8b2af644e116b0 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Thu, 14 Dec 2023 01:17:45 -0500 Subject: [PATCH 7/9] remove @pytest.fixture --- tests/test_core.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_core.py b/tests/test_core.py index ad83c850..cb6cf6f8 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -91,7 +91,6 @@ def child(): child() -@pytest.fixture(scope="session", autouse=True) @pytest.mark.skipif(IS_WINDOWS, reason="requires more than 1 tty") def test_number_of_colors_without_tty(): """``number_of_colors`` should return 0 when there's no tty.""" From 8eb91f9ab05e5d63cdfb49254ad61ed2efe01dc3 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Thu, 14 Dec 2023 01:19:21 -0500 Subject: [PATCH 8/9] use r""" as suggested by pydocstyle D301: Use r""" if any backslashes in a docstring Use r"""raw triple double quotes""" if you use any backslashes (\) in your docstrings. Exceptions are backslashes for line-continuation and unicode escape sequences \N... and \u... These are considered intended unescaped content in docstrings. --- blessed/terminal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blessed/terminal.py b/blessed/terminal.py index 12b1f8b2..fe9e03f4 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -1427,7 +1427,7 @@ def keypad(self): self.stream.flush() def inkey(self, timeout=None, esc_delay=DEFAULT_ESCDELAY): - """ + r""" Read and return the next keyboard event within given timeout. Generally, this should be used inside the :meth:`raw` context manager. From 47328d9a051167eb93014d73081e743046b4e7ea Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Thu, 14 Dec 2023 01:37:43 -0500 Subject: [PATCH 9/9] linting & pin docformatter - fix for 'pylint: disable' - pin docformatter, As it seems the bug that @avylove created may not been addressed, freeze docformatter for now, to prevent failing CI https://github.com/PyCQA/docformatter/issues/264 --- blessed/keyboard.py | 7 ++++++- blessed/terminal.py | 6 +++--- tests/accessories.py | 2 +- tests/test_keyboard.py | 3 +++ tox.ini | 6 ++++-- 5 files changed, 17 insertions(+), 7 deletions(-) diff --git a/blessed/keyboard.py b/blessed/keyboard.py index 7734a69b..401ec459 100644 --- a/blessed/keyboard.py +++ b/blessed/keyboard.py @@ -455,8 +455,11 @@ def _read_until(term, pattern, timeout): #: 350ms, or 0.35 seconds. It is still a bit conservative, for remote telnet or #: ssh servers, for example. DEFAULT_ESCDELAY = 0.35 + + def _reinit_escdelay(): - # pylint: disable=W0603 Using the global statement, this is necessary to + # pylint: disable=W0603 + # Using the global statement: this is necessary to # allow test coverage without complex module reload global DEFAULT_ESCDELAY if os.environ.get('ESCDELAY'): @@ -465,6 +468,8 @@ def _reinit_escdelay(): except ValueError: # invalid values of 'ESCDELAY' are ignored pass + + _reinit_escdelay() diff --git a/blessed/terminal.py b/blessed/terminal.py index fe9e03f4..fc0b0c4f 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -18,13 +18,13 @@ # local from .color import COLOR_DISTANCE_ALGORITHMS -from .keyboard import (_time_left, +from .keyboard import (DEFAULT_ESCDELAY, + _time_left, _read_until, resolve_sequence, get_keyboard_codes, get_leading_prefixes, - get_keyboard_sequences, - DEFAULT_ESCDELAY) + get_keyboard_sequences) from .sequences import Termcap, Sequence, SequenceTextWrapper from .colorspace import RGB_256TABLE from .formatters import (COLORS, diff --git a/tests/accessories.py b/tests/accessories.py index 9d04d5aa..54eec455 100644 --- a/tests/accessories.py +++ b/tests/accessories.py @@ -61,7 +61,7 @@ class as_subprocess(object): # pylint: disable=too-few-public-methods def __init__(self, func): self.func = func - def __call__(self, *args, **kwargs): # pylint: disable=too-many-locals, too-complex + def __call__(self, *args, **kwargs): # pylint: disable=too-many-locals,too-complex if IS_WINDOWS: self.func(*args, **kwargs) return diff --git a/tests/test_keyboard.py b/tests/test_keyboard.py index 189c6ac4..8cd50e03 100644 --- a/tests/test_keyboard.py +++ b/tests/test_keyboard.py @@ -365,6 +365,7 @@ def child(kind): # pylint: disable=too-many-statements child('xterm') + def test_ESCDELAY_unset_unchanged(): """Unset ESCDELAY leaves DEFAULT_ESCDELAY unchanged in _reinit_escdelay().""" if 'ESCDELAY' in os.environ: @@ -374,6 +375,7 @@ def test_ESCDELAY_unset_unchanged(): blessed.keyboard._reinit_escdelay() assert blessed.keyboard.DEFAULT_ESCDELAY == prev_value + def test_ESCDELAY_bad_value_unchanged(): """Invalid ESCDELAY leaves DEFAULT_ESCDELAY unchanged in _reinit_escdelay().""" os.environ['ESCDELAY'] = 'XYZ123!' @@ -383,6 +385,7 @@ def test_ESCDELAY_bad_value_unchanged(): assert blessed.keyboard.DEFAULT_ESCDELAY == prev_value del os.environ['ESCDELAY'] + def test_ESCDELAY_10ms(): """Verify ESCDELAY modifies DEFAULT_ESCDELAY in _reinit_escdelay().""" os.environ['ESCDELAY'] = '1234' diff --git a/tox.ini b/tox.ini index 017f354d..d3a44051 100644 --- a/tox.ini +++ b/tox.ini @@ -67,8 +67,9 @@ commands = autopep8 --in-place --recursive --aggressive --aggressive blessed/ bin/ setup.py [testenv:docformatter] +# docformatter pinned due to https://github.com/PyCQA/docformatter/issues/264 deps = - docformatter + docformatter<1.7.4 untokenize commands = docformatter \ @@ -83,8 +84,9 @@ commands = {toxinidir}/docs/conf.py [testenv:docformatter_check] +# docformatter pinned due to https://github.com/PyCQA/docformatter/issues/264 deps = - docformatter + docformatter<1.7.4 untokenize commands = docformatter \