diff options
author | Alexey Stepanov <penguinolog@users.noreply.github.com> | 2023-04-05 12:50:17 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-04-05 12:50:17 +0200 |
commit | 9976f338c122a208b8a9108590ea525086cdd5a1 (patch) | |
tree | ed60a446d878799f2c78664f1ccf536cd7d22f33 | |
parent | 3cfa240252ab1efc74772ae45d8c6efe0b4acb39 (diff) | |
download | urwid-9976f338c122a208b8a9108590ea525086cdd5a1.tar.gz |
Fix input handling and extra type annotations (#530)
* keypress always receive `str`
* fix `CF635Screen`: was missed return parameter in get_input_nonblocking
* fix `LCDScreen`: wrong type used for buffer (pyserial return `bytes`, concatenation to `str` is wrong)
Partial #406
Partial #512
Related #408
Co-authored-by: Aleksei Stepanov <alekseis@nvidia.com>
-rwxr-xr-x | urwid/curses_display.py | 62 | ||||
-rwxr-xr-x | urwid/decoration.py | 8 | ||||
-rwxr-xr-x | urwid/display_common.py | 195 | ||||
-rw-r--r-- | urwid/escape.py | 6 | ||||
-rwxr-xr-x | urwid/graphics.py | 1 | ||||
-rwxr-xr-x | urwid/html_fragment.py | 57 | ||||
-rw-r--r-- | urwid/lcd_display.py | 152 | ||||
-rwxr-xr-x | urwid/main_loop.py | 87 | ||||
-rwxr-xr-x | urwid/old_str_util.py | 2 | ||||
-rw-r--r-- | urwid/raw_display.py | 81 | ||||
-rw-r--r-- | urwid/treetools.py | 27 | ||||
-rw-r--r-- | urwid/vterm.py | 44 | ||||
-rwxr-xr-x | urwid/web_display.py | 26 | ||||
-rw-r--r-- | urwid/widget.py | 54 | ||||
-rwxr-xr-x | urwid/wimp.py | 125 |
15 files changed, 539 insertions, 388 deletions
diff --git a/urwid/curses_display.py b/urwid/curses_display.py index bc13125..31e0b22 100755 --- a/urwid/curses_display.py +++ b/urwid/curses_display.py @@ -27,14 +27,18 @@ Curses-based UI implementation from __future__ import annotations import curses +import typing import _curses from urwid import escape from urwid.display_common import UNPRINTABLE_TRANS_TABLE, AttrSpec, BaseScreen, RealTerminal -KEY_RESIZE = 410 # curses.KEY_RESIZE (sometimes not defined) -KEY_MOUSE = 409 # curses.KEY_MOUSE +if typing.TYPE_CHECKING: + from typing_extensions import Literal + +KEY_RESIZE = 410 # curses.KEY_RESIZE (sometimes not defined) +KEY_MOUSE = 409 # curses.KEY_MOUSE _curses_colours = { 'default': (-1, 0), @@ -179,7 +183,7 @@ class Screen(BaseScreen, RealTerminal): self.s.clear() self.s.refresh() - def _getch(self, wait_tenths: int | None): + def _getch(self, wait_tenths: int | None) -> int: if wait_tenths == 0: return self._getch_nodelay() if wait_tenths is None: @@ -189,7 +193,7 @@ class Screen(BaseScreen, RealTerminal): self.s.nodelay(0) return self.s.getch() - def _getch_nodelay(self): + def _getch_nodelay(self) -> int: self.s.nodelay(1) while 1: # this call fails sometimes, but seems to work when I try again @@ -233,7 +237,15 @@ class Screen(BaseScreen, RealTerminal): self.complete_tenths = convert_to_tenths(complete_wait) self.resize_tenths = convert_to_tenths(resize_wait) - def get_input(self, raw_keys=False): + @typing.overload + def get_input(self, raw_keys: Literal[False]) -> list[str]: + ... + + @typing.overload + def get_input(self, raw_keys: Literal[True]) -> tuple[list[str], list[int]]: + ... + + def get_input(self, raw_keys: bool = False) -> list[str] | tuple[list[str], list[int]]: """Return pending input as a list. raw_keys -- return raw keycodes as well as translated versions @@ -276,7 +288,7 @@ class Screen(BaseScreen, RealTerminal): """ assert self._started - keys, raw = self._get_input( self.max_tenths ) + keys, raw = self._get_input(self.max_tenths) # Avoid pegging CPU at 100% when slowly resizing, and work # around a bug with some braindead curses implementations that @@ -286,15 +298,13 @@ class Screen(BaseScreen, RealTerminal): keys, raw2 = self._get_input(self.resize_tenths) raw += raw2 if not keys: - keys, raw2 = self._get_input( - self.resize_tenths) + keys, raw2 = self._get_input(self.resize_tenths) raw += raw2 - if keys!=['window resize']: + if keys != ['window resize']: break - if keys[-1:]!=['window resize']: + if keys[-1:] != ['window resize']: keys.append('window resize') - if keys == ['window resize']: self.prev_input_resize = 2 elif self.prev_input_resize == 2 and not keys: @@ -306,7 +316,7 @@ class Screen(BaseScreen, RealTerminal): return keys, raw return keys - def _get_input(self, wait_tenths): + def _get_input(self, wait_tenths: int | None) -> tuple[list[str], list[int]]: # this works around a strange curses bug with window resizing # not being reported correctly with repeated calls to this # function without a doupdate call in between @@ -319,9 +329,9 @@ class Screen(BaseScreen, RealTerminal): while key >= 0: raw.append(key) - if key==KEY_RESIZE: + if key == KEY_RESIZE: resize = True - elif key==KEY_MOUSE: + elif key == KEY_MOUSE: keys += self._encode_mouse_event() else: keys.append(key) @@ -337,9 +347,9 @@ class Screen(BaseScreen, RealTerminal): key = self._getch(self.complete_tenths) while key >= 0: raw.append(key) - if key==KEY_RESIZE: + if key == KEY_RESIZE: resize = True - elif key==KEY_MOUSE: + elif key == KEY_MOUSE: keys += self._encode_mouse_event() else: keys.append(key) @@ -353,20 +363,24 @@ class Screen(BaseScreen, RealTerminal): return processed, raw - def _encode_mouse_event(self): + def _encode_mouse_event(self) -> list[int]: # convert to escape sequence last = next = self.last_bstate - (id,x,y,z,bstate) = curses.getmouse() + (id, x, y, z, bstate) = curses.getmouse() mod = 0 - if bstate & curses.BUTTON_SHIFT: mod |= 4 - if bstate & curses.BUTTON_ALT: mod |= 8 - if bstate & curses.BUTTON_CTRL: mod |= 16 + if bstate & curses.BUTTON_SHIFT: + mod |= 4 + if bstate & curses.BUTTON_ALT: + mod |= 8 + if bstate & curses.BUTTON_CTRL: + mod |= 16 l = [] - def append_button( b ): + + def append_button(b: int) -> None: b |= mod - l.extend([ 27, '[', 'M', b+32, x+33, y+33 ]) + l.extend([27, ord('['), ord('M'), b+32, x+33, y+33]) if bstate & curses.BUTTON1_PRESSED and last & 1 == 0: append_button( 0 ) @@ -509,7 +523,7 @@ class Screen(BaseScreen, RealTerminal): try: if cs in ("0", "U"): for i in range(len(seg)): - self.s.addch( 0x400000 + seg[i] ) + self.s.addch(0x400000 + seg[i]) else: assert cs is None assert isinstance(seg, bytes) diff --git a/urwid/decoration.py b/urwid/decoration.py index 9722c5a..bcdecde 100755 --- a/urwid/decoration.py +++ b/urwid/decoration.py @@ -414,7 +414,7 @@ class BoxAdapter(WidgetDecoration): return None return self._original_widget.get_pref_col((maxcol, self.height)) - def keypress(self, size: tuple[int], key): + def keypress(self, size: tuple[int], key: str) -> str | None: (maxcol,) = size return self._original_widget.keypress((maxcol, self.height), key) @@ -667,7 +667,7 @@ class Padding(WidgetDecoration): return frows return self._original_widget.rows((maxcol-left-right,), focus=focus) - def keypress(self, size: tuple[int] | tuple[int, int], key): + def keypress(self, size: tuple[int] | tuple[int, int], key: str) -> str | None: """Pass keypress to self._original_widget.""" maxcol = size[0] left, right = self.padding_values(size, True) @@ -897,14 +897,14 @@ class Filler(WidgetDecoration): canv.pad_trim_top_bottom(top, bottom) return canv - def keypress(self, size: tuple[int, int], key): + def keypress(self, size: tuple[int, int], key: str) -> str | None: """Pass keypress to self.original_widget.""" (maxcol, maxrow) = size if self.height_type == PACK: return self._original_widget.keypress((maxcol,), key) top, bottom = self.filler_values((maxcol, maxrow), True) - return self._original_widget.keypress((maxcol,maxrow-top-bottom), key) + return self._original_widget.keypress((maxcol, maxrow-top-bottom), key) def get_cursor_coords(self, size: tuple[int, int]) -> tuple[int, int] | None: """Return cursor coords from self.original_widget if any.""" diff --git a/urwid/display_common.py b/urwid/display_common.py index cceaf07..a4636fe 100755 --- a/urwid/display_common.py +++ b/urwid/display_common.py @@ -23,7 +23,9 @@ from __future__ import annotations import os import sys +import typing import warnings +from collections.abc import Iterable, Sequence try: import termios @@ -33,8 +35,11 @@ except ImportError: from urwid import signals from urwid.util import StoppingContext, int_scale +if typing.TYPE_CHECKING: + from typing_extensions import Literal + # for replacing unprintable bytes with '?' -UNPRINTABLE_TRANS_TABLE = b"?" * 32 + bytes(list(range(32,256))) +UNPRINTABLE_TRANS_TABLE = b"?" * 32 + bytes(range(32, 256)) # signals sent by BaseScreen @@ -43,16 +48,16 @@ INPUT_DESCRIPTORS_CHANGED = "input descriptors changed" # AttrSpec internal values -_BASIC_START = 0 # first index of basic color aliases -_CUBE_START = 16 # first index of color cube -_CUBE_SIZE_256 = 6 # one side of the color cube +_BASIC_START = 0 # first index of basic color aliases +_CUBE_START = 16 # first index of color cube +_CUBE_SIZE_256 = 6 # one side of the color cube _GRAY_SIZE_256 = 24 _GRAY_START_256 = _CUBE_SIZE_256 ** 3 + _CUBE_START -_CUBE_WHITE_256 = _GRAY_START_256 -1 +_CUBE_WHITE_256 = _GRAY_START_256 - 1 _CUBE_SIZE_88 = 4 _GRAY_SIZE_88 = 8 _GRAY_START_88 = _CUBE_SIZE_88 ** 3 + _CUBE_START -_CUBE_WHITE_88 = _GRAY_START_88 -1 +_CUBE_WHITE_88 = _GRAY_START_88 - 1 _CUBE_BLACK = _CUBE_START # values copied from xterm 256colres.h: @@ -148,7 +153,8 @@ _ATTRIBUTES = { 'strikethrough': _STRIKETHROUGH, } -def _value_lookup_table(values, size): + +def _value_lookup_table(values: Sequence[int], size: int) -> list[int]: """ Generate a lookup table for finding the closest item in values. Lookup returns (index into values)+1 @@ -168,6 +174,7 @@ def _value_lookup_table(values, size): lookup_table.extend([i] * count) return lookup_table + _CUBE_256_LOOKUP = _value_lookup_table(_CUBE_STEPS_256, 256) _GRAY_256_LOOKUP = _value_lookup_table([0] + _GRAY_STEPS_256 + [0xff], 256) _CUBE_88_LOOKUP = _value_lookup_table(_CUBE_STEPS_88, 256) @@ -199,7 +206,7 @@ _GRAY_88_LOOKUP_101 = [_GRAY_88_LOOKUP[int_scale(n, 101, 0x100)] # customized by an end-user. -def _gray_num_256(gnum): +def _gray_num_256(gnum: int) -> int: """Return ths color number for gray number gnum. Color cube black and white are returned for 0 and 25 respectively @@ -216,7 +223,7 @@ def _gray_num_256(gnum): return _GRAY_START_256 + gnum -def _gray_num_88(gnum): +def _gray_num_88(gnum: int) -> int: """Return ths color number for gray number gnum. Color cube black and white are returned for 0 and 9 respectively @@ -233,11 +240,12 @@ def _gray_num_88(gnum): return _GRAY_START_88 + gnum -def _color_desc_true(num): +def _color_desc_true(num: int) -> str: return f"#{num:06x}" -def _color_desc_256(num): + +def _color_desc_256(num: int) -> str: """ Return a string description of color number num. 0..15 -> 'h0'..'h15' basic colors (as high-colors) @@ -260,17 +268,17 @@ def _color_desc_256(num): """ assert num >= 0 and num < 256, num if num < _CUBE_START: - return 'h%d' % num + return f'h{num:d}' if num < _GRAY_START_256: num -= _CUBE_START b, num = num % _CUBE_SIZE_256, num // _CUBE_SIZE_256 g, num = num % _CUBE_SIZE_256, num // _CUBE_SIZE_256 r = num % _CUBE_SIZE_256 - return '#%x%x%x' % (_CUBE_STEPS_256_16[r], _CUBE_STEPS_256_16[g], - _CUBE_STEPS_256_16[b]) - return 'g%d' % _GRAY_STEPS_256_101[num - _GRAY_START_256] + return f'#{_CUBE_STEPS_256_16[r]:x}{_CUBE_STEPS_256_16[g]:x}{_CUBE_STEPS_256_16[b]:x}' + return f'g{_GRAY_STEPS_256_101[num - _GRAY_START_256]:d}' -def _color_desc_88(num): + +def _color_desc_88(num: int) -> str: """ Return a string description of color number num. 0..15 -> 'h0'..'h15' basic colors (as high-colors) @@ -291,18 +299,18 @@ def _color_desc_88(num): 'g45' """ - assert num > 0 and num < 88 + assert 0 < num < 88 if num < _CUBE_START: - return 'h%d' % num + return f'h{num:d}' if num < _GRAY_START_88: num -= _CUBE_START b, num = num % _CUBE_SIZE_88, num // _CUBE_SIZE_88 - g, r= num % _CUBE_SIZE_88, num // _CUBE_SIZE_88 - return '#%x%x%x' % (_CUBE_STEPS_88_16[r], _CUBE_STEPS_88_16[g], - _CUBE_STEPS_88_16[b]) - return 'g%d' % _GRAY_STEPS_88_101[num - _GRAY_START_88] + g, r = num % _CUBE_SIZE_88, num // _CUBE_SIZE_88 + return f'#{_CUBE_STEPS_88_16[r]:x}{_CUBE_STEPS_88_16[g]:x}{_CUBE_STEPS_88_16[b]:x}' + return f'g{_GRAY_STEPS_88_101[num - _GRAY_START_88]:d}' + -def _parse_color_true(desc): +def _parse_color_true(desc: str) -> int | None: c = _parse_color_256(desc) if c is not None: @@ -319,7 +327,8 @@ def _parse_color_true(desc): return int(h, 16) return None -def _parse_color_256(desc): + +def _parse_color_256(desc: str) -> int | None: """ Return a color number for the description desc. 'h0'..'h255' -> 0..255 actual color number @@ -388,9 +397,9 @@ def _parse_color_256(desc): return None -def _true_to_256(desc): +def _true_to_256(desc: str) -> str | None: - if not (desc.startswith('#') and len(desc) == 7): + if not (desc.startswith('#') and len(desc) == 7): return None c256 = _parse_color_256("#" + "".join([ @@ -401,7 +410,7 @@ def _true_to_256(desc): return _color_desc_256(c256) -def _parse_color_88(desc): +def _parse_color_88(desc: str) -> int | None: """ Return a color number for the description desc. 'h0'..'h87' -> 0..87 actual color number @@ -472,11 +481,13 @@ def _parse_color_88(desc): except ValueError: return None + class AttrSpecError(Exception): pass + class AttrSpec: - def __init__(self, fg, bg, colors=256): + def __init__(self, fg: str, bg: str, colors: Literal[1, 16, 88, 256, 16777216] = 256) -> None: """ fg -- a string containing a comma-separated foreground color and settings @@ -533,7 +544,7 @@ class AttrSpec: AttrSpec('#ccc', '#000', colors=88) """ if colors not in (1, 16, 88, 256, 2**24): - raise AttrSpecError('invalid number of colors (%d).' % colors) + raise AttrSpecError(f'invalid number of colors ({colors:d}).') self._value = 0 | _HIGH_88_COLOR * (colors == 88) | _HIGH_TRUE_COLOR * (colors == 2**24) self.foreground = fg self.background = bg @@ -544,62 +555,62 @@ class AttrSpec: ) @property - def foreground_basic(self): + def foreground_basic(self) -> bool: return self._value & _FG_BASIC_COLOR != 0 @property - def foreground_high(self): + def foreground_high(self) -> bool: return self._value & _FG_HIGH_COLOR != 0 @property - def foreground_true(self): + def foreground_true(self) -> bool: return self._value & _FG_TRUE_COLOR != 0 @property - def foreground_number(self): + def foreground_number(self) -> int: return self._value & _FG_COLOR_MASK @property - def background_basic(self): + def background_basic(self) -> bool: return self._value & _BG_BASIC_COLOR != 0 @property - def background_high(self): + def background_high(self) -> bool: return self._value & _BG_HIGH_COLOR != 0 @property - def background_true(self): + def background_true(self) -> bool: return self._value & _BG_TRUE_COLOR != 0 @property - def background_number(self): + def background_number(self) -> int: return (self._value & _BG_COLOR_MASK) >> _BG_SHIFT @property - def italics(self): + def italics(self) -> bool: return self._value & _ITALICS != 0 @property - def bold(self): + def bold(self) -> bool: return self._value & _BOLD != 0 @property - def underline(self): + def underline(self) -> bool: return self._value & _UNDERLINE != 0 @property - def blink(self): + def blink(self) -> bool: return self._value & _BLINK != 0 @property - def standout(self): + def standout(self) -> bool: return self._value & _STANDOUT != 0 @property - def strikethrough(self): + def strikethrough(self) -> bool: return self._value & _STRIKETHROUGH != 0 - def _colors(self): + def _colors(self) -> int: """ Return the maximum colors required for this object. @@ -616,7 +627,7 @@ class AttrSpec: return 1 colors = property(_colors) - def __repr__(self): + def __repr__(self) -> str: """ Return an executable python representation of the AttrSpec object. @@ -627,7 +638,7 @@ class AttrSpec: args = f"{args}, colors=88" return f"{self.__class__.__name__}({args})" - def _foreground_color(self): + def _foreground_color(self) -> str: """Return only the color component of the foreground.""" if not (self.foreground_basic or self.foreground_high or self.foreground_true): return 'default' @@ -639,13 +650,18 @@ class AttrSpec: return _color_desc_true(self.foreground_number) return _color_desc_256(self.foreground_number) - def _foreground(self): - return (self._foreground_color() + - ',bold' * self.bold + ',italics' * self.italics + - ',standout' * self.standout + ',blink' * self.blink + - ',underline' * self.underline + ',strikethrough' * self.strikethrough) + def _foreground(self) -> str: + return ( + self._foreground_color() + + ',bold' * self.bold + + ',italics' * self.italics + + ',standout' * self.standout + + ',blink' * self.blink + + ',underline' * self.underline + + ',strikethrough' * self.strikethrough + ) - def _set_foreground(self, foreground): + def _set_foreground(self, foreground: str) -> None: color = None flags = 0 # handle comma-separated foreground @@ -685,7 +701,7 @@ class AttrSpec: foreground = property(_foreground, _set_foreground) - def _background(self): + def _background(self) -> str: """Return the background color.""" if not (self.background_basic or self.background_high or self.background_true): return 'default' @@ -697,7 +713,7 @@ class AttrSpec: return _color_desc_true(self.background_number) return _color_desc_256(self.background_number) - def _set_background(self, background): + def _set_background(self, background: str) -> None: flags = 0 if background in ('', 'default'): color = 0 @@ -755,23 +771,28 @@ class AttrSpec: else: return vals + _COLOR_VALUES_256[self.background_number] - def __eq__(self, other): + def __eq__(self, other: typing.Any) -> bool: return isinstance(other, AttrSpec) and self._value == other._value - def __ne__(self, other): + def __ne__(self, other: typing.Any) -> bool: return not self == other - __hash__ = object.__hash__ - class RealTerminal: - def __init__(self): + def __init__(self) -> None: super().__init__() self._signal_keys_set = False self._old_signal_keys = None - def tty_signal_keys(self, intr=None, quit=None, start=None, - stop=None, susp=None, fileno=None): + def tty_signal_keys( + self, + intr: Literal['undefined'] | int | None = None, + quit: Literal['undefined'] | int | None = None, + start: Literal['undefined'] | int | None = None, + stop: Literal['undefined'] | int | None = None, + susp: Literal['undefined'] | int | None = None, + fileno: int | None = None, + ): """ Read and/or set the tty's signal character settings. This function returns the current settings as a tuple. @@ -792,9 +813,13 @@ class RealTerminal: tattr = termios.tcgetattr(fileno) sattr = tattr[6] - skeys = (sattr[termios.VINTR], sattr[termios.VQUIT], - sattr[termios.VSTART], sattr[termios.VSTOP], - sattr[termios.VSUSP]) + skeys = ( + sattr[termios.VINTR], + sattr[termios.VQUIT], + sattr[termios.VSTART], + sattr[termios.VSTOP], + sattr[termios.VSUSP], + ) if intr == 'undefined': intr = 0 if quit == 'undefined': quit = 0 @@ -808,9 +833,7 @@ class RealTerminal: if stop is not None: tattr[6][termios.VSTOP] = stop if susp is not None: tattr[6][termios.VSUSP] = susp - if intr is not None or quit is not None or \ - start is not None or stop is not None or \ - susp is not None: + if any(item is not None for item in (intr, quit, start, stop, susp)): termios.tcsetattr(fileno, termios.TCSADRAIN, tattr) self._signal_keys_set = True @@ -820,22 +843,23 @@ class RealTerminal: class ScreenError(Exception): pass + class BaseScreen(metaclass=signals.MetaSignals): """ Base class for Screen classes (raw_display.Screen, .. etc) """ signals = [UPDATE_PALETTE_ENTRY, INPUT_DESCRIPTORS_CHANGED] - def __init__(self): + def __init__(self) -> None: super().__init__() self._palette = {} self._started = False @property - def started(self): + def started(self) -> bool: return self._started - def start(self, *args, **kwargs): + def start(self, *args, **kwargs) -> StoppingContext: """Set up the screen. If the screen has already been started, does nothing. @@ -856,7 +880,7 @@ class BaseScreen(metaclass=signals.MetaSignals): def _start(self): pass - def stop(self): + def stop(self) -> None: if self._started: self._stop() self._started = False @@ -877,12 +901,19 @@ class BaseScreen(metaclass=signals.MetaSignals): with self.start(*args, **kwargs): return fn() - def register_palette(self, palette): + def register_palette( + self, + palette: Iterable[ + tuple[str, str] + | tuple[str, str, str] + | tuple[str, str, str, str] + | tuple[str, str, str, str, str, str] + ] + ) -> None: """Register a set of palette entries. palette -- a list of (name, like_other_name) or - (name, foreground, background, mono, foreground_high, - background_high) tuples + (name, foreground, background, mono, foreground_high, background_high) tuples The (name, like_other_name) format will copy the settings from the palette entry like_other_name, which must appear @@ -895,7 +926,7 @@ class BaseScreen(metaclass=signals.MetaSignals): """ for item in palette: - if len(item) in (3,4,6): + if len(item) in (3, 4, 6): self.register_palette_entry(*item) continue if len(item) != 2: @@ -905,8 +936,15 @@ class BaseScreen(metaclass=signals.MetaSignals): raise ScreenError(f"palette entry '{like_name}' doesn't exist") self._palette[name] = self._palette[like_name] - def register_palette_entry(self, name, foreground, background, - mono=None, foreground_high=None, background_high=None): + def register_palette_entry( + self, + name: str, + foreground: str, + background: str, + mono: str | None = None, + foreground_high: str | None = None, + background_high: str | None = None, + ) -> None: """Register a single palette entry. name -- new entry/attribute name @@ -983,13 +1021,14 @@ class BaseScreen(metaclass=signals.MetaSignals): # 'hX' where X > 15 are different in 88/256 color, use # basic colors for 88-color mode if high colors are specified # in this way (also avoids crash when X > 87) - def large_h(desc): + def large_h(desc: str) -> bool: if not desc.startswith('h'): return False if ',' in desc: desc = desc.split(',',1)[0] num = int(desc[1:], 10) return num > 15 + if large_h(foreground_high) or large_h(background_high): high_88 = basic else: diff --git a/urwid/escape.py b/urwid/escape.py index c475da1..81ea576 100644 --- a/urwid/escape.py +++ b/urwid/escape.py @@ -363,7 +363,7 @@ _keyconv = { } -def process_keyqueue(codes: Sequence[int], more_available: bool): +def process_keyqueue(codes: Sequence[int], more_available: bool) -> tuple[list[str], Sequence[int]]: """ codes -- list of key codes more_available -- if True then raise MoreInputRequired when in the @@ -385,7 +385,7 @@ def process_keyqueue(codes: Sequence[int], more_available: bool): em = str_util.get_byte_encoding() - if em == 'wide' and code < 256 and within_double_byte(chr(code), 0, 0): + if em == 'wide' and code < 256 and within_double_byte(code.to_bytes(1, "little"), 0, 0): if not codes[1:] and more_available: raise MoreInputRequired() if codes[1:] and codes[1] < 256: @@ -409,7 +409,7 @@ def process_keyqueue(codes: Sequence[int], more_available: bool): else: return [f"<{code:d}>"], codes[1:] k = codes[i+1] - if k>256 or k&0xc0 != 0x80: + if k > 256 or k & 0xc0 != 0x80: return [f"<{code:d}>"], codes[1:] s = bytes(codes[:need_more+1]) diff --git a/urwid/graphics.py b/urwid/graphics.py index f671c81..b3356a4 100755 --- a/urwid/graphics.py +++ b/urwid/graphics.py @@ -164,6 +164,7 @@ class LineBox(WidgetDecoration, WidgetWrap): lline = SolidFill(lline) if rline: rline = SolidFill(rline) + tlcorner, trcorner = Text(tlcorner), Text(trcorner) blcorner, brcorner = Text(blcorner), Text(brcorner) diff --git a/urwid/html_fragment.py b/urwid/html_fragment.py index c5ce1f2..1f56509 100755 --- a/urwid/html_fragment.py +++ b/urwid/html_fragment.py @@ -26,19 +26,26 @@ HTML PRE-based UI implementation from __future__ import annotations +import typing + from urwid import util from urwid.display_common import AttrSpec, BaseScreen from urwid.main_loop import ExitMainLoop +if typing.TYPE_CHECKING: + from typing_extensions import Literal + # replace control characters with ?'s _trans_table = "?" * 32 + "".join([chr(x) for x in range(32, 256)]) _default_foreground = 'black' _default_background = 'light gray' + class HtmlGeneratorSimulationError(Exception): pass + class HtmlGenerator(BaseScreen): # class variables fragments = [] @@ -49,13 +56,16 @@ class HtmlGenerator(BaseScreen): def __init__(self): super().__init__() self.colors = 16 - self.bright_is_bold = False # ignored - self.has_underline = True # ignored - self.register_palette_entry(None, - _default_foreground, _default_background) + self.bright_is_bold = False # ignored + self.has_underline = True # ignored + self.register_palette_entry(None, _default_foreground, _default_background) - def set_terminal_properties(self, colors=None, bright_is_bold=None, - has_underline=None): + def set_terminal_properties( + self, + colors: int | None = None, + bright_is_bold: bool | None = None, + has_underline: bool | None = None, + ) -> None: if colors is None: colors = self.colors @@ -100,9 +110,7 @@ class HtmlGenerator(BaseScreen): col = 0 for a, cs, run in row: - if not str is bytes: - run = run.decode() - run = run.translate(_trans_table) + run = run.decode().translate(_trans_table) if isinstance(a, AttrSpec): aspec = a else: @@ -141,7 +149,15 @@ class HtmlGenerator(BaseScreen): raise HtmlGeneratorSimulationError("Ran out of screen sizes to return!") return self.sizes.pop(0) - def get_input(self, raw_keys=False): + @typing.overload + def get_input(self, raw_keys: Literal[False]) -> list[str]: + ... + + @typing.overload + def get_input(self, raw_keys: Literal[True]) -> tuple[list[str], list[int]]: + ... + + def get_input(self, raw_keys: bool = False) -> list[str] | tuple[list[str], list[int]]: """Return the next list of keypresses in HtmlGenerator.keys.""" if not self.keys: raise ExitMainLoop() @@ -149,11 +165,12 @@ class HtmlGenerator(BaseScreen): return (self.keys.pop(0), []) return self.keys.pop(0) + _default_aspec = AttrSpec(_default_foreground, _default_background) -(_d_fg_r, _d_fg_g, _d_fg_b, _d_bg_r, _d_bg_g, _d_bg_b) = ( - _default_aspec.get_rgb_values()) +(_d_fg_r, _d_fg_g, _d_fg_b, _d_bg_r, _d_bg_g, _d_bg_b) = _default_aspec.get_rgb_values() -def html_span(s, aspec, cursor = -1): + +def html_span(s, aspec, cursor: int = -1): fg_r, fg_g, fg_b, bg_r, bg_g, bg_b = aspec.get_rgb_values() # use real colours instead of default fg/bg if fg_r is None: @@ -164,13 +181,10 @@ def html_span(s, aspec, cursor = -1): html_bg = f"#{bg_r:02x}{bg_g:02x}{bg_b:02x}" if aspec.standout: html_fg, html_bg = html_bg, html_fg - extra = (";text-decoration:underline" * aspec.underline + - ";font-weight:bold" * aspec.bold) + extra = (";text-decoration:underline" * aspec.underline + ";font-weight:bold" * aspec.bold) def html_span(fg, bg, s): if not s: return "" - return ('<span style="color:%s;' - 'background:%s%s">%s</span>' % - (fg, bg, extra, html_escape(s))) + return f'<span style="color:{fg};background:{bg}{extra}">{html_escape(s)}</span>' if cursor >= 0: c_off, _ign = util.calc_text_pos(s, 0, len(s), cursor) @@ -182,14 +196,15 @@ def html_span(s, aspec, cursor = -1): return html_span(html_fg, html_bg, s) -def html_escape(text): +def html_escape(text: str) -> str: """Escape text so that it will be displayed safely within HTML""" text = text.replace('&','&') text = text.replace('<','<') text = text.replace('>','>') return text -def screenshot_init( sizes, keys ): + +def screenshot_init(sizes: list[tuple[int, int]], keys: list[list[str]]) -> None: """ Replace curses_display.Screen and raw_display.Screen class with HtmlGenerator. @@ -219,7 +234,7 @@ def screenshot_init( sizes, keys ): [ ["down"]*5, ["a","b","c","window resize"], ["Q"] ] ) """ try: - for (row,col) in sizes: + for (row, col) in sizes: assert isinstance(row, int) assert row > 0 and col > 0 except (AssertionError, ValueError): diff --git a/urwid/lcd_display.py b/urwid/lcd_display.py index 9b0a8e8..6e69577 100644 --- a/urwid/lcd_display.py +++ b/urwid/lcd_display.py @@ -23,9 +23,14 @@ from __future__ import annotations import time +import typing +from collections.abc import Iterable, Sequence from .display_common import BaseScreen +if typing.TYPE_CHECKING: + from typing_extensions import Literal + class LCDScreen(BaseScreen): def set_terminal_properties(self, colors=None, bright_is_bold=None, @@ -89,7 +94,7 @@ class CFLCDScreen(LCDScreen): colors = 1 has_underline = False - def __init__(self, device_path, baud): + def __init__(self, device_path: str, baud: int): """ device_path -- eg. '/dev/ttyUSB0' baud -- baud rate @@ -98,44 +103,43 @@ class CFLCDScreen(LCDScreen): self.device_path = device_path from serial import Serial self._device = Serial(device_path, baud, timeout=0) - self._unprocessed = "" - + self._unprocessed = bytearray() @classmethod - def get_crc(cls, buf): + def get_crc(cls, buf: bytearray) -> int: # This seed makes the output of this shift based algorithm match # the table based algorithm. The center 16 bits of the 32-bit # "newCRC" are used for the CRC. The MSB of the lower byte is used # to see what bit was shifted out of the center 16 bit CRC # accumulator ("carry flag analog"); - newCRC = 0x00F32100 + new_crc = 0x00F32100 for byte in buf: # Push this byte’s bits through a software # implementation of a hardware shift & xor. for bit_count in range(8): # Shift the CRC accumulator - newCRC >>= 1 + new_crc >>= 1 # The new MSB of the CRC accumulator comes # from the LSB of the current data byte. - if ord(byte) & (0x01 << bit_count): - newCRC |= 0x00800000 + if byte & (0x01 << bit_count): + new_crc |= 0x00800000 # If the low bit of the current CRC accumulator was set # before the shift, then we need to XOR the accumulator # with the polynomial (center 16 bits of 0x00840800) - if newCRC & 0x00000080: - newCRC ^= 0x00840800 + if new_crc & 0x00000080: + new_crc ^= 0x00840800 # All the data has been done. Do 16 more bits of 0 data. for bit_count in range(16): # Shift the CRC accumulator - newCRC >>= 1 + new_crc >>= 1 # If the low bit of the current CRC accumulator was set # before the shift we need to XOR the accumulator with # 0x00840800. - if newCRC & 0x00000080: - newCRC ^= 0x00840800 + if new_crc & 0x00000080: + new_crc ^= 0x00840800 # Return the center 16 bits, making this CRC match the one’s # complement that is sent in the packet. - return ((~newCRC)>>8) & 0xffff + return ((~new_crc) >> 8) & 0xffff def _send_packet(self, command, data): """ @@ -148,7 +152,7 @@ class CFLCDScreen(LCDScreen): buf = buf + chr(crc & 0xff) + chr(crc >> 8) self._device.write(buf) - def _read_packet(self): + def _read_packet(self) -> tuple[int, bytearray] | None: """ low-level packet reading. returns (command/report code, data) or None @@ -157,25 +161,26 @@ class CFLCDScreen(LCDScreen): is received. """ # pull in any new data available - self._unprocessed = self._unprocessed + self._device.read() + self._unprocessed += self._device.read() while True: try: command, data, unprocessed = self._parse_data(self._unprocessed) self._unprocessed = unprocessed return command, data except self.MoreDataRequired: - return + return None except self.InvalidPacket: # throw out a byte and try to parse again self._unprocessed = self._unprocessed[1:] class InvalidPacket(Exception): pass + class MoreDataRequired(Exception): pass @classmethod - def _parse_data(cls, data): + def _parse_data(cls, data: bytearray) -> tuple[int, bytearray, bytearray]: """ Try to read a packet from the start of data, returning (command/report code, packet_data, remaining_data) @@ -183,18 +188,22 @@ class CFLCDScreen(LCDScreen): """ if len(data) < 2: raise cls.MoreDataRequired - command = ord(data[0]) - plen = ord(data[1]) - if plen > cls.MAX_PACKET_DATA_LENGTH: + + command: int = data[0] + packet_len: int = data[1] + + if packet_len > cls.MAX_PACKET_DATA_LENGTH: raise cls.InvalidPacket("length value too large") - if len(data) < plen + 4: + + if len(data) < packet_len + 4: raise cls.MoreDataRequired - crc = cls.get_crc(data[:2 + plen]) - pcrc = ord(data[2 + plen]) + (ord(data[3 + plen]) << 8 ) + + data_end = 2 + packet_len + crc = cls.get_crc(data[:data_end]) + pcrc = ord(data[data_end: data_end + 1]) + (ord(data[data_end + 1: data_end + 2]) << 8) if crc != pcrc: raise cls.InvalidPacket("CRC doesn't match") - return (command, data[2:2 + plen], data[4 + plen:]) - + return command, data[2: data_end], data[data_end + 2:] class KeyRepeatSimulator: @@ -205,50 +214,48 @@ class KeyRepeatSimulator: If two or more keys are pressed disable repeating until all keys are released. """ - def __init__(self, repeat_delay, repeat_next): + def __init__(self, repeat_delay: float | int, repeat_next: float | int) -> None: """ repeat_delay -- seconds to wait before starting to repeat keys repeat_next -- time between each repeated key """ self.repeat_delay = repeat_delay self.repeat_next = repeat_next - self.pressed = {} + self.pressed: dict[str, float] = {} self.multiple_pressed = False - def press(self, key): + def press(self, key: str) -> None: if self.pressed: self.multiple_pressed = True self.pressed[key] = time.time() - def release(self, key): + def release(self, key: str) -> None: if key not in self.pressed: - return # ignore extra release events + return # ignore extra release events del self.pressed[key] if not self.pressed: self.multiple_pressed = False - def next_event(self): + def next_event(self) -> tuple[float, str] | None: """ Return (remaining, key) where remaining is the number of seconds (float) until the key repeat event should be sent, or None if no events are pending. """ if len(self.pressed) != 1 or self.multiple_pressed: - return + return None for key in self.pressed: - return max(0, self.pressed[key] + self.repeat_delay - - time.time()), key + return max(0., self.pressed[key] + self.repeat_delay - time.time()), key - def sent_event(self): + def sent_event(self) -> None: """ Cakk this method when you have sent a key repeat event so the timer will be reset for the next event """ if len(self.pressed) != 1: - return # ignore event that shouldn't have been sent + return # ignore event that shouldn't have been sent for key in self.pressed: - self.pressed[key] = ( - time.time() - self.repeat_delay + self.repeat_next) + self.pressed[key] = (time.time() - self.repeat_delay + self.repeat_next) return @@ -294,9 +301,14 @@ class CF635Screen(CFLCDScreen): cursor_style = CFLCDScreen.CURSOR_INVERTING_BLINKING_BLOCK - def __init__(self, device_path, baud=115200, - repeat_delay=0.5, repeat_next=0.125, - key_map=['up', 'down', 'left', 'right', 'enter', 'esc']): + def __init__( + self, + device_path: str, + baud: int = 115200, + repeat_delay: float = 0.5, + repeat_next: float = 0.125, + key_map: Iterable[str] = ('up', 'down', 'left', 'right', 'enter', 'esc'), + ): """ device_path -- eg. '/dev/ttyUSB0' baud -- baud rate @@ -309,7 +321,7 @@ class CF635Screen(CFLCDScreen): self.repeat_delay = repeat_delay self.repeat_next = repeat_next self.key_repeat = KeyRepeatSimulator(repeat_delay, repeat_next) - self.key_map = key_map + self.key_map = tuple(key_map) self._last_command = None self._last_command_time = 0 @@ -318,7 +330,6 @@ class CF635Screen(CFLCDScreen): self._previous_canvas = None self._update_cursor = False - def get_input_descriptors(self): """ return the fd from our serial device so we get called @@ -326,7 +337,7 @@ class CF635Screen(CFLCDScreen): """ return [self._device.fd] - def get_input_nonblocking(self): + def get_input_nonblocking(self) -> tuple[None, list[str], list[int]]: """ Return a (next_input_timeout, keys_pressed, raw_keycodes) tuple. @@ -341,18 +352,16 @@ class CF635Screen(CFLCDScreen): raw_keycodes are the bytes of messages we received, which might not seem to have any correspondence to keys_pressed. """ - input = [] - raw_input = [] + data_input: list[str] = [] + raw_data_input: list[int] = [] timeout = None - while True: - packet = self._read_packet() - if not packet: - break + packet = self._read_packet() + while packet: command, data = packet if command == self.CMD_KEY_ACTIVITY and data: - d0 = ord(data[0]) + d0 = data[0] if 1 <= d0 <= 12: release = d0 > 6 keycode = d0 - (release * 6) - 1 @@ -360,26 +369,27 @@ class CF635Screen(CFLCDScreen): if release: self.key_repeat.release(key) else: - input.append(key) + data_input.append(key) self.key_repeat.press(key) - raw_input.append(d0) + raw_data_input.append(d0) - elif command & 0xc0 == 0x40: # "ACK" + elif command & 0xc0 == 0x40: # "ACK" if command & 0x3f == self._last_command: self._send_next_command() + packet = self._read_packet() + next_repeat = self.key_repeat.next_event() if next_repeat: timeout, key = next_repeat if not timeout: - input.append(key) + data_input.append(key) self.key_repeat.sent_event() timeout = None - return timeout, input, [] + return timeout, data_input, raw_data_input - - def _send_next_command(self): + def _send_next_command(self) -> None: """ send out the next command in the queue """ @@ -391,13 +401,13 @@ class CF635Screen(CFLCDScreen): self._last_command = command # record command for ACK self._last_command_time = time.time() - def queue_command(self, command, data): + def queue_command(self, command: int, data: str) -> None: self._command_queue.append((command, data)) # not waiting? send away! if self._last_command is None: self._send_next_command() - def draw_screen(self, size, canvas): + def draw_screen(self, size: tuple[int, int], canvas): assert size == self.DISPLAY_SIZE if self._screen_buf: @@ -412,8 +422,7 @@ class CF635Screen(CFLCDScreen): for a, cs, run in row: text.append(run) if not osb or osb[y] != text: - self.queue_command(self.CMD_LCD_DATA, chr(0) + chr(y) + - "".join(text)) + self.queue_command(self.CMD_LCD_DATA, chr(0) + chr(y) + "".join(text)) sb.append(text) y += 1 @@ -432,7 +441,7 @@ class CF635Screen(CFLCDScreen): self._screen_buf = sb self._previous_canvas = canvas - def program_cgram(self, index, data): + def program_cgram(self, index: int, data: Sequence[int]) -> None: """ Program character data. Characters available as chr(0) through chr(7), and repeated as chr(8) through chr(15). @@ -444,10 +453,9 @@ class CF635Screen(CFLCDScreen): """ assert 0 <= index <= 7 assert len(data) == 8 - self.queue_command(self.CMD_CGRAM, chr(index) + - "".join([chr(x) for x in data])) + self.queue_command(self.CMD_CGRAM, chr(index) + "".join([chr(x) for x in data])) - def set_cursor_style(self, style): + def set_cursor_style(self, style: Literal[1, 2, 3, 4]) -> None: """ style -- CURSOR_BLINKING_BLOCK, CURSOR_UNDERSCORE, CURSOR_BLINKING_BLOCK_UNDERSCORE or @@ -457,7 +465,7 @@ class CF635Screen(CFLCDScreen): self.cursor_style = style self._update_cursor = True - def set_backlight(self, value): + def set_backlight(self, value: int) -> None: """ Set backlight brightness @@ -466,14 +474,14 @@ class CF635Screen(CFLCDScreen): assert 0 <= value <= 100 self.queue_command(self.CMD_BACKLIGHT, chr(value)) - def set_lcd_contrast(self, value): + def set_lcd_contrast(self, value: int) -> None: """ value -- 0 to 255 """ assert 0 <= value <= 255 self.queue_command(self.CMD_LCD_CONTRAST, chr(value)) - def set_led_pin(self, led, rg, value): + def set_led_pin(self, led: Literal[0, 1, 2, 3], rg: Literal[0, 1], value: int): """ led -- 0 to 3 rg -- 0 for red, 1 for green @@ -482,6 +490,4 @@ class CF635Screen(CFLCDScreen): assert 0 <= led <= 3 assert rg in (0, 1) assert 0 <= value <= 100 - self.queue_command(self.CMD_GPO, chr(12 - 2 * led - rg) + - chr(value)) - + self.queue_command(self.CMD_GPO, chr(12 - 2 * led - rg) + chr(value)) diff --git a/urwid/main_loop.py b/urwid/main_loop.py index 8cee637..7f93f2e 100755 --- a/urwid/main_loop.py +++ b/urwid/main_loop.py @@ -30,6 +30,8 @@ import select import signal import sys import time +import typing +from collections.abc import Callable, Iterable from functools import wraps from itertools import count from weakref import WeakKeyDictionary @@ -37,7 +39,7 @@ from weakref import WeakKeyDictionary try: import fcntl except ImportError: - pass # windows + pass # windows from urwid import signals from urwid.command_map import REDRAW_SCREEN, command_map @@ -46,8 +48,13 @@ from urwid.display_common import INPUT_DESCRIPTORS_CHANGED from urwid.util import StoppingContext, is_mouse_event from urwid.wimp import PopUpTarget +if typing.TYPE_CHECKING: + from .display_common import BaseScreen + from .widget import Widget + PIPE_BUFFER_READ_SIZE = 4096 # can expect this much on Linux, so try for that + class ExitMainLoop(Exception): """ When this exception is raised within a main loop the main loop @@ -55,9 +62,11 @@ class ExitMainLoop(Exception): """ pass + class CantUseExternalLoop(Exception): pass + class MainLoop: """ This is the standard main loop implementation for a single interactive @@ -104,9 +113,17 @@ class MainLoop: The event loop object this main loop uses for waiting on alarms and IO """ - def __init__(self, widget, palette=(), screen=None, - handle_mouse=True, input_filter=None, unhandled_input=None, - event_loop=None, pop_ups=False): + def __init__( + self, + widget: Widget, + palette=(), + screen: BaseScreen | None = None, + handle_mouse: bool = True, + input_filter: Callable[[list[str], list[int]], list[str]] | None = None, + unhandled_input: Callable[[str], bool] | None = None, + event_loop=None, + pop_ups: bool = False, + ): self._widget = widget self.handle_mouse = handle_mouse self.pop_ups = pop_ups # triggers property setting side-effect @@ -124,10 +141,8 @@ class MainLoop: self._unhandled_input = unhandled_input self._input_filter = input_filter - if not hasattr(screen, 'hook_event_loop' - ) and event_loop is not None: - raise NotImplementedError("screen object passed " - "%r does not support external event loops" % (screen,)) + if not hasattr(screen, 'hook_event_loop') and event_loop is not None: + raise NotImplementedError(f"screen object passed {screen!r} does not support external event loops") if event_loop is None: event_loop = SelectEventLoop() self.event_loop = event_loop @@ -385,11 +400,11 @@ class MainLoop: try: self.event_loop.run() except: - self.screen.stop() # clean up screen control + self.screen.stop() # clean up screen control raise self.stop() - def _update(self, keys, raw): + def _update(self, keys: list[str], raw: list[int]): """ >>> w = _refl("widget") >>> w.selectable_rval = True @@ -414,7 +429,7 @@ class MainLoop: if 'window resize' in keys: self.screen_size = None - def _run_screen_event_loop(self): + def _run_screen_event_loop(self) -> None: """ This method is used when the screen does not support using external event loops. @@ -430,7 +445,8 @@ class MainLoop: if not next_alarm and self.event_loop._alarms: next_alarm = heapq.heappop(self.event_loop._alarms) - keys = None + keys: list[str] = [] + raw: list[int] = [] while not keys: if next_alarm: sec = max(0, next_alarm[0] - time.time()) @@ -484,7 +500,7 @@ class MainLoop: screen.get_input(True) """ - def process_input(self, keys): + def process_input(self, keys: Iterable[str]) -> bool: """ This method will pass keyboard input and mouse events to :attr:`widget`. This method is called automatically from the :meth:`run` method when @@ -537,7 +553,7 @@ class MainLoop: True """ - def input_filter(self, keys, raw): + def input_filter(self, keys: list[str], raw: list[int]) -> list[str]: """ This function is passed each all the input events and raw keystroke values. These values are passed to the *input_filter* function @@ -549,7 +565,7 @@ class MainLoop: return self._input_filter(keys, raw) return keys - def unhandled_input(self, input): + def unhandled_input(self, input: str) -> bool: """ This function is called with any input that was not handled by the widgets, and calls the *unhandled_input* function passed to the @@ -594,7 +610,7 @@ class EventLoop: Abstract class representing an event loop to be used by :class:`MainLoop`. """ - def alarm(self, seconds, callback): + def alarm(self, seconds: float | int, callback): """ Call callback() a given time from now. No parameters are passed to callback. @@ -685,6 +701,7 @@ class EventLoop: """ return signal.signal(signum, handler) + class SelectEventLoop(EventLoop): """ Event loop based on :func:`select.select` @@ -697,7 +714,7 @@ class SelectEventLoop(EventLoop): self._idle_callbacks = {} self._tie_break = count() - def alarm(self, seconds, callback): + def alarm(self, seconds: float | int, callback): """ Call callback() a given time from now. No parameters are passed to callback. @@ -804,8 +821,7 @@ class SelectEventLoop(EventLoop): if self._alarms: tm = self._alarms[0][0] timeout = max(0, tm - time.time()) - if self._did_something and (not self._alarms or - (self._alarms and timeout > 0)): + if self._did_something and (not self._alarms or (self._alarms and timeout > 0)): timeout = 0 tm = 'idle' ready, w, err = select.select(fds, [], fds, timeout) @@ -846,7 +862,7 @@ class GLibEventLoop(EventLoop): self._enable_glib_idle() self._signal_handlers = {} - def alarm(self, seconds, callback): + def alarm(self, seconds: float | int, callback): """ Call callback() a given time from now. No parameters are passed to callback. @@ -943,8 +959,7 @@ class GLibEventLoop(EventLoop): callback() self._enable_glib_idle() return True - self._watch_files[fd] = \ - self.GLib.io_add_watch(fd,self.GLib.IO_IN,io_callback) + self._watch_files[fd] = self.GLib.io_add_watch(fd, self.GLib.IO_IN, io_callback) return fd def remove_watch_file(self, handle): @@ -1009,7 +1024,7 @@ class GLibEventLoop(EventLoop): self._exc_info = None reraise(*exc_info) - def handle_exit(self,f): + def handle_exit(self, f): """ Decorator that cleanly exits the :class:`GLibEventLoop` if :exc:`ExitMainLoop` is thrown inside of the wrapped function. Store the @@ -1018,9 +1033,9 @@ class GLibEventLoop(EventLoop): *f* -- function to be wrapped """ - def wrapper(*args,**kargs): + def wrapper(*args, **kargs): try: - return f(*args,**kargs) + return f(*args, **kargs) except ExitMainLoop: self._loop.quit() except: @@ -1054,7 +1069,7 @@ class TornadoEventLoop(EventLoop): self._idle_done = False self._prev_timeout = 0 - def __getattr__(self, name): + def __getattr__(self, name: str): return getattr(self.__poll_obj, name) def poll(self, timeout): @@ -1106,7 +1121,7 @@ class TornadoEventLoop(EventLoop): self._exception = None def alarm(self, secs, callback): - ioloop = self._ioloop + ioloop = self._ioloop def wrapped(): try: del self._pending_alarms[handle] @@ -1180,6 +1195,7 @@ try: except ImportError: FileDescriptor = object + class TwistedInputDescriptor(FileDescriptor): def __init__(self, reactor, fd, cb): self._fileno = fd @@ -1197,9 +1213,9 @@ class TwistedEventLoop(EventLoop): """ Event loop based on Twisted_ """ - _idle_emulation_delay = 1.0/256 # a short time (in seconds) + _idle_emulation_delay = 1.0/256 # a short time (in seconds) - def __init__(self, reactor=None, manage_reactor=True): + def __init__(self, reactor=None, manage_reactor: bool = True): """ :param reactor: reactor to use :type reactor: :class:`twisted.internet.reactor`. @@ -1270,8 +1286,7 @@ class TwistedEventLoop(EventLoop): fd -- file descriptor to watch for input callback -- function to call when input is available """ - ind = TwistedInputDescriptor(self.reactor, fd, - self.handle_exit(callback)) + ind = TwistedInputDescriptor(self.reactor, fd, self.handle_exit(callback)) self._watch_files[fd] = ind self.reactor.addReader(ind) return fd @@ -1345,7 +1360,7 @@ class TwistedEventLoop(EventLoop): self._exc_info = None reraise(*exc_info) - def handle_exit(self, f, enable_idle=True): + def handle_exit(self, f, enable_idle: bool = True): """ Decorator that cleanly exits the :class:`TwistedEventLoop` if :class:`ExitMainLoop` is thrown inside of the wrapped function. Store the @@ -1499,7 +1514,7 @@ class AsyncioEventLoop(EventLoop): from ._async_kw_event_loop import TrioEventLoop -def _refl(name, rval=None, exit=False): +def _refl(name: str, rval=None, exit=False): """ This function is used to test the main loop classes. @@ -1516,9 +1531,10 @@ def _refl(name, rval=None, exit=False): """ class Reflect: - def __init__(self, name, rval=None): + def __init__(self, name: int, rval=None): self._name = name self._rval = rval + def __call__(self, *argl, **argd): args = ", ".join([repr(a) for a in argl]) if args and argd: @@ -1528,6 +1544,7 @@ def _refl(name, rval=None, exit=False): if exit: raise ExitMainLoop() return self._rval + def __getattr__(self, attr): if attr.endswith("_rval"): raise AttributeError() @@ -1537,9 +1554,11 @@ def _refl(name, rval=None, exit=False): return Reflect(f"{self._name}.{attr}") return Reflect(name) + def _test(): import doctest doctest.testmod() + if __name__=='__main__': _test() diff --git a/urwid/old_str_util.py b/urwid/old_str_util.py index 3a9bd84..8861b53 100755 --- a/urwid/old_str_util.py +++ b/urwid/old_str_util.py @@ -317,7 +317,7 @@ def within_double_byte(text: bytes, line_start: int, pos: int) -> Literal[0, 1, assert isinstance(text, bytes) v = text[pos] - if v >= 0x40 and v < 0x7f: + if 0x40 <= v < 0x7f: # might be second half of big5, uhc or gbk encoding if pos == line_start: return 0 diff --git a/urwid/raw_display.py b/urwid/raw_display.py index bbdf758..1b06862 100644 --- a/urwid/raw_display.py +++ b/urwid/raw_display.py @@ -32,6 +32,7 @@ import select import signal import struct import sys +import typing try: import fcntl @@ -52,6 +53,9 @@ from urwid.display_common import ( RealTerminal, ) +if typing.TYPE_CHECKING: + from typing_extensions import Literal + class Screen(BaseScreen, RealTerminal): def __init__(self, input=sys.stdin, output=sys.stdout): @@ -61,10 +65,9 @@ class Screen(BaseScreen, RealTerminal): super().__init__() self._pal_escape = {} self._pal_attrspec = {} - signals.connect_signal(self, UPDATE_PALETTE_ENTRY, - self._on_update_palette_entry) - self.colors = 16 # FIXME: detect this - self.has_underline = True # FIXME: detect this + signals.connect_signal(self, UPDATE_PALETTE_ENTRY, self._on_update_palette_entry) + self.colors = 16 # FIXME: detect this + self.has_underline = True # FIXME: detect this self._keyqueue = [] self.prev_input_resize = 0 self.set_input_timeouts() @@ -72,8 +75,8 @@ class Screen(BaseScreen, RealTerminal): self._screen_buf_canvas = None self._resized = False self.maxrow = None - self.gpm_mev = None - self.gpm_event_pending = False + self.gpm_mev: Popen | None = None + self.gpm_event_pending: bool = False self._mouse_tracking_enabled = False self.last_bstate = 0 self._setup_G1_done = False @@ -210,8 +213,7 @@ class Screen(BaseScreen, RealTerminal): return if not Popen: return - m = Popen(["/usr/bin/mev","-e","158"], stdin=PIPE, stdout=PIPE, - close_fds=True) + m = Popen(["/usr/bin/mev", "-e", "158"], stdin=PIPE, stdout=PIPE, close_fds=True, encoding="ascii") fcntl.fcntl(m.stdout.fileno(), fcntl.F_SETFL, os.O_NONBLOCK) self.gpm_mev = m @@ -303,7 +305,15 @@ class Screen(BaseScreen, RealTerminal): """ self._term_output_file.flush() - def get_input(self, raw_keys=False): + @typing.overload + def get_input(self, raw_keys: Literal[False]) -> list[str]: + ... + + @typing.overload + def get_input(self, raw_keys: Literal[True]) -> tuple[list[str], list[int]]: + ... + + def get_input(self, raw_keys: bool = False) -> list[str] | tuple[list[str], list[int]]: """Return pending input as a list. raw_keys -- return raw keycodes as well as translated versions @@ -353,21 +363,21 @@ class Screen(BaseScreen, RealTerminal): keys, raw = self.parse_input(None, None, self.get_available_raw_input()) # Avoid pegging CPU at 100% when slowly resizing - if keys==['window resize'] and self.prev_input_resize: + if keys == ['window resize'] and self.prev_input_resize: while True: self._wait_for_input_ready(self.resize_wait) keys, raw2 = self.parse_input(None, None, self.get_available_raw_input()) raw += raw2 - #if not keys: - # keys, raw2 = self._get_input( - # self.resize_wait) - # raw += raw2 - if keys!=['window resize']: + # if not keys: + # keys, raw2 = self._get_input( + # self.resize_wait) + # raw += raw2 + if keys != ['window resize']: break - if keys[-1:]!=['window resize']: + if keys[-1:] != ['window resize']: keys.append('window resize') - if keys==['window resize']: + if keys == ['window resize']: self.prev_input_resize = 2 elif self.prev_input_resize == 2 and not keys: self.prev_input_resize = 1 @@ -378,7 +388,7 @@ class Screen(BaseScreen, RealTerminal): return keys, raw return keys - def get_input_descriptors(self): + def get_input_descriptors(self) -> list[int]: """ Return a list of integer file descriptors that should be polled in external event loops to check for user input. @@ -571,7 +581,7 @@ class Screen(BaseScreen, RealTerminal): break return ready - def _getch(self, timeout): + def _getch(self, timeout: int) ->int: ready = self._wait_for_input_ready(timeout) if self.gpm_mev is not None: if self.gpm_mev.stdout.fileno() in ready: @@ -581,17 +591,17 @@ class Screen(BaseScreen, RealTerminal): return ord(os.read(fd, 1)) return -1 - def _encode_gpm_event( self ): + def _encode_gpm_event(self) -> list[int]: self.gpm_event_pending = False - s = self.gpm_mev.stdout.readline().decode('ascii') - l = s.split(",") + s = self.gpm_mev.stdout.readline() + l = s.split(", ") if len(l) != 6: # unexpected output, stop tracking self._stop_gpm_tracking() signals.emit_signal(self, INPUT_DESCRIPTORS_CHANGED) return [] ev, x, y, ign, b, m = s.split(",") - ev = int( ev.split("x")[-1], 16) + ev = int(ev.split("x")[-1], 16) x = int( x.split(" ")[-1] ) y = int( y.lstrip().split(" ")[0] ) b = int( b.split(" ")[-1] ) @@ -603,15 +613,18 @@ class Screen(BaseScreen, RealTerminal): l = [] mod = 0 - if m & 1: mod |= 4 # shift - if m & 10: mod |= 8 # alt - if m & 4: mod |= 16 # ctrl - - def append_button( b ): + if m & 1: + mod |= 4 # shift + if m & 10: + mod |= 8 # alt + if m & 4: + mod |= 16 # ctrl + + def append_button(b): b |= mod - l.extend([ 27, ord('['), ord('M'), b+32, x+32, y+32 ]) + l.extend([27, ord('['), ord('M'), b+32, x+32, y+32]) - def determine_button_release( flag ): + def determine_button_release(flag: int) -> None: if b & 4 and last & 1: append_button( 0 + flag ) next |= 1 @@ -670,21 +683,19 @@ class Screen(BaseScreen, RealTerminal): def _getch_nodelay(self): return self._getch(0) - def get_cols_rows(self): """Return the terminal dimensions (num columns, num rows).""" y, x = 24, 80 try: if hasattr(self._term_output_file, 'fileno'): - buf = fcntl.ioctl(self._term_output_file.fileno(), - termios.TIOCGWINSZ, ' '*4) + buf = fcntl.ioctl(self._term_output_file.fileno(), termios.TIOCGWINSZ, ' '*4) y, x = struct.unpack('hh', buf) except OSError: # Term size could not be determined pass # Provide some lightweight fallbacks in case the TIOCWINSZ doesn't # give sane answers - if (x <= 0 or y <= 0) and self.term in ('ansi','vt100'): + if (x <= 0 or y <= 0) and self.term in ('ansi', 'vt100'): y, x = 24, 80 self.maxrow = y return x, y @@ -856,7 +867,7 @@ class Screen(BaseScreen, RealTerminal): icss = escape.IBMPC_ON else: icss = escape.SO - o += [ "\x08"*back, + o += ["\x08" * back, ias, icss, escape.INSERT_ON, inserttext, escape.INSERT_OFF ] diff --git a/urwid/treetools.py b/urwid/treetools.py index 94d7c69..a6480ba 100644 --- a/urwid/treetools.py +++ b/urwid/treetools.py @@ -67,8 +67,7 @@ class TreeWidget(urwid.WidgetWrap): [self.unexpanded_icon, self.expanded_icon][self.expanded]), widget], dividechars=1) indent_cols = self.get_indent_cols() - return urwid.Padding(widget, - width=('relative', 100), left=indent_cols) + return urwid.Padding(widget, width=('relative', 100), left=indent_cols) def update_expanded_icon(self): """Update display widget text for parent widgets""" @@ -138,7 +137,7 @@ class TreeWidget(urwid.WidgetWrap): prevnode = thisnode.get_parent() return prevnode.get_widget() - def keypress(self, size, key): + def keypress(self, size, key: str) -> str | None: """Handle expand & collapse requests (non-leaf nodes)""" if self.is_leaf: return key @@ -154,8 +153,8 @@ class TreeWidget(urwid.WidgetWrap): else: return key - def mouse_event(self, size, event, button, col, row, focus): - if self.is_leaf or event != 'mouse press' or button!=1: + def mouse_event(self, size, event, button: int, col: int, row: int, focus: bool) -> bool: + if self.is_leaf or event != 'mouse press' or button !=1: return False if row == 0 and col == self.get_indent_cols(): @@ -416,11 +415,11 @@ class TreeListBox(urwid.ListBox): """A ListBox with special handling for navigation and collapsing of TreeWidgets""" - def keypress(self, size, key): + def keypress(self, size: tuple[int, int], key: str) -> str | None: key = super().keypress(size, key) return self.unhandled_input(size, key) - def unhandled_input(self, size, input): + def unhandled_input(self, size: tuple[int, int], input: str) -> str | None: """Handle macro-navigation keys""" if input == 'left': self.move_focus_to_parent(size) @@ -429,7 +428,7 @@ class TreeListBox(urwid.ListBox): else: return input - def collapse_focus_parent(self, size): + def collapse_focus_parent(self, size: tuple[int, int]) -> None: """Collapse parent directory.""" widget, pos = self.body.get_focus() @@ -439,7 +438,7 @@ class TreeListBox(urwid.ListBox): if pos != ppos: self.keypress(size, "-") - def move_focus_to_parent(self, size): + def move_focus_to_parent(self, size: tuple[int, int]) -> None: """Move focus to parent of widget in focus.""" widget, pos = self.body.get_focus() @@ -449,7 +448,7 @@ class TreeListBox(urwid.ListBox): if parentpos is None: return - middle, top, bottom = self.calculate_visible( size ) + middle, top, bottom = self.calculate_visible(size) row_offset, focus_widget, focus_pos, focus_rows, cursor = middle trim_top, fill_above = top @@ -462,20 +461,20 @@ class TreeListBox(urwid.ListBox): self.change_focus(size, pos.get_parent()) - def _keypress_max_left(self, size): + def _keypress_max_left(self, size: tuple[int, int]) -> None: return self.focus_home(size) - def _keypress_max_right(self, size): + def _keypress_max_right(self, size: tuple[int, int]) -> None: return self.focus_end(size) - def focus_home(self, size): + def focus_home(self, size: tuple[int, int]) -> None: """Move focus to very top.""" widget, pos = self.body.get_focus() rootnode = pos.get_root() self.change_focus(size, rootnode) - def focus_end( self, size ): + def focus_end(self, size: tuple[int, int]) -> None: """Move focus to far bottom.""" maxrow, maxcol = size diff --git a/urwid/vterm.py b/urwid/vterm.py index 34c29c6..e234a76 100644 --- a/urwid/vterm.py +++ b/urwid/vterm.py @@ -1327,8 +1327,14 @@ class Terminal(Widget): signals = ['closed', 'beep', 'leds', 'title'] - def __init__(self, command, env=None, main_loop=None, escape_sequence=None, - encoding='utf-8'): + def __init__( + self, + command, + env=None, + main_loop=None, + escape_sequence=None, + encoding='utf-8', + ): """ A terminal emulator within a widget. @@ -1388,7 +1394,7 @@ class Terminal(Widget): self.has_focus = False self.terminated = False - def get_cursor_coords(self, size): + def get_cursor_coords(self, size: tuple[int, int]) -> tuple[int, int] | None: """Return the cursor coordinates for this terminal """ if self.term is None: @@ -1410,7 +1416,7 @@ class Terminal(Widget): return (x, y) - def spawn(self): + def spawn(self) -> None: env = self.env env['TERM'] = 'linux' @@ -1434,7 +1440,7 @@ class Terminal(Widget): atexit.register(self.terminate) - def terminate(self): + def terminate(self) -> None: if self.terminated: return @@ -1462,28 +1468,28 @@ class Terminal(Widget): os.close(self.master) - def beep(self): + def beep(self) -> None: self._emit('beep') - def leds(self, which): + def leds(self, which) -> None: self._emit('leds', which) - def respond(self, string): + def respond(self, string) -> None: """ Respond to the underlying application with 'string'. """ self.response_buffer.append(string) - def flush_responses(self): + def flush_responses(self) -> None: for string in self.response_buffer: os.write(self.master, string.encode('ascii')) self.response_buffer = [] - def set_termsize(self, width, height): + def set_termsize(self, width: int, height: int) -> None: winsize = struct.pack("HHHH", height, width, 0, 0) fcntl.ioctl(self.master, termios.TIOCSWINSZ, winsize) - def touch_term(self, width, height): + def touch_term(self, width: int, height: int) -> None: process_opened = False if self.pid is None: @@ -1506,10 +1512,10 @@ class Terminal(Widget): if process_opened: self.add_watch() - def set_title(self, title): + def set_title(self, title) -> None: self._emit('title', title) - def change_focus(self, has_focus): + def change_focus(self, has_focus) -> None: """ Ignore SIGINT if this widget has focus. """ @@ -1529,7 +1535,7 @@ class Terminal(Widget): if hasattr(self, "old_tios"): RealTerminal().tty_signal_keys(*self.old_tios) - def render(self, size, focus=False): + def render(self, size: tuple[int, int], focus: bool = False): if not self.terminated: self.change_focus(focus) @@ -1541,17 +1547,17 @@ class Terminal(Widget): return self.term - def add_watch(self): + def add_watch(self) -> None: if self.main_loop is None: return self.main_loop.watch_file(self.master, self.feed) - def remove_watch(self): + def remove_watch(self) -> None: if self.main_loop is None: return self.main_loop.remove_watch_file(self.master) - def wait_and_feed(self, timeout=1.0): + def wait_and_feed(self, timeout: float = 1.0) -> None: while True: try: select.select([self.master], [], [], timeout) @@ -1561,7 +1567,7 @@ class Terminal(Widget): raise self.feed() - def feed(self): + def feed(self) -> None: data = EOF try: @@ -1583,7 +1589,7 @@ class Terminal(Widget): self.flush_responses() - def keypress(self, size, key): + def keypress(self, size: tuple[int, int], key: str) -> str | None: if self.terminated: return key diff --git a/urwid/web_display.py b/urwid/web_display.py index 37e021a..c51c2a4 100755 --- a/urwid/web_display.py +++ b/urwid/web_display.py @@ -32,9 +32,13 @@ import select import signal import socket import sys +import typing from urwid import util +if typing.TYPE_CHECKING: + from typing_extensions import Literal + _js_code = r""" // Urwid web (CGI/Asynchronous Javascript) display module // Copyright (C) 2004-2005 Ian Ward @@ -883,31 +887,37 @@ class Screen: """Return the screen size.""" return self.screen_size - def get_input(self, raw_keys=False): + @typing.overload + def get_input(self, raw_keys: Literal[False]) -> list[str]: + ... + + @typing.overload + def get_input(self, raw_keys: Literal[True]) -> tuple[list[str], list[int]]: + ... + + def get_input(self, raw_keys: bool = False) -> list[str] | tuple[list[str], list[int]]: """Return pending input as a list.""" l = [] resized = False try: - iready,oready,eready = select.select( - [self.input_fd],[],[],0.5) + iready, oready, eready = select.select([self.input_fd],[],[],0.5) except OSError as e: # return on interruptions if e.args[0] == 4: if raw_keys: - return [],[] + return [], [] return [] raise if not iready: if raw_keys: - return [],[] + return [], [] return [] keydata = os.read(self.input_fd, MAX_READ) os.close(self.input_fd) - self.input_fd = os.open(f"{self.pipe_name}.in", - os.O_NONBLOCK | os.O_RDONLY) + self.input_fd = os.open(f"{self.pipe_name}.in", os.O_NONBLOCK | os.O_RDONLY) #sys.stderr.write( repr((keydata,self.input_tail))+"\n" ) keys = keydata.split("\n") keys[0] = self.input_tail + keys[0] @@ -915,7 +925,7 @@ class Screen: for k in keys[:-1]: if k.startswith("window resize "): - ign1,ign2,x,y = k.split(" ",3) + ign1, ign2, x, y = k.split(" ", 3) x = int(x) y = int(y) self._set_screen_size(x, y) diff --git a/urwid/widget.py b/urwid/widget.py index 9de17e0..52b8597 100644 --- a/urwid/widget.py +++ b/urwid/widget.py @@ -521,7 +521,7 @@ class Widget(metaclass=WidgetMeta): """ return self._sizing - def pack(self, size, focus=False): + def pack(self, size: tuple[[]] | tuple[int] | tuple[int, int], focus: bool = False) -> tuple[int, int]: """ See :meth:`Widget.render` for parameter details. @@ -550,8 +550,7 @@ class Widget(metaclass=WidgetMeta): """ if not size: if FIXED in self.sizing(): - raise NotImplementedError('Fixed widgets must override' - ' Widget.pack()') + raise NotImplementedError('Fixed widgets must override Widget.pack()') raise WidgetError(f'Cannot pack () size, this is not a fixed widget: {self!r}') elif len(size) == 1: if FLOW in self.sizing(): @@ -560,7 +559,7 @@ class Widget(metaclass=WidgetMeta): return size @property - def base_widget(self): + def base_widget(self) -> Widget: """ Read-only property that steps through decoration widgets and returns the one at the base. This default implementation @@ -569,7 +568,7 @@ class Widget(metaclass=WidgetMeta): return self @property - def focus(self): + def focus(self) -> Widget | None: """ Read-only property returning the child widget in focus for container widgets. This default implementation @@ -630,13 +629,13 @@ class FlowWidget(Widget): DeprecationWarning, ) - def rows(self, size, focus=False): + def rows(self, size: int, focus: bool = False) -> int: """ All flow widgets must implement this function. """ raise NotImplementedError() - def render(self, size, focus=False): + def render(self, size: tuple[int], focus: bool = False): """ All widgets must implement this function. """ @@ -670,7 +669,7 @@ class BoxWidget(Widget): DeprecationWarning, ) - def render(self, size, focus=False): + def render(self, size: tuple[int, int], focus: bool = False): """ All widgets must implement this function. """ @@ -1183,16 +1182,16 @@ class Edit(Text): # (this variable is picked up by the MetaSignals metaclass) signals = ["change", "postchange"] - def valid_char(self, ch): + def valid_char(self, ch: str) -> bool: """ Filter for text that may be entered into this widget by the user :param ch: character to be inserted - :type ch: bytes or unicode + :type ch: str This implementation returns True for all printable characters. """ - return is_wide_char(ch,0) or (len(ch)==1 and ord(ch) >= 32) + return is_wide_char(ch, 0) or (len(ch) == 1 and ord(ch) >= 32) def __init__( self, @@ -1535,7 +1534,7 @@ class Edit(Text): result_pos += len(text) return (result_text, result_pos) - def keypress(self, size: tuple[int], key: str | bytes): + def keypress(self, size: tuple[int], key: str) -> str | None: """ Handle editing keystrokes, return others. @@ -1770,9 +1769,9 @@ class IntEdit(Edit): """ Return true for decimal digits. """ - return len(ch)==1 and ch in "0123456789" + return len(ch) == 1 and ch in "0123456789" - def __init__(self,caption="",default=None): + def __init__(self, caption="", default: int | str = None) -> None: """ caption -- caption markup default -- default edit value @@ -1780,11 +1779,13 @@ class IntEdit(Edit): >>> IntEdit(u"", 42) <IntEdit selectable flow widget '42' edit_pos=2> """ - if default is not None: val = str(default) - else: val = "" - super().__init__(caption,val) + if default is not None: + val = str(default) + else: + val = "" + super().__init__(caption, val) - def keypress(self, size, key): + def keypress(self, size: tuple[int], key: str) -> str | None: """ Handle editing keystrokes. Remove leading zeros. @@ -1808,7 +1809,7 @@ class IntEdit(Edit): return unhandled - def value(self): + def value(self) -> int: """ Return the numeric value of self.edit_text. @@ -1824,7 +1825,7 @@ class IntEdit(Edit): return 0 -def delegate_to_widget_mixin(attribute_name): +def delegate_to_widget_mixin(attribute_name: str): """ Return a mixin class that delegates all standard widget methods to an attribute given by attribute_name. @@ -1835,10 +1836,11 @@ def delegate_to_widget_mixin(attribute_name): # when layout and rendering are separated get_delegate = attrgetter(attribute_name) + class DelegateToWidgetMixin(Widget): - no_cache = ["rows"] # crufty metaclass work-around + no_cache = ["rows"] # crufty metaclass work-around - def render(self, size, focus=False): + def render(self, size, focus: bool = False) -> CompositeCanvas: canv = get_delegate(self).render(size, focus=focus) return CompositeCanvas(canv) @@ -1881,12 +1883,12 @@ def delegate_to_widget_mixin(attribute_name): return DelegateToWidgetMixin - class WidgetWrapError(Exception): pass + class WidgetWrap(delegate_to_widget_mixin('_wrapped_widget'), Widget): - def __init__(self, w): + def __init__(self, w: Widget): """ w -- widget to wrap, stored as self._w @@ -1932,10 +1934,10 @@ class WidgetWrap(delegate_to_widget_mixin('_wrapped_widget'), Widget): w = property(_raise_old_name_error, _raise_old_name_error) - def _test(): import doctest doctest.testmod() -if __name__=='__main__': + +if __name__ == '__main__': _test() diff --git a/urwid/wimp.py b/urwid/wimp.py index 622af1d..7376dce 100755 --- a/urwid/wimp.py +++ b/urwid/wimp.py @@ -22,6 +22,9 @@ from __future__ import annotations +import typing +from collections.abc import MutableSequence + from urwid.canvas import CompositeCanvas from urwid.command_map import ACTIVATE from urwid.container import Columns, Overlay @@ -32,10 +35,17 @@ from urwid.text_layout import calc_coords from urwid.util import is_mouse_press from urwid.widget import BOX, FLOW, Text, WidgetWrap, delegate_to_widget_mixin +if typing.TYPE_CHECKING: + from typing_extensions import Literal + + from urwid.canvas import Canvas, TextCanvas + from urwid.widget import Widget + class SelectableIcon(Text): ignore_focus = False _selectable = True + def __init__(self, text, cursor_position=0): """ :param text: markup for this widget; see :class:`Text` for @@ -50,7 +60,7 @@ class SelectableIcon(Text): super().__init__(text) self._cursor_position = cursor_position - def render(self, size, focus=False): + def render(self, size: tuple[int], focus: bool = False) -> TextCanvas | CompositeCanvas: """ Render the text content of this widget with a cursor when in focus. @@ -73,7 +83,7 @@ class SelectableIcon(Text): c.cursor = self.get_cursor_coords(size) return c - def get_cursor_coords(self, size): + def get_cursor_coords(self, size: tuple[int]) -> tuple[int, int] | None: """ Return the position of the cursor if visible. This method is required for widgets that display a cursor. @@ -89,16 +99,18 @@ class SelectableIcon(Text): return None return x, y - def keypress(self, size, key): + def keypress(self, size: tuple[int], key: str) -> str: """ No keys are handled by this widget. This method is required for selectable widgets. """ return key + class CheckBoxError(Exception): pass + class CheckBox(WidgetWrap): def sizing(self): return frozenset([FLOW]) @@ -114,8 +126,15 @@ class CheckBox(WidgetWrap): # (this variable is picked up by the MetaSignals metaclass) signals = ["change", 'postchange'] - def __init__(self, label, state=False, has_mixed=False, - on_state_change=None, user_data=None, checked_symbol=None): + def __init__( + self, + label, + state: bool | Literal['mixed'] = False, + has_mixed: bool = False, + on_state_change=None, + user_data=None, + checked_symbol: str | None = None, + ): """ :param label: markup for check box label :param state: False, True or "mixed" @@ -200,7 +219,7 @@ class CheckBox(WidgetWrap): return self._label.text label = property(get_label) - def set_state(self, state, do_callback=True): + def set_state(self, state: bool | Literal['mixed'], do_callback: bool = True) -> None: """ Set the CheckBox state. @@ -249,12 +268,12 @@ class CheckBox(WidgetWrap): if do_callback and old_state is not None: self._emit('postchange', old_state) - def get_state(self): + def get_state(self) -> bool | Literal['mixed']: """Return the state of the checkbox.""" return self._state state = property(get_state, set_state) - def keypress(self, size, key): + def keypress(self, size: tuple[int], key: str) -> str | None: """ Toggle state on 'activate' command. @@ -276,7 +295,7 @@ class CheckBox(WidgetWrap): self.toggle_state() - def toggle_state(self): + def toggle_state(self) -> None: """ Cycle to the next valid state. @@ -303,7 +322,7 @@ class CheckBox(WidgetWrap): elif self.state == 'mixed': self.set_state(False) - def mouse_event(self, size, event, button, x, y, focus): + def mouse_event(self, size: tuple[int], event, button: int, x: int, y: int, focus: bool) -> bool: """ Toggle state on button 1 press. @@ -329,8 +348,14 @@ class RadioButton(CheckBox): 'mixed': SelectableIcon("(#)", 1) } reserve_columns = 4 - def __init__(self, group, label, state="first True", - on_state_change=None, user_data=None): + def __init__( + self, + group: MutableSequence[CheckBox], + label, + state: bool | Literal['mixed', 'first True'] = "first True", + on_state_change=None, + user_data=None, + ) -> None: """ :param group: list for radio buttons in same group :param label: markup for radio button label @@ -365,17 +390,14 @@ class RadioButton(CheckBox): >>> b2.render((15,), focus=True).text # ... = b in Python 3 [...'( ) Disagree '] """ - if state=="first True": + if state == "first True": state = not group self.group = group - super().__init__(label, state, False, on_state_change, - user_data) + super().__init__(label, state, False, on_state_change, user_data) group.append(self) - - - def set_state(self, state, do_callback=True): + def set_state(self, state: bool | Literal['mixed'], do_callback: bool = True) -> None: """ Set the RadioButton state. @@ -416,12 +438,12 @@ class RadioButton(CheckBox): # clear the state of each other radio button for cb in self.group: - if cb is self: continue + if cb is self: + continue if cb._state: cb.set_state(False) - - def toggle_state(self): + def toggle_state(self) -> None: """ Set state to True. @@ -449,7 +471,7 @@ class Button(WidgetWrap): signals = ["click"] - def __init__(self, label, on_press=None, user_data=None): + def __init__(self, label, on_press=None, user_data=None) -> None: """ :param label: markup for button label :param on_press: shorthand for connect_signal() @@ -493,7 +515,7 @@ class Button(WidgetWrap): return super()._repr_words() + [ repr(self.label)] - def set_label(self, label): + def set_label(self, label) -> None: """ Change the button label. @@ -519,7 +541,7 @@ class Button(WidgetWrap): return self._label.text label = property(get_label) - def keypress(self, size, key): + def keypress(self, size: tuple[int], key: str) -> str | None: """ Send 'click' signal on 'activate' command. @@ -541,7 +563,7 @@ class Button(WidgetWrap): self._emit('click') - def mouse_event(self, size, event, button, x, y, focus): + def mouse_event(self, size: tuple[int], event, button: int, x: int, y: int, focus: bool) -> bool: """ Send 'click' signal on button 1 press. @@ -566,7 +588,7 @@ class Button(WidgetWrap): class PopUpLauncher(delegate_to_widget_mixin('_original_widget'), WidgetDecoration): - def __init__(self, original_widget): + def __init__(self, original_widget: Widget) -> None: super().__init__(original_widget) self._pop_up_widget = None @@ -588,15 +610,15 @@ class PopUpLauncher(delegate_to_widget_mixin('_original_widget'), WidgetDecorati """ raise NotImplementedError("Subclass must override this method") - def open_pop_up(self, *args, **kwargs): + def open_pop_up(self, *args, **kwargs) -> None: self._pop_up_widget = self.create_pop_up(*args, **kwargs) self._invalidate() - def close_pop_up(self): + def close_pop_up(self) -> None: self._pop_up_widget = None self._invalidate() - def render(self, size, focus=False): + def render(self, size, focus: bool = False) -> CompositeCanvas | Canvas: canv = super().render(size, focus) if self._pop_up_widget: canv = CompositeCanvas(canv) @@ -610,23 +632,27 @@ class PopUpTarget(WidgetDecoration): _sizing = {BOX} _selectable = True - def __init__(self, original_widget): + def __init__(self, original_widget: Widget) -> None: super().__init__(original_widget) self._pop_up = None self._current_widget = self._original_widget - def _update_overlay(self, size, focus): + def _update_overlay(self, size: tuple[int, int], focus: bool) -> None: canv = self._original_widget.render(size, focus=focus) - self._cache_original_canvas = canv # imperfect performance hack + self._cache_original_canvas = canv # imperfect performance hack pop_up = canv.get_pop_up() if pop_up: - left, top, ( - w, overlay_width, overlay_height) = pop_up + left, top, (w, overlay_width, overlay_height) = pop_up if self._pop_up != w: self._pop_up = w - self._current_widget = Overlay(w, self._original_widget, - ('fixed left', left), overlay_width, - ('fixed top', top), overlay_height) + self._current_widget = Overlay( + w, + self._original_widget, + ('fixed left', left), + overlay_width, + ('fixed top', top), + overlay_height, + ) else: self._current_widget.set_overlay_parameters( ('fixed left', left), overlay_width, @@ -635,36 +661,39 @@ class PopUpTarget(WidgetDecoration): self._pop_up = None self._current_widget = self._original_widget - def render(self, size, focus=False): + def render(self, size: tuple[int, int], focus: bool = False) -> Canvas: self._update_overlay(size, focus) return self._current_widget.render(size, focus=focus) - def get_cursor_coords(self, size): + + def get_cursor_coords(self, size: tuple[int, int]) -> tuple[int, int] | None: self._update_overlay(size, True) return self._current_widget.get_cursor_coords(size) - def get_pref_col(self, size): + + def get_pref_col(self, size: tuple[int, int]) -> int: self._update_overlay(size, True) return self._current_widget.get_pref_col(size) - def keypress(self, size, key): + + def keypress(self, size: tuple[int, int], key: str) -> str | None: self._update_overlay(size, True) return self._current_widget.keypress(size, key) - def move_cursor_to_coords(self, size, x, y): + + def move_cursor_to_coords(self, size: tuple[int, int], x: int, y: int): self._update_overlay(size, True) return self._current_widget.move_cursor_to_coords(size, x, y) - def mouse_event(self, size, event, button, x, y, focus): + + def mouse_event(self, size: tuple[int, int], event, button: int, x: int, y: int, focus: bool) -> bool | None: self._update_overlay(size, focus) return self._current_widget.mouse_event(size, event, button, x, y, focus) - def pack(self, size=None, focus=False): + + def pack(self, size: tuple[int, int] | None = None, focus: bool = False) -> tuple[int, int]: self._update_overlay(size, focus) return self._current_widget.pack(size) - - - - def _test(): import doctest doctest.testmod() + if __name__=='__main__': _test() |