summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlexey Stepanov <penguinolog@users.noreply.github.com>2023-04-05 12:50:17 +0200
committerGitHub <noreply@github.com>2023-04-05 12:50:17 +0200
commit9976f338c122a208b8a9108590ea525086cdd5a1 (patch)
treeed60a446d878799f2c78664f1ccf536cd7d22f33
parent3cfa240252ab1efc74772ae45d8c6efe0b4acb39 (diff)
downloadurwid-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-xurwid/curses_display.py62
-rwxr-xr-xurwid/decoration.py8
-rwxr-xr-xurwid/display_common.py195
-rw-r--r--urwid/escape.py6
-rwxr-xr-xurwid/graphics.py1
-rwxr-xr-xurwid/html_fragment.py57
-rw-r--r--urwid/lcd_display.py152
-rwxr-xr-xurwid/main_loop.py87
-rwxr-xr-xurwid/old_str_util.py2
-rw-r--r--urwid/raw_display.py81
-rw-r--r--urwid/treetools.py27
-rw-r--r--urwid/vterm.py44
-rwxr-xr-xurwid/web_display.py26
-rw-r--r--urwid/widget.py54
-rwxr-xr-xurwid/wimp.py125
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('&','&amp;')
text = text.replace('<','&lt;')
text = text.replace('>','&gt;')
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()