summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlexey Stepanov <penguinolog@users.noreply.github.com>2023-04-04 13:31:23 +0200
committerGitHub <noreply@github.com>2023-04-04 13:31:23 +0200
commit3cfa240252ab1efc74772ae45d8c6efe0b4acb39 (patch)
tree057956da0f02c903685e75efc2d294e67a9f748a
parent2d27ffc2bebc6436fc38ee3f1b3828635dc976a3 (diff)
downloadurwid-3cfa240252ab1efc74772ae45d8c6efe0b4acb39.tar.gz
Annotate types in simple cases and use isinstance (& protocol) based type checking (#529)
* Use `super()` where possible instead of direct base class Related #525 Partial #406 Fix #510 Co-authored-by: Aleksei Stepanov <alekseis@nvidia.com>
-rw-r--r--urwid/__init__.py53
-rw-r--r--urwid/canvas.py308
-rwxr-xr-xurwid/container.py420
-rwxr-xr-xurwid/curses_display.py84
-rwxr-xr-xurwid/decoration.py404
-rwxr-xr-xurwid/display_common.py3
-rw-r--r--urwid/escape.py109
-rwxr-xr-xurwid/font.py17
-rwxr-xr-xurwid/graphics.py96
-rwxr-xr-xurwid/html_fragment.py8
-rw-r--r--urwid/listbox.py314
-rwxr-xr-xurwid/main_loop.py5
-rwxr-xr-xurwid/monitored_list.py55
-rw-r--r--urwid/numedit.py53
-rwxr-xr-xurwid/old_str_util.py67
-rw-r--r--urwid/raw_display.py1
-rw-r--r--urwid/signals.py17
-rw-r--r--urwid/text_layout.py232
-rw-r--r--urwid/treetools.py2
-rw-r--r--urwid/util.py60
-rw-r--r--urwid/vterm.py4
-rw-r--r--urwid/widget.py198
22 files changed, 1392 insertions, 1118 deletions
diff --git a/urwid/__init__.py b/urwid/__init__.py
index 99eaab5..e28b9a8 100644
--- a/urwid/__init__.py
+++ b/urwid/__init__.py
@@ -97,31 +97,10 @@ from urwid.graphics import (
ProgressBar,
scale_bar_values,
)
-from urwid.listbox import (
- ListBox,
- ListBoxError,
- ListWalker,
- ListWalkerError,
- SimpleFocusListWalker,
- SimpleListWalker,
-)
-from urwid.main_loop import (
- AsyncioEventLoop,
- ExitMainLoop,
- GLibEventLoop,
- MainLoop,
- SelectEventLoop,
- TornadoEventLoop,
-)
+from urwid.listbox import ListBox, ListBoxError, ListWalker, ListWalkerError, SimpleFocusListWalker, SimpleListWalker
+from urwid.main_loop import AsyncioEventLoop, ExitMainLoop, GLibEventLoop, MainLoop, SelectEventLoop, TornadoEventLoop
from urwid.monitored_list import MonitoredFocusList, MonitoredList
-from urwid.signals import (
- MetaSignals,
- Signals,
- connect_signal,
- disconnect_signal,
- emit_signal,
- register_signal,
-)
+from urwid.signals import MetaSignals, Signals, connect_signal, disconnect_signal, emit_signal, register_signal
from urwid.version import VERSION, __version__
from urwid.widget import (
ANY,
@@ -160,15 +139,7 @@ from urwid.widget import (
delegate_to_widget_mixin,
fixed_size,
)
-from urwid.wimp import (
- Button,
- CheckBox,
- CheckBoxError,
- PopUpLauncher,
- PopUpTarget,
- RadioButton,
- SelectableIcon,
-)
+from urwid.wimp import Button, CheckBox, CheckBoxError, PopUpLauncher, PopUpTarget, RadioButton, SelectableIcon
try:
from urwid.main_loop import TwistedEventLoop
@@ -204,20 +175,8 @@ from urwid.display_common import (
RealTerminal,
ScreenError,
)
-from urwid.text_layout import (
- LayoutSegment,
- StandardTextLayout,
- TextLayout,
- default_layout,
-)
-from urwid.treetools import (
- ParentNode,
- TreeListBox,
- TreeNode,
- TreeWalker,
- TreeWidget,
- TreeWidgetError,
-)
+from urwid.text_layout import LayoutSegment, StandardTextLayout, TextLayout, default_layout
+from urwid.treetools import ParentNode, TreeListBox, TreeNode, TreeWalker, TreeWidget, TreeWidgetError
from urwid.util import (
MetaSuper,
TagMarkupException,
diff --git a/urwid/canvas.py b/urwid/canvas.py
index 1f4789e..b1f7c9c 100644
--- a/urwid/canvas.py
+++ b/urwid/canvas.py
@@ -22,7 +22,9 @@
from __future__ import annotations
+import typing
import weakref
+from collections.abc import Sequence
from urwid.text_layout import LayoutSegment, trim_line
from urwid.util import (
@@ -36,6 +38,9 @@ from urwid.util import (
trim_text_attr_cs,
)
+if typing.TYPE_CHECKING:
+ from .widget import Widget
+
class CanvasCache:
"""
@@ -185,6 +190,7 @@ class CanvasCache:
class CanvasError(Exception):
pass
+
class Canvas:
"""
base class for canvases
@@ -202,7 +208,12 @@ class Canvas:
"lists, please see the TextCanvas class for the new "
"representation of canvas content.")
- def __init__(self, value1=None, value2=None, value3=None):
+ def __init__(
+ self,
+ value1: typing.Any = None,
+ value2: typing.Any = None,
+ value3: typing.Any = None,
+ ) -> None:
"""
value1, value2, value3 -- if not None, raise a helpful error:
the old Canvas class is now called TextCanvas.
@@ -233,7 +244,7 @@ class Canvas:
return self._widget_info
widget_info = property(_get_widget_info)
- def _raise_old_repr_error(self, val=None):
+ def _raise_old_repr_error(self, val=None) -> typing.NoReturn:
raise self._old_repr_error
def _text_content(self):
@@ -248,8 +259,14 @@ class Canvas:
attr = property(_raise_old_repr_error, _raise_old_repr_error)
cs = property(_raise_old_repr_error, _raise_old_repr_error)
- def content(self, trim_left=0, trim_top=0, cols=None, rows=None,
- attr=None):
+ def content(
+ self,
+ trim_left: int = 0,
+ trim_top: int = 0,
+ cols: int | None = None,
+ rows: int | None = None,
+ attr=None,
+ ):
raise NotImplementedError()
def cols(self):
@@ -261,11 +278,12 @@ class Canvas:
def content_delta(self):
raise NotImplementedError()
- def get_cursor(self):
+ def get_cursor(self) -> tuple[int, int] | None:
c = self.coords.get("cursor", None)
if not c:
- return
- return c[:2] # trim off data part
+ return None
+ return c[:2] # trim off data part
+
def set_cursor(self, c):
if self.widget_info and self.cacheable:
raise self._finalized_error
@@ -275,7 +293,7 @@ class Canvas:
except KeyError:
pass
return
- self.coords["cursor"] = c + (None,) # data part
+ self.coords["cursor"] = c + (None,) # data part
cursor = property(get_cursor, set_cursor)
def get_pop_up(self):
@@ -283,7 +301,8 @@ class Canvas:
if not c:
return
return c
- def set_pop_up(self, w, left, top, overlay_width, overlay_height):
+
+ def set_pop_up(self, w: Widget, left: int, top: int, overlay_width: int, overlay_height: int):
"""
This method adds pop-up information to the canvas. This information
is intercepted by a PopUpTarget widget higher in the chain to
@@ -304,10 +323,9 @@ class Canvas:
if self.widget_info and self.cacheable:
raise self._finalized_error
- self.coords["pop up"] = (left, top, (
- w, overlay_width, overlay_height))
+ self.coords["pop up"] = (left, top, (w, overlay_width, overlay_height))
- def translate_coords(self, dx, dy):
+ def translate_coords(self, dx: int, dy: int):
"""
Return coords shifted by (dx, dy).
"""
@@ -317,13 +335,19 @@ class Canvas:
return d
-
class TextCanvas(Canvas):
"""
class for storing rendered text and attributes
"""
- def __init__(self, text=None, attr=None, cs=None,
- cursor=None, maxcol=None, check_width=True):
+ def __init__(
+ self,
+ text: Sequence[bytes] | None = None,
+ attr=None,
+ cs=None,
+ cursor: tuple[int, int] | None = None,
+ maxcol: int | None = None,
+ check_width: bool = True,
+ ) -> None:
"""
text -- list of strings, one for each line
attr -- list of run length encoded attributes for text
@@ -332,18 +356,18 @@ class TextCanvas(Canvas):
maxcol -- screen columns taken by this canvas
check_width -- check and fix width of all lines in text
"""
- Canvas.__init__(self)
+ super().__init__()
if text is None:
text = []
if check_width:
widths = []
for t in text:
- if type(t) != bytes:
+ if not isinstance(t, bytes):
raise CanvasError("Canvas text must be plain strings encoded in the screen's encoding", repr(text))
- widths.append( calc_width( t, 0, len(t)) )
+ widths.append(calc_width(t, 0, len(t)))
else:
- assert type(maxcol) == int
+ assert isinstance(maxcol, int)
widths = [maxcol] * len(text)
if maxcol is None:
@@ -354,9 +378,9 @@ class TextCanvas(Canvas):
maxcol = 0
if attr is None:
- attr = [[] for x in range(len(text))]
+ attr = [[]] * len(text)
if cs is None:
- cs = [[] for x in range(len(text))]
+ cs = [[]] * len(text)
# pad text and attr to maxcol
for i in range(len(text)):
@@ -364,7 +388,7 @@ class TextCanvas(Canvas):
if w > maxcol:
raise CanvasError(f"Canvas text is wider than the maxcol specified \n{maxcol!r}\n{widths!r}\n{text!r}")
if w < maxcol:
- text[i] = text[i] + b''.rjust(maxcol-w)
+ text[i] += b''.rjust(maxcol - w)
a_gap = len(text[i]) - rle_len( attr[i] )
if a_gap < 0:
raise CanvasError(f"Attribute extends beyond text \n{text[i]!r}\n{attr[i]!r}" )
@@ -383,28 +407,34 @@ class TextCanvas(Canvas):
self._text = text
self._maxcol = maxcol
- def rows(self):
+ def rows(self) -> int:
"""Return the number of rows in this canvas."""
rows = len(self._text)
assert isinstance(rows, int)
return rows
- def cols(self):
+ def cols(self) -> int:
"""Return the screen column width of this canvas."""
return self._maxcol
- def translated_coords(self,dx,dy):
+ def translated_coords(self, dx: int, dy: int) -> tuple[int, int] | None:
"""
Return cursor coords shifted by (dx, dy), or None if there
is no cursor.
"""
if self.cursor:
x, y = self.cursor
- return x+dx, y+dy
+ return x + dx, y + dy
return None
- def content(self, trim_left=0, trim_top=0, cols=None, rows=None,
- attr_map=None):
+ def content(
+ self,
+ trim_left: int = 0,
+ trim_top: int = 0,
+ cols: int = 0,
+ rows: int = 0,
+ attr_map=None,
+ ):
"""
Return the canvas content as a list of rows where each row
is a list of (attr, cs, text) tuples.
@@ -419,9 +449,9 @@ class TextCanvas(Canvas):
if not rows:
rows = maxrow - trim_top
- assert trim_left >= 0 and trim_left < maxcol
+ assert 0 <= trim_left < maxcol
assert cols > 0 and trim_left + cols <= maxcol
- assert trim_top >=0 and trim_top < maxrow
+ assert 0 <= trim_top < maxrow
assert rows > 0 and trim_top + rows <= maxrow
if trim_top or rows < maxrow:
@@ -447,7 +477,6 @@ class TextCanvas(Canvas):
i += run
yield row
-
def content_delta(self, other):
"""
Return the differences between other and this canvas.
@@ -457,20 +486,26 @@ class TextCanvas(Canvas):
content().
"""
if other is self:
- return [self.cols()]*self.rows()
+ return [self.cols()] * self.rows()
return self.content()
-
class BlankCanvas(Canvas):
"""
a canvas with nothing on it, only works as part of a composite canvas
since it doesn't know its own size
"""
- def __init__(self):
- Canvas.__init__(self, None)
-
- def content(self, trim_left, trim_top, cols, rows, attr):
+ def __init__(self) -> None:
+ super().__init__(None)
+
+ def content(
+ self,
+ trim_left: int = 0,
+ trim_top: int = 0,
+ cols: int = 0,
+ rows: int = 0,
+ attr=None,
+ ):
"""
return (cols, rows) of spaces with default attributes.
"""
@@ -478,18 +513,19 @@ class BlankCanvas(Canvas):
if attr and None in attr:
def_attr = attr[None]
line = [(def_attr, None, b''.rjust(cols))]
- for i in range(rows):
+ for _ in range(rows):
yield line
- def cols(self):
+ def cols(self) -> typing.NoReturn:
raise NotImplementedError("BlankCanvas doesn't know its own size!")
- def rows(self):
+ def rows(self) -> typing.NoReturn:
raise NotImplementedError("BlankCanvas doesn't know its own size!")
- def content_delta(self):
+ def content_delta(self) -> typing.NoReturn:
raise NotImplementedError("BlankCanvas doesn't know its own size!")
+
blank_canvas = BlankCanvas()
@@ -497,8 +533,8 @@ class SolidCanvas(Canvas):
"""
A canvas filled completely with a single character.
"""
- def __init__(self, fill_char, cols, rows):
- Canvas.__init__(self)
+ def __init__(self, fill_char, cols: int, rows: int) -> None:
+ super().__init__()
end, col = calc_text_pos(fill_char, 0, len(fill_char), 1)
assert col == 1, f"Invalid fill_char: {fill_char!r}"
self._text, cs = apply_target_encoding(fill_char[:end])
@@ -506,14 +542,20 @@ class SolidCanvas(Canvas):
self.size = cols, rows
self.cursor = None
- def cols(self):
+ def cols(self) -> int:
return self.size[0]
- def rows(self):
+ def rows(self) -> int:
return self.size[1]
- def content(self, trim_left=0, trim_top=0, cols=None, rows=None,
- attr=None):
+ def content(
+ self,
+ trim_left: int = 0,
+ trim_top: int = 0,
+ cols: int | None = None,
+ rows: int | None = None,
+ attr=None,
+ ):
if cols is None:
cols = self.size[0]
if rows is None:
@@ -523,7 +565,7 @@ class SolidCanvas(Canvas):
def_attr = attr[None]
line = [(def_attr, self._cs, self._text*cols)]
- for i in range(rows):
+ for _ in range(rows):
yield line
def content_delta(self, other):
@@ -535,13 +577,11 @@ class SolidCanvas(Canvas):
return self.content()
-
-
class CompositeCanvas(Canvas):
"""
class for storing a combination of canvases
"""
- def __init__(self, canv=None):
+ def __init__(self, canv: Canvas = None) -> None:
"""
canv -- a Canvas object to wrap this CompositeCanvas around.
@@ -558,7 +598,7 @@ class CompositeCanvas(Canvas):
# tuples that define the unfinished cviews that are part of
# shards following the first shard.
- Canvas.__init__(self)
+ super().__init__()
if canv is None:
self.shards = []
@@ -567,32 +607,29 @@ class CompositeCanvas(Canvas):
if hasattr(canv, "shards"):
self.shards = canv.shards
else:
- self.shards = [(canv.rows(), [
- (0, 0, canv.cols(), canv.rows(),
- None, canv)])]
+ self.shards = [(canv.rows(), [(0, 0, canv.cols(), canv.rows(), None, canv)])]
self.children = [(0, 0, canv, None)]
self.coords.update(canv.coords)
for shortcut in canv.shortcuts:
self.shortcuts[shortcut] = "wrap"
- def rows(self):
- for r,cv in self.shards:
+ def rows(self) -> int:
+ for r, cv in self.shards:
try:
assert isinstance(r, int)
except AssertionError:
raise AssertionError(r, cv)
- rows = sum([r for r,cv in self.shards])
+ rows = sum([r for r, cv in self.shards])
assert isinstance(rows, int)
return rows
- def cols(self):
+ def cols(self) -> int:
if not self.shards:
return 0
cols = sum([cv[2] for cv in self.shards[0][1]])
assert isinstance(cols, int)
return cols
-
def content(self):
"""
Return the canvas content as a list of rows where each row
@@ -604,14 +641,12 @@ class CompositeCanvas(Canvas):
sbody = shard_body(cviews, shard_tail)
# output rows
- for i in range(num_rows):
+ for _ in range(num_rows):
yield shard_body_row(sbody)
# prepare next shard tail
shard_tail = shard_body_tail(num_rows, sbody)
-
-
def content_delta(self, other):
"""
Return the differences between other and this canvas.
@@ -622,14 +657,13 @@ class CompositeCanvas(Canvas):
return
shard_tail = []
- for num_rows, cviews in shards_delta(
- self.shards, other.shards):
+ for num_rows, cviews in shards_delta(self.shards, other.shards):
# combine shard and shard tail
sbody = shard_body(cviews, shard_tail)
# output rows
row = []
- for i in range(num_rows):
+ for _ in range(num_rows):
# if whole shard is unchanged, don't keep
# calling shard_body_row
if len(row) != 1 or type(row[0]) != int:
@@ -639,16 +673,14 @@ class CompositeCanvas(Canvas):
# prepare next shard tail
shard_tail = shard_body_tail(num_rows, sbody)
-
- def trim(self, top, count=None):
+ def trim(self, top: int, count: int | None = None) -> None:
"""Trim lines from the top and/or bottom of canvas.
top -- number of lines to remove from top
count -- number of lines to keep, or None for all the rest
"""
- assert top >= 0, "invalid trim amount %d!"%top
- assert top < self.rows(), "cannot trim %d lines from %d!"%(
- top, self.rows())
+ assert top >= 0, f"invalid trim amount {top:d}!"
+ assert top < self.rows(), f"cannot trim {top:d} lines from {self.rows():d}!"
if self.widget_info:
raise self._finalized_error
@@ -662,22 +694,19 @@ class CompositeCanvas(Canvas):
self.coords = self.translate_coords(0, -top)
-
- def trim_end(self, end):
+ def trim_end(self, end: int) -> None:
"""Trim lines from the bottom of the canvas.
end -- number of lines to remove from the end
"""
- assert end > 0, "invalid trim amount %d!"%end
- assert end <= self.rows(), "cannot trim %d lines from %d!"%(
- end, self.rows())
+ assert end > 0, f"invalid trim amount {end:d}!"
+ assert end <= self.rows(), f"cannot trim {end:d} lines from {self.rows():d}!"
if self.widget_info:
raise self._finalized_error
self.shards = shards_trim_rows(self.shards, self.rows() - end)
-
- def pad_trim_left_right(self, left, right):
+ def pad_trim_left_right(self, left: int, right: int) -> None:
"""
Pad or trim this canvas on the left and right
@@ -696,22 +725,18 @@ class CompositeCanvas(Canvas):
if left > 0 or right > 0:
top_rows, top_cviews = shards[0]
if left > 0:
- new_top_cviews = (
- [(0,0,left,rows,None,blank_canvas)] +
- top_cviews)
+ new_top_cviews = ([(0, 0, left, rows, None, blank_canvas)] + top_cviews)
else:
- new_top_cviews = top_cviews[:] #copy
+ new_top_cviews = top_cviews[:] #copy
if right > 0:
- new_top_cviews.append(
- (0,0,right,rows,None,blank_canvas))
+ new_top_cviews.append((0, 0, right, rows, None, blank_canvas))
shards = [(top_rows, new_top_cviews)] + shards[1:]
self.coords = self.translate_coords(left, 0)
self.shards = shards
-
- def pad_trim_top_bottom(self, top, bottom):
+ def pad_trim_top_bottom(self, top: int, bottom: int) -> None:
"""
Pad or trim this canvas on the top and bottom.
"""
@@ -726,19 +751,15 @@ class CompositeCanvas(Canvas):
cols = self.cols()
if top > 0:
- self.shards = [(top,
- [(0,0,cols,top,None,blank_canvas)])] + \
- self.shards
+ self.shards = [(top, [(0, 0, cols, top, None, blank_canvas)])] + self.shards
self.coords = self.translate_coords(0, top)
if bottom > 0:
if orig_shards is self.shards:
self.shards = self.shards[:]
- self.shards.append((bottom,
- [(0,0,cols,bottom,None,blank_canvas)]))
-
+ self.shards.append((bottom, [(0, 0, cols, bottom, None, blank_canvas)]))
- def overlay(self, other, left, top ):
+ def overlay(self, other, left: int, top: int) -> None:
"""Overlay other onto this canvas."""
if self.widget_info:
raise self._finalized_error
@@ -767,14 +788,12 @@ class CompositeCanvas(Canvas):
if left > 0:
left_shards = [shards_trim_sides(side_shards, 0, left)]
if right > 0:
- right_shards = [shards_trim_sides(side_shards,
- max(0, left + width), right)]
+ right_shards = [shards_trim_sides(side_shards, max(0, left + width), right)]
if not self.rows():
middle_shards = []
elif left or right:
- middle_shards = shards_join(left_shards +
- [other.shards] + right_shards)
+ middle_shards = shards_join(left_shards + [other.shards] + right_shards)
else:
middle_shards = other.shards
@@ -782,15 +801,14 @@ class CompositeCanvas(Canvas):
self.coords.update(other.translate_coords(left, top))
-
- def fill_attr(self, a):
+ def fill_attr(self, a) -> None:
"""
Apply attribute a to all areas of this canvas with default
attribute currently set to None, leaving other attributes
intact."""
self.fill_attr_apply({None:a})
- def fill_attr_apply(self, mapping):
+ def fill_attr_apply(self, mapping) -> None:
"""
Apply an attribute-mapping dictionary to the canvas.
@@ -805,14 +823,11 @@ class CompositeCanvas(Canvas):
for cv in original_cviews:
# cv[4] == attr_map
if cv[4] is None:
- new_cviews.append(cv[:4] +
- (mapping,) + cv[5:])
+ new_cviews.append(cv[:4] + (mapping,) + cv[5:])
else:
combined = dict(mapping)
- combined.update([
- (k, mapping.get(v, v)) for k,v in cv[4].items()])
- new_cviews.append(cv[:4] +
- (combined,) + cv[5:])
+ combined.update([(k, mapping.get(v, v)) for k,v in cv[4].items()])
+ new_cviews.append(cv[:4] + (combined,) + cv[5:])
shards.append((num_rows, new_cviews))
self.shards = shards
@@ -848,7 +863,7 @@ def shard_body_row(sbody):
return row
-def shard_body_tail(num_rows, sbody):
+def shard_body_tail(num_rows: int, sbody):
"""
Return a new shard tail that follows this shard body.
"""
@@ -890,6 +905,7 @@ def shards_delta(shards, other_shards):
other_num_rows = None
done += num_rows
+
def shard_cviews_delta(cviews, other_cviews):
"""
"""
@@ -916,8 +932,7 @@ def shard_cviews_delta(cviews, other_cviews):
cols += cv[2]
-
-def shard_body(cviews, shard_tail, create_iter=True, iter_default=None):
+def shard_body(cviews, shard_tail, create_iter: bool = True, iter_default=None):
"""
Return a list of (done_rows, content_iter, cview) tuples for
this shard and shard tail.
@@ -930,7 +945,7 @@ def shard_body(cviews, shard_tail, create_iter=True, iter_default=None):
is created.
"""
col = 0
- body = [] # build the next shard tail
+ body = [] # build the next shard tail
cviews_iter = iter(cviews)
for col_gap, done_rows, content_iter, tail_cview in shard_tail:
while col_gap:
@@ -938,13 +953,11 @@ def shard_body(cviews, shard_tail, create_iter=True, iter_default=None):
cview = next(cviews_iter)
except StopIteration:
break
- (trim_left, trim_top, cols, rows, attr_map, canv) = \
- cview[:6]
+ (trim_left, trim_top, cols, rows, attr_map, canv) = cview[:6]
col += cols
col_gap -= cols
if col_gap < 0:
- raise CanvasError("cviews overflow gaps in"
- " shard_tail!")
+ raise CanvasError("cviews overflow gaps in shard_tail!")
if create_iter and canv:
new_iter = canv.content(trim_left, trim_top,
cols, rows, attr_map)
@@ -953,18 +966,16 @@ def shard_body(cviews, shard_tail, create_iter=True, iter_default=None):
body.append((0, new_iter, cview))
body.append((done_rows, content_iter, tail_cview))
for cview in cviews_iter:
- (trim_left, trim_top, cols, rows, attr_map, canv) = \
- cview[:6]
+ (trim_left, trim_top, cols, rows, attr_map, canv) = cview[:6]
if create_iter and canv:
- new_iter = canv.content(trim_left, trim_top, cols, rows,
- attr_map)
+ new_iter = canv.content(trim_left, trim_top, cols, rows, attr_map)
else:
new_iter = iter_default
body.append((0, new_iter, cview))
return body
-def shards_trim_top(shards, top):
+def shards_trim_top(shards, top: int):
"""
Return shards with top rows removed.
"""
@@ -987,19 +998,18 @@ def shards_trim_top(shards, top):
# trim the top of this shard
new_sbody = []
for done_rows, content_iter, cv in sbody:
- new_sbody.append((0, content_iter,
- cview_trim_top(cv, done_rows+top)))
+ new_sbody.append((0, content_iter, cview_trim_top(cv, done_rows + top)))
sbody = new_sbody
- new_shards = [(num_rows-top,
- [cv for done_rows, content_iter, cv in sbody])]
+ new_shards = [(num_rows - top, [cv for done_rows, content_iter, cv in sbody])]
# write out the rest of the shards
new_shards.extend(shard_iter)
return new_shards
-def shards_trim_rows(shards, keep_rows):
+
+def shards_trim_rows(shards, keep_rows: int):
"""
Return the topmost keep_rows rows from shards.
"""
@@ -1026,7 +1036,8 @@ def shards_trim_rows(shards, keep_rows):
return new_shards
-def shards_trim_sides(shards, left, cols):
+
+def shards_trim_sides(shards, left: int, cols: int):
"""
Return shards with starting from column left and cols total width.
"""
@@ -1059,6 +1070,7 @@ def shards_trim_sides(shards, left, cols):
new_shards.append((num_rows, new_cviews))
return new_shards
+
def shards_join(shard_lists):
"""
Return the result of joining shard lists horizontally.
@@ -1092,19 +1104,20 @@ def shards_join(shard_lists):
return new_shards
-def cview_trim_rows(cv, rows):
+def cview_trim_rows(cv, rows: int):
return cv[:3] + (rows,) + cv[4:]
-def cview_trim_top(cv, trim):
+
+def cview_trim_top(cv, trim: int):
return (cv[0], trim + cv[1], cv[2], cv[3] - trim) + cv[4:]
-def cview_trim_left(cv, trim):
- return (cv[0] + trim, cv[1], cv[2] - trim,) + cv[3:]
-def cview_trim_cols(cv, cols):
- return cv[:2] + (cols,) + cv[3:]
+def cview_trim_left(cv, trim: int):
+ return (cv[0] + trim, cv[1], cv[2] - trim,) + cv[3:]
+def cview_trim_cols(cv, cols: int):
+ return cv[:2] + (cols,) + cv[3:]
def CanvasCombine(l):
@@ -1119,7 +1132,7 @@ def CanvasCombine(l):
True if this canvas is the one that would be in focus
if the whole widget is in focus
"""
- clist = [(CompositeCanvas(c),p,f) for c,p,f in l]
+ clist = [(CompositeCanvas(c), p, f) for c, p, f in l]
combined_canvas = CompositeCanvas()
shards = []
@@ -1139,25 +1152,23 @@ def CanvasCombine(l):
n += 1
if focus_index:
- children = [children[focus_index]] + children[:focus_index] + \
- children[focus_index+1:]
+ children = [children[focus_index]] + children[:focus_index] + children[focus_index+1:]
combined_canvas.shards = shards
combined_canvas.children = children
return combined_canvas
-def CanvasOverlay(top_c, bottom_c, left, top):
+def CanvasOverlay(top_c, bottom_c, left: int, top: int):
"""
Overlay canvas top_c onto bottom_c at position (left, top).
"""
overlayed_canvas = CompositeCanvas(bottom_c)
overlayed_canvas.overlay(top_c, left, top)
- overlayed_canvas.children = [(left, top, top_c, None),
- (0, 0, bottom_c, None)]
+ overlayed_canvas.children = [(left, top, top_c, None), (0, 0, bottom_c, None)]
overlayed_canvas.shortcuts = {} # disable background shortcuts
for shortcut in top_c.shortcuts.keys():
- overlayed_canvas.shortcuts[shortcut]="fg"
+ overlayed_canvas.shortcuts[shortcut] = "fg"
return overlayed_canvas
@@ -1218,18 +1229,19 @@ def CanvasJoin(l):
return joined_canvas
-def apply_text_layout(text, attr, ls, maxcol):
+def apply_text_layout(text, attr, ls, maxcol: int):
t = []
a = []
c = []
class AttrWalk:
pass
+
aw = AttrWalk
- aw.k = 0 # counter for moving through elements of a
- aw.off = 0 # current offset into text of attr[ak]
+ aw.k = 0 # counter for moving through elements of a
+ aw.off = 0 # current offset into text of attr[ak]
- def arange( start_offs, end_offs ):
+ def arange(start_offs: int, end_offs: int):
"""Return an attribute list for the range of text specified."""
if start_offs < aw.off:
aw.k = 0
@@ -1237,7 +1249,7 @@ def apply_text_layout(text, attr, ls, maxcol):
o = []
# the loop should run at least once, the '=' part ensures that
while aw.off <= end_offs:
- if len(attr)<=aw.k:
+ if len(attr) <= aw.k:
# run out of attributes
o.append((None,end_offs-max(start_offs,aw.off)))
break
@@ -1255,26 +1267,25 @@ def apply_text_layout(text, attr, ls, maxcol):
aw.off += run
return o
-
for line_layout in ls:
# trim the line to fit within maxcol
- line_layout = trim_line( line_layout, text, 0, maxcol )
+ line_layout = trim_line(line_layout, text, 0, maxcol)
line = []
linea = []
linec = []
- def attrrange( start_offs, end_offs, destw ):
+ def attrrange(start_offs: int, end_offs: int, destw: int) -> None:
"""
Add attributes based on attributes between
start_offs and end_offs.
"""
if start_offs == end_offs:
- [(at,run)] = arange(start_offs,end_offs)
+ [(at,run)] = arange(start_offs, end_offs)
rle_append_modify( linea, ( at, destw ))
return
if destw == end_offs-start_offs:
- for at, run in arange(start_offs,end_offs):
+ for at, run in arange(start_offs, end_offs):
rle_append_modify( linea, ( at, run ))
return
# encoded version has different width
@@ -1291,7 +1302,6 @@ def apply_text_layout(text, attr, ls, maxcol):
o += run
destw -= segw
-
for seg in line_layout:
#if seg is None: assert 0, ls
s = LayoutSegment(seg)
diff --git a/urwid/container.py b/urwid/container.py
index 889c92e..376e8e5 100755
--- a/urwid/container.py
+++ b/urwid/container.py
@@ -22,15 +22,11 @@
from __future__ import annotations
+import typing
+from collections.abc import Iterable, Sequence
from itertools import chain, repeat
-from urwid.canvas import (
- CanvasCombine,
- CanvasJoin,
- CanvasOverlay,
- CompositeCanvas,
- SolidCanvas,
-)
+from urwid.canvas import CanvasCombine, CanvasJoin, CanvasOverlay, CompositeCanvas, SolidCanvas
from urwid.decoration import (
Filler,
Padding,
@@ -66,6 +62,9 @@ from urwid.widget import (
WidgetWrap,
)
+if typing.TYPE_CHECKING:
+ from typing_extensions import Literal
+
class WidgetContainerMixin:
"""
@@ -119,7 +118,7 @@ class WidgetContainerMixin:
w.focus_position = p # modifies w.focus
w = w.focus.base_widget
- def get_focus_widgets(self):
+ def get_focus_widgets(self) -> list[Widget]:
"""
Return the .focus values starting from this container
and proceeding along each child widget until reaching a leaf
@@ -137,6 +136,7 @@ class WidgetContainerMixin:
return out
out.append(w)
+
class WidgetContainerListContentsMixin:
"""
Mixin class for widget containers whose positions are indexes into
@@ -160,6 +160,7 @@ class WidgetContainerListContentsMixin:
class GridFlowError(Exception):
pass
+
class GridFlow(WidgetWrap, WidgetContainerMixin, WidgetContainerListContentsMixin):
"""
The GridFlow widget is a flow widget that renders all the widgets it
@@ -169,7 +170,14 @@ class GridFlow(WidgetWrap, WidgetContainerMixin, WidgetContainerListContentsMixi
def sizing(self):
return frozenset([FLOW])
- def __init__(self, cells, cell_width, h_sep, v_sep, align):
+ def __init__(
+ self,
+ cells: Sequence[Widget],
+ cell_width: int,
+ h_sep: int,
+ v_sep: int,
+ align: Literal['left', 'center', 'right'] | tuple[Literal['relative'], int],
+ ):
"""
:param cells: list of flow widgets to display
:param cell_width: column width for each cell
@@ -179,8 +187,7 @@ class GridFlow(WidgetWrap, WidgetContainerMixin, WidgetContainerListContentsMixi
:param align: horizontal alignment of cells, one of:
'left', 'center', 'right', ('relative', percentage 0=left 100=right)
"""
- self._contents = MonitoredFocusList([
- (w, (GIVEN, cell_width)) for w in cells])
+ self._contents = MonitoredFocusList([(w, (GIVEN, cell_width)) for w in cells])
self._contents.set_modified_callback(self._invalidate)
self._contents.set_focus_changed_callback(lambda f: self._invalidate())
self._contents.set_validate_contents_modified(self._contents_modified)
@@ -193,7 +200,7 @@ class GridFlow(WidgetWrap, WidgetContainerMixin, WidgetContainerListContentsMixi
# set self._w to something other than None
self.get_display_widget(((h_sep+cell_width)*len(cells),))
- def _invalidate(self):
+ def _invalidate(self) -> None:
self._cache_maxcol = None
super()._invalidate()
@@ -212,6 +219,7 @@ class GridFlow(WidgetWrap, WidgetContainerMixin, WidgetContainerListContentsMixi
self._set_cells(ml)
ml.set_modified_callback(user_modified)
return ml
+
def _set_cells(self, widgets):
focus_position = self.focus_position
self.contents = [
@@ -226,9 +234,10 @@ class GridFlow(WidgetWrap, WidgetContainerMixin, WidgetContainerListContentsMixi
contents.
""")
- def _get_cell_width(self):
+ def _get_cell_width(self) -> int:
return self._cell_width
- def _set_cell_width(self, width):
+
+ def _set_cell_width(self, width: int) -> None:
focus_position = self.focus_position
self.contents = [
(w, (GIVEN, width)) for (w, options) in self.contents]
@@ -241,6 +250,7 @@ class GridFlow(WidgetWrap, WidgetContainerMixin, WidgetContainerListContentsMixi
def _get_contents(self):
return self._contents
+
def _set_contents(self, c):
self._contents[:] = c
contents = property(_get_contents, _set_contents, doc="""
@@ -257,7 +267,11 @@ class GridFlow(WidgetWrap, WidgetContainerMixin, WidgetContainerListContentsMixi
.. seealso:: Create new options tuples with the :meth:`options` method.
""")
- def options(self, width_type=GIVEN, width_amount=None):
+ def options(
+ self,
+ width_type: Literal['given'] = GIVEN,
+ width_amount: int | None = None,
+ ) -> tuple[Literal['given'], int]:
"""
Return a new options tuple for use in a GridFlow's .contents list.
@@ -270,7 +284,7 @@ class GridFlow(WidgetWrap, WidgetContainerMixin, WidgetContainerListContentsMixi
width_amount = self._cell_width
return (width_type, width_amount)
- def set_focus(self, cell):
+ def set_focus(self, cell: Widget | int):
"""
Set the cell in focus, for backwards compatibility.
@@ -297,7 +311,7 @@ class GridFlow(WidgetWrap, WidgetContainerMixin, WidgetContainerListContentsMixi
focus = property(get_focus,
doc="the child widget in focus or None when GridFlow is empty")
- def _set_focus_cell(self, cell):
+ def _set_focus_cell(self, cell: Widget) -> None:
for i, (w, options) in enumerate(self.contents):
if cell == w:
self.focus_position = i
@@ -312,7 +326,7 @@ class GridFlow(WidgetWrap, WidgetContainerMixin, WidgetContainerListContentsMixi
index.
""")
- def _get_focus_position(self):
+ def _get_focus_position(self) -> int | None:
"""
Return the index of the widget in focus or None if this GridFlow is
empty.
@@ -320,7 +334,8 @@ class GridFlow(WidgetWrap, WidgetContainerMixin, WidgetContainerListContentsMixi
if not self.contents:
raise IndexError("No focus_position, GridFlow is empty")
return self.contents.focus
- def _set_focus_position(self, position):
+
+ def _set_focus_position(self, position: int) -> None:
"""
Set the widget in focus.
@@ -337,7 +352,7 @@ class GridFlow(WidgetWrap, WidgetContainerMixin, WidgetContainerListContentsMixi
GridFlow is empty, or when set to an invalid index.
""")
- def get_display_widget(self, size):
+ def get_display_widget(self, size: tuple[int]) -> Divider | Pile:
"""
Arrange the cells into columns (and possibly a pile) for
display, input or to calculate rows, and update the display
@@ -353,7 +368,7 @@ class GridFlow(WidgetWrap, WidgetContainerMixin, WidgetContainerListContentsMixi
return self._w
- def generate_display_widget(self, size):
+ def generate_display_widget(self, size: tuple[int]) -> Divider | Pile:
"""
Actually generate display widget (ignoring cache)
"""
@@ -407,7 +422,7 @@ class GridFlow(WidgetWrap, WidgetContainerMixin, WidgetContainerListContentsMixi
return p
- def _set_focus_from_display_widget(self):
+ def _set_focus_from_display_widget(self) -> None:
"""
Set the focus to the item in focus in the display widget.
"""
@@ -431,8 +446,7 @@ class GridFlow(WidgetWrap, WidgetContainerMixin, WidgetContainerListContentsMixi
# pad.first_position was set by generate_display_widget() above
self.focus_position = pile_focus.first_position + col_focus_position
-
- def keypress(self, size, key):
+ def keypress(self, size: tuple[int], key: str) -> str | None:
"""
Pass keypress to display widget for handling.
Captures focus changes.
@@ -443,42 +457,42 @@ class GridFlow(WidgetWrap, WidgetContainerMixin, WidgetContainerListContentsMixi
self._set_focus_from_display_widget()
return key
- def rows(self, size, focus=False):
+ def rows(self, size: tuple[int], focus: bool = False) -> int:
self.get_display_widget(size)
return super().rows(size, focus=focus)
- def render(self, size, focus=False ):
+ def render(self, size: tuple[int], focus: bool = False):
self.get_display_widget(size)
return super().render(size, focus)
- def get_cursor_coords(self, size):
+ def get_cursor_coords(self, size: tuple[int]) -> tuple[int, int]:
"""Get cursor from display widget."""
self.get_display_widget(size)
return super().get_cursor_coords(size)
- def move_cursor_to_coords(self, size, col, row):
+ def move_cursor_to_coords(self, size: tuple[int], col: int, row: int):
"""Set the widget in focus based on the col + row."""
self.get_display_widget(size)
rval = super().move_cursor_to_coords(size, col, row)
self._set_focus_from_display_widget()
return rval
- def mouse_event(self, size, event, button, col, row, focus):
+ def mouse_event(self, size: tuple[int], event, button: int, col: int, row: int, focus: bool) -> Literal[True]:
self.get_display_widget(size)
super().mouse_event(size, event, button, col, row, focus)
self._set_focus_from_display_widget()
- return True # at a minimum we adjusted our focus
+ return True # at a minimum we adjusted our focus
- def get_pref_col(self, size):
+ def get_pref_col(self, size: tuple[int]):
"""Return pref col from display widget."""
self.get_display_widget(size)
return super().get_pref_col(size)
-
class OverlayError(Exception):
pass
+
class Overlay(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
"""
Overlay contains two box widgets and renders one on top of the other
@@ -490,8 +504,21 @@ class Overlay(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
LEFT, None, RELATIVE, 100, None, 0, 0,
TOP, None, RELATIVE, 100, None, 0, 0)
- def __init__(self, top_w, bottom_w, align, width, valign, height,
- min_width=None, min_height=None, left=0, right=0, top=0, bottom=0):
+ def __init__(
+ self,
+ top_w: Widget,
+ bottom_w: Widget,
+ align: Literal['left', 'center', 'right'] | tuple[Literal['relative'], int],
+ width: Literal['pack'] | int | tuple[Literal['relative'], int],
+ valign: Literal['top', 'middle', 'bottom'] | tuple[Literal['relative'], int],
+ height: Literal['pack'] | int | tuple[Literal['relative'], int],
+ min_width: int | None = None,
+ min_height: int | None = None,
+ left: int = 0,
+ right: int = 0,
+ top: int = 0,
+ bottom: int = 0,
+ ) -> None:
"""
:param top_w: a flow, box or fixed widget to overlay "on top"
:type top_w: Widget
@@ -547,9 +574,22 @@ class Overlay(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
min_width, min_height, left, right, top, bottom)
@staticmethod
- def options(align_type, align_amount, width_type, width_amount,
- valign_type, valign_amount, height_type, height_amount,
- min_width=None, min_height=None, left=0, right=0, top=0, bottom=0):
+ def options(
+ align_type: Literal['left', 'center', 'right', 'relative'],
+ align_amount: int | None,
+ width_type,
+ width_amount,
+ valign_type: Literal['top', 'middle', 'bottom', 'relative'],
+ valign_amount: int | None,
+ height_type,
+ height_amount,
+ min_width: int | None = None,
+ min_height: int | None = None,
+ left: int = 0,
+ right: int = 0,
+ top: int = 0,
+ bottom: int = 0,
+ ):
"""
Return a new options tuple for use in this Overlay's .contents mapping.
@@ -563,8 +603,19 @@ class Overlay(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
min_width, left, right, valign_type, valign_amount,
height_type, height_amount, min_height, top, bottom)
- def set_overlay_parameters(self, align, width, valign, height,
- min_width=None, min_height=None, left=0, right=0, top=0, bottom=0):
+ def set_overlay_parameters(
+ self,
+ align: Literal['left', 'center', 'right'] | tuple[Literal['relative'], int],
+ width: int | None,
+ valign: Literal['top', 'middle', 'bottom'] | tuple[Literal['relative'], int],
+ height: int | None,
+ min_width: int | None = None,
+ min_height: int | None = None,
+ left: int = 0,
+ right: int = 0,
+ top: int = 0,
+ bottom: int = 0,
+ ):
"""
Adjust the overlay size and position parameters.
@@ -601,7 +652,7 @@ class Overlay(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
top = height[1]
height = RELATIVE_100
- if width is None: # more obsolete values accepted
+ if width is None: # more obsolete values accepted
width = PACK
if height is None:
height = PACK
@@ -620,16 +671,15 @@ class Overlay(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
valign_type, valign_amount, height_type, height_amount,
min_width, min_height, left, right, top, bottom))
- def selectable(self):
+ def selectable(self) -> bool:
"""Return selectable from top_w."""
return self.top_w.selectable()
- def keypress(self, size, key):
+ def keypress(self, size: tuple[int, int], key: str) -> str | None:
"""Pass keypress to top_w."""
- return self.top_w.keypress(self.top_w_size(size,
- *self.calculate_padding_filler(size, True)), key)
+ return self.top_w.keypress(self.top_w_size(size, *self.calculate_padding_filler(size, True)), key)
- def _get_focus(self):
+ def _get_focus(self) -> Widget:
"""
Currently self.top_w is always the focus of an Overlay
"""
@@ -637,11 +687,12 @@ class Overlay(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
focus = property(_get_focus,
doc="the top widget in this overlay is always in focus")
- def _get_focus_position(self):
+ def _get_focus_position(self) -> Literal[1]:
"""
Return the top widget position (currently always 1).
"""
return 1
+
def _set_focus_position(self, position):
"""
Set the widget in focus. Currently only position 0 is accepted.
@@ -649,8 +700,7 @@ class Overlay(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
position -- index of child widget to be made focus
"""
if position != 1:
- raise IndexError("Overlay widget focus_position currently "
- "must always be set to 1, not %s" % (position,))
+ raise IndexError(f"Overlay widget focus_position currently must always be set to 1, not {position}")
focus_position = property(_get_focus_position, _set_focus_position,
doc="index of child widget in focus, currently always 1")
@@ -661,7 +711,8 @@ class Overlay(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
__getitem__ = self._contents__getitem__
__setitem__ = self._contents__setitem__
return OverlayContents()
- def _contents__getitem__(self, index):
+
+ def _contents__getitem__(self, index: Literal[0, 1]):
if index == 0:
return (self.bottom_w, self._DEFAULT_BOTTOM_OPTIONS)
if index == 1:
@@ -673,15 +724,15 @@ class Overlay(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
self.height_type, self.height_amount,
self.min_height, self.top, self.bottom))
raise IndexError(f"Overlay.contents has no position {index!r}")
- def _contents__setitem__(self, index, value):
+
+ def _contents__setitem__(self, index: Literal[0, 1], value):
try:
value_w, value_options = value
except (ValueError, TypeError):
raise OverlayError(f"added content invalid: {value!r}")
if index == 0:
if value_options != self._DEFAULT_BOTTOM_OPTIONS:
- raise OverlayError("bottom_options must be set to "
- "%r" % (self._DEFAULT_BOTTOM_OPTIONS,))
+ raise OverlayError(f"bottom_options must be set to {self._DEFAULT_BOTTOM_OPTIONS!r}")
self.bottom_w = value_w
elif index == 1:
try:
@@ -739,20 +790,19 @@ class Overlay(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
:exc:`OverlayError`.
""")
- def get_cursor_coords(self, size):
+ def get_cursor_coords(self, size: tuple[int, int]) -> tuple[int, int] | None:
"""Return cursor coords from top_w, if any."""
if not hasattr(self.top_w, 'get_cursor_coords'):
return None
(maxcol, maxrow) = size
- left, right, top, bottom = self.calculate_padding_filler(size,
- True)
+ left, right, top, bottom = self.calculate_padding_filler(size, True)
x, y = self.top_w.get_cursor_coords(
(maxcol-left-right, maxrow-top-bottom) )
if y >= maxrow: # required??
y = maxrow-1
return x+left, y+top
- def calculate_padding_filler(self, size, focus):
+ def calculate_padding_filler(self, size: tuple[int, int], focus: bool) -> tuple[int, int, int, int]:
"""Return (padding left, right, filler top, bottom)."""
(maxcol, maxrow) = size
height = None
@@ -779,7 +829,7 @@ class Overlay(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
elif self.height_type == PACK:
# top_w is a flow widget
height = self.top_w.rows((maxcol,),focus=focus)
- top, bottom = calculate_top_bottom_filler(maxrow,
+ top, bottom = calculate_top_bottom_filler(maxrow,
self.valign_type, self.valign_amount,
GIVEN, height, None, self.top, self.bottom)
if height > maxrow: # flow widget rendered too large
@@ -802,11 +852,9 @@ class Overlay(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
return (maxcol-left-right,)
return (maxcol-left-right, maxrow-top-bottom)
-
- def render(self, size, focus=False):
+ def render(self, size: tuple[int, int], focus: bool = False) -> CompositeCanvas:
"""Render top_w overlayed on bottom_w."""
- left, right, top, bottom = self.calculate_padding_filler(size,
- focus)
+ left, right, top, bottom = self.calculate_padding_filler(size, focus)
bottom_c = self.bottom_w.render(size)
if not bottom_c.cols() or not bottom_c.rows():
return CompositeCanvas(bottom_c)
@@ -821,17 +869,14 @@ class Overlay(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
return CanvasOverlay(top_c, bottom_c, left, top)
-
- def mouse_event(self, size, event, button, col, row, focus):
+ def mouse_event(self, size: tuple[int, int], event, button: int, col: int, row: int, focus: bool) -> bool | None:
"""Pass event to top_w, ignore if outside of top_w."""
if not hasattr(self.top_w, 'mouse_event'):
return False
- left, right, top, bottom = self.calculate_padding_filler(size,
- focus)
+ left, right, top, bottom = self.calculate_padding_filler(size, focus)
maxcol, maxrow = size
- if ( col<left or col>=maxcol-right or
- row<top or row>=maxrow-bottom ):
+ if col<left or col>=maxcol-right or row<top or row>=maxrow-bottom:
return False
return self.top_w.mouse_event(
@@ -842,6 +887,7 @@ class Overlay(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
class FrameError(Exception):
pass
+
class Frame(Widget, WidgetContainerMixin):
"""
Frame widget is a box widget with optional header and footer
@@ -856,7 +902,13 @@ class Frame(Widget, WidgetContainerMixin):
_selectable = True
_sizing = frozenset([BOX])
- def __init__(self, body, header=None, footer=None, focus_part='body'):
+ def __init__(
+ self,
+ body: Widget,
+ header: Widget | None = None,
+ footer: Widget | None = None,
+ focus_part: Literal['header', 'footer', 'body'] = 'body',
+ ):
"""
:param body: a box widget for the body of the frame
:type body: Widget
@@ -874,32 +926,34 @@ class Frame(Widget, WidgetContainerMixin):
self._footer = footer
self.focus_part = focus_part
- def get_header(self):
+ def get_header(self) -> Widget | None:
return self._header
- def set_header(self, header):
+
+ def set_header(self, header: Widget | None):
self._header = header
if header is None and self.focus_part == 'header':
self.focus_part = 'body'
self._invalidate()
header = property(get_header, set_header)
- def get_body(self):
+ def get_body(self) -> Widget:
return self._body
- def set_body(self, body):
+ def set_body(self, body: Widget) -> None:
self._body = body
self._invalidate()
body = property(get_body, set_body)
- def get_footer(self):
+ def get_footer(self) -> Widget | None:
return self._footer
- def set_footer(self, footer):
+
+ def set_footer(self, footer: Widget | None) -> None:
self._footer = footer
if footer is None and self.focus_part == 'footer':
self.focus_part = 'body'
self._invalidate()
footer = property(get_footer, set_footer)
- def set_focus(self, part):
+ def set_focus(self, part: Literal['header', 'footer', 'body']) -> None:
"""
Determine which part of the frame is in focus.
@@ -917,7 +971,7 @@ class Frame(Widget, WidgetContainerMixin):
self.focus_part = part
self._invalidate()
- def get_focus(self):
+ def get_focus(self) -> Literal['header', 'footer', 'body']:
"""
Return an indicator which part of the frame is in focus
@@ -929,7 +983,7 @@ class Frame(Widget, WidgetContainerMixin):
"""
return self.focus_part
- def _get_focus(self):
+ def _get_focus(self) -> Widget:
return {
'header': self._header,
'footer': self._footer,
@@ -948,10 +1002,13 @@ class Frame(Widget, WidgetContainerMixin):
class FrameContents:
def __len__(inner_self):
return len(inner_self.keys())
+
def items(inner_self):
return [(k, inner_self[k]) for k in inner_self.keys()]
+
def values(inner_self):
return [inner_self[k] for k in inner_self.keys()]
+
def update(inner_self, E=None, **F):
if E:
keys = getattr(E, 'keys', None)
@@ -968,14 +1025,16 @@ class Frame(Widget, WidgetContainerMixin):
__setitem__ = self._contents__setitem__
__delitem__ = self._contents__delitem__
return FrameContents()
- def _contents_keys(self):
+
+ def _contents_keys(self) -> list[Literal['header', 'footer', 'body']]:
keys = ['body']
if self._header:
keys.append('header')
if self._footer:
keys.append('footer')
return keys
- def _contents__getitem__(self, key):
+
+ def _contents__getitem__(self, key: Literal['header', 'footer', 'body']):
if key == 'body':
return (self._body, None)
if key == 'header' and self._header:
@@ -983,7 +1042,8 @@ class Frame(Widget, WidgetContainerMixin):
if key == 'footer' and self._footer:
return (self._footer, None)
raise KeyError(f"Frame.contents has no key: {key!r}")
- def _contents__setitem__(self, key, value):
+
+ def _contents__setitem__(self, key: Literal['header', 'footer', 'body'], value):
if key not in ('body', 'header', 'footer'):
raise KeyError(f"Frame.contents has no key: {key!r}")
try:
@@ -998,7 +1058,8 @@ class Frame(Widget, WidgetContainerMixin):
self.footer = value_w
else:
self.header = value_w
- def _contents__delitem__(self, key):
+
+ def _contents__delitem__(self, key: Literal['header', 'footer', 'body']):
if key not in ('header', 'footer'):
raise KeyError(f"Frame.contents can't remove key: {key!r}")
if (key == 'header' and self._header is None
@@ -1029,7 +1090,7 @@ class Frame(Widget, WidgetContainerMixin):
compatibility.
""")
- def options(self):
+ def options(self) -> None:
"""
There are currently no options for Frame contents.
@@ -1037,7 +1098,7 @@ class Frame(Widget, WidgetContainerMixin):
"""
return None
- def frame_top_bottom(self, size, focus):
+ def frame_top_bottom(self, size: tuple[int, int], focus: bool) -> tuple[tuple[int, int], tuple[int, int]]:
"""
Calculate the number of rows for the header and footer.
@@ -1090,11 +1151,9 @@ class Frame(Widget, WidgetContainerMixin):
return (hrows, frows),(hrows, frows)
-
- def render(self, size, focus=False):
+ def render(self, size: tuple[int, int], focus: bool = False) -> CompositeCanvas:
(maxcol, maxrow) = size
- (htrim, ftrim),(hrows, frows) = self.frame_top_bottom(
- (maxcol, maxrow), focus)
+ (htrim, ftrim),(hrows, frows) = self.frame_top_bottom((maxcol, maxrow), focus)
combinelist = []
depends_on = []
@@ -1136,8 +1195,7 @@ class Frame(Widget, WidgetContainerMixin):
return CanvasCombine(combinelist)
-
- def keypress(self, size, key):
+ def keypress(self, size: tuple[int, int], key: str) -> str | None:
"""Pass keypress to widget in focus."""
(maxcol, maxrow) = size
@@ -1162,15 +1220,13 @@ class Frame(Widget, WidgetContainerMixin):
return key
return self.body.keypress( (maxcol, remaining), key )
-
- def mouse_event(self, size, event, button, col, row, focus):
+ def mouse_event(self, size: tuple[str, str], event, button: int, col: int, row: int, focus: bool) -> bool | None:
"""
Pass mouse event to appropriate part of frame.
Focus may be changed on button 1 press.
"""
(maxcol, maxrow) = size
- (htrim, ftrim),(hrows, frows) = self.frame_top_bottom(
- (maxcol, maxrow), focus)
+ (htrim, ftrim),(hrows, frows) = self.frame_top_bottom((maxcol, maxrow), focus)
if row < htrim: # within header
focus = focus and self.focus_part == 'header'
@@ -1203,7 +1259,7 @@ class Frame(Widget, WidgetContainerMixin):
return self.body.mouse_event( (maxcol, maxrow-htrim-ftrim),
event, button, col, row-htrim, focus )
- def get_cursor_coords(self, size):
+ def get_cursor_coords(self, size: tuple[int, int]) -> tuple[int, int] | None:
"""Return the cursor coordinates of the focus widget."""
if not self.focus.selectable():
return None
@@ -1254,13 +1310,14 @@ class Frame(Widget, WidgetContainerMixin):
class PileError(Exception):
pass
+
class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
"""
A pile of widgets stacked vertically from top to bottom
"""
_sizing = frozenset([FLOW, BOX])
- def __init__(self, widget_list, focus_item=None):
+ def __init__(self, widget_list: Iterable[Widget], focus_item: Widget | int | None = None) -> None:
"""
:param widget_list: child widgets
:type widget_list: iterable
@@ -1293,7 +1350,6 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
self._contents.set_focus_changed_callback(lambda f: self._invalidate())
self._contents.set_validate_contents_modified(self._validate_contents_modified)
- focus_item = focus_item
for i, original in enumerate(widget_list):
w = original
if not isinstance(w, tuple):
@@ -1321,7 +1377,7 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
self.pref_col = 0
- def _contents_modified(self):
+ def _contents_modified(self) -> None:
"""
Recalculate whether this widget should be selectable whenever the
contents has been changed.
@@ -1340,10 +1396,13 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
def _get_widget_list(self):
ml = MonitoredList(w for w, t in self.contents)
+
def user_modified():
self._set_widget_list(ml)
+
ml.set_modified_callback(user_modified)
return ml
+
def _set_widget_list(self, widgets):
focus_position = self.focus_position
self.contents = [
@@ -1364,10 +1423,12 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
# return the old item type names
({GIVEN: FIXED, PACK: FLOW}.get(f, f), height)
for w, (f, height) in self.contents)
+
def user_modified():
self._set_item_types(ml)
ml.set_modified_callback(user_modified)
return ml
+
def _set_item_types(self, item_types):
focus_position = self.focus_position
self.contents = [
@@ -1415,7 +1476,10 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
""")
@staticmethod
- def options(height_type=WEIGHT, height_amount=1):
+ def options(
+ height_type: Literal['pack', 'given', 'weight'] = WEIGHT,
+ height_amount: int | None = 1,
+ ) -> tuple[Literal['pack'], None] | tuple[Literal['given', 'weight'], int]:
"""
Return a new options tuple for use in a Pile's :attr:`contents` list.
@@ -1430,7 +1494,7 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
raise PileError(f'invalid height_type: {height_type!r}')
return (height_type, height_amount)
- def set_focus(self, item):
+ def set_focus(self, item: Widget | int) -> None:
"""
Set the item in focus, for backwards compatibility.
@@ -1449,7 +1513,7 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
return
raise ValueError(f"Widget not found in Pile contents: {item!r}")
- def get_focus(self):
+ def get_focus(self) -> Widget | None:
"""
Return the widget in focus, for backwards compatibility. You may
also use the new standard container property .focus to get the
@@ -1472,7 +1536,7 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
focus position.
""")
- def _get_focus_position(self):
+ def _get_focus_position(self) -> int:
"""
Return the index of the widget in focus or None if this Pile is
empty.
@@ -1480,7 +1544,7 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
if not self.contents:
raise IndexError("No focus_position, Pile is empty")
return self.contents.focus
- def _set_focus_position(self, position):
+ def _set_focus_position(self, position: int) -> None:
"""
Set the widget in focus.
@@ -1504,7 +1568,13 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
self._update_pref_col_from_focus(size)
return self.pref_col
- def get_item_size(self, size, i, focus, item_rows=None):
+ def get_item_size(
+ self,
+ size: tuple[int] | tuple[int, int],
+ i: int,
+ focus: bool,
+ item_rows: list[int] | None = None,
+ ) -> tuple[int] | tuple[int, int]:
"""
Return a size appropriate for passing to self.contents[i][0].render
"""
@@ -1519,7 +1589,7 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
else:
return (maxcol,)
- def get_item_rows(self, size, focus):
+ def get_item_rows(self, size: tuple[int] | tuple[int, int], focus: bool) -> list[int]:
"""
Return a list of the number of rows used by each widget
in self.contents
@@ -1537,8 +1607,7 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
if f == GIVEN:
l.append(height)
else:
- l.append(w.rows((maxcol,),
- focus=focus and self.focus_item == w))
+ l.append(w.rows((maxcol,), focus=focus and self.focus_item == w))
return l
# pile is a box widget
@@ -1556,7 +1625,7 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
l.append(None)
wtotal += height
else:
- l.append(0) # zero-weighted items treated as ('given', 0)
+ l.append(0) # zero-weighted items treated as ('given', 0)
if wtotal == 0:
raise PileError("No weighted widgets found for Pile treated as a box widget")
@@ -1603,7 +1672,7 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
out.pad_trim_top_bottom(0, size[1] - out.rows())
return out
- def get_cursor_coords(self, size):
+ def get_cursor_coords(self, size: tuple[int] | tuple[int, int]) -> tuple[int, int] | None:
"""Return the cursor coordinates of the focus widget."""
if not self.selectable():
return None
@@ -1635,10 +1704,10 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
y += r
return x, y
- def rows(self, size, focus=False ):
+ def rows(self, size: tuple[int] | tuple[int, int], focus: bool = False) -> int:
return sum(self.get_item_rows(size, focus))
- def keypress(self, size, key ):
+ def keypress(self, size: tuple[int] | tuple[int, int], key: str) -> str | None:
"""Pass the keypress to the widget in focus.
Unhandled 'up' and 'down' keys may cause a focus change."""
if not self.contents:
@@ -1687,7 +1756,7 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
# nothing to select
return key
- def _update_pref_col_from_focus(self, size):
+ def _update_pref_col_from_focus(self, size: tuple[int] | tuple[int, int]) -> None:
"""Update self.pref_col from the focus widget."""
if not hasattr(self.focus, 'get_pref_col'):
@@ -1698,16 +1767,15 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
if pref_col is not None:
self.pref_col = pref_col
- def move_cursor_to_coords(self, size, col, row):
+ def move_cursor_to_coords(self, size: tuple[int] | tuple[int, int], col: int, row: int) -> bool:
"""Capture pref col and set new focus."""
self.pref_col = col
#FIXME guessing focus==True
- focus=True
+ focus = True
wrow = 0
item_rows = self.get_item_rows(size, focus)
- for i, (r, w) in enumerate(zip(item_rows,
- (w for (w, options) in self.contents))):
+ for i, (r, w) in enumerate(zip(item_rows, (w for (w, options) in self.contents))):
if wrow + r > row:
break
wrow += r
@@ -1726,7 +1794,15 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
self.focus_position = i
return True
- def mouse_event(self, size, event, button, col, row, focus):
+ def mouse_event(
+ self,
+ size: tuple[int] | tuple[int, int],
+ event,
+ button: int,
+ col: int,
+ row: int,
+ focus: bool,
+ ) -> bool | None:
"""
Pass the event to the contained widget.
May change focus on button 1 press.
@@ -1750,9 +1826,7 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
return False
tsize = self.get_item_size(size, i, focus, item_rows)
- return w.mouse_event(tsize, event, button, col, row-wrow,
- focus)
-
+ return w.mouse_event(tsize, event, button, col, row-wrow, focus)
class ColumnsError(Exception):
@@ -1765,8 +1839,14 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
"""
_sizing = frozenset([FLOW, BOX])
- def __init__(self, widget_list, dividechars=0, focus_column=None,
- min_width=1, box_columns=None):
+ def __init__(
+ self,
+ widget_list: Iterable[Widget],
+ dividechars: int = 0,
+ focus_column: int | None = None,
+ min_width: int = 1,
+ box_columns: Iterable[int] | None = None,
+ ):
"""
:param widget_list: iterable of flow or box widgets
:param dividechars: number of blank characters between columns
@@ -1841,7 +1921,7 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
self.min_width = min_width
self._cache_maxcol = None
- def _contents_modified(self):
+ def _contents_modified(self) -> None:
"""
Recalculate whether this widget should be selectable whenever the
contents has been changed.
@@ -1849,7 +1929,7 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
self._selectable = any(w.selectable() for w, o in self.contents)
self._invalidate()
- def _validate_contents_modified(self, slc, new_items):
+ def _validate_contents_modified(self, slc, new_items) -> None:
for item in new_items:
try:
w, (t, n, b) = item
@@ -1858,18 +1938,22 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
except (TypeError, ValueError):
raise ColumnsError(f"added content invalid {item!r}")
- def _get_widget_list(self):
+ def _get_widget_list(self) -> MonitoredList:
ml = MonitoredList(w for w, t in self.contents)
def user_modified():
self._set_widget_list(ml)
ml.set_modified_callback(user_modified)
return ml
+
def _set_widget_list(self, widgets):
focus_position = self.focus_position
self.contents = [
- (new, options) for (new, (w, options)) in zip(widgets,
# need to grow contents list if widgets is longer
- chain(self.contents, repeat((None, (WEIGHT, 1, False)))))]
+ (new, options) for (new, (w, options)) in zip(
+ widgets,
+ chain(self.contents, repeat((None, (WEIGHT, 1, False))))
+ )
+ ]
if focus_position < len(widgets):
self.focus_position = focus_position
widget_list = property(_get_widget_list, _set_widget_list, doc="""
@@ -1879,7 +1963,7 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
standard container property :attr:`contents`.
""")
- def _get_column_types(self):
+ def _get_column_types(self) -> MonitoredList:
ml = MonitoredList(
# return the old column type names
({GIVEN: FIXED, PACK: FLOW}.get(t, t), n)
@@ -1888,12 +1972,14 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
self._set_column_types(ml)
ml.set_modified_callback(user_modified)
return ml
+
def _set_column_types(self, column_types):
focus_position = self.focus_position
self.contents = [
(w, ({FIXED: GIVEN, FLOW: PACK}.get(new_t, new_t), new_n, b))
for ((new_t, new_n), (w, (t, n, b)))
- in zip(column_types, self.contents)]
+ in zip(column_types, self.contents)
+ ]
if focus_position < len(column_types):
self.focus_position = focus_position
column_types = property(_get_column_types, _set_column_types, doc="""
@@ -1902,13 +1988,14 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
container property .contents to modify Pile contents.
""")
- def _get_box_columns(self):
+ def _get_box_columns(self) -> MonitoredList:
ml = MonitoredList(
i for i, (w, (t, n, b)) in enumerate(self.contents) if b)
def user_modified():
self._set_box_columns(ml)
ml.set_modified_callback(user_modified)
return ml
+
def _set_box_columns(self, box_columns):
box_columns = set(box_columns)
self.contents = [
@@ -1922,21 +2009,21 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
standard container property :attr:`contents`.
""")
- def _get_has_pack_type(self):
+ def _get_has_pack_type(self) -> bool:
import warnings
- warnings.warn(".has_flow_type is deprecated, "
- "read values from .contents instead.", DeprecationWarning)
+ warnings.warn(".has_flow_type is deprecated, read values from .contents instead.", DeprecationWarning)
return PACK in self.column_types
+
def _set_has_pack_type(self, value):
import warnings
- warnings.warn(".has_flow_type is deprecated, "
- "read values from .contents instead.", DeprecationWarning)
+ warnings.warn(".has_flow_type is deprecated, read values from .contents instead.", DeprecationWarning)
has_flow_type = property(_get_has_pack_type, _set_has_pack_type, doc="""
.. deprecated:: 1.0 Read values from :attr:`contents` instead.
""")
def _get_contents(self):
return self._contents
+
def _set_contents(self, c):
self._contents[:] = c
contents = property(_get_contents, _set_contents, doc="""
@@ -1948,7 +2035,11 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
""")
@staticmethod
- def options(width_type=WEIGHT, width_amount=1, box_widget=False):
+ def options(
+ width_type: Literal['pack', 'given', 'weight'] = WEIGHT,
+ width_amount: int | None = 1,
+ box_widget: bool = False,
+ ) -> tuple[Literal['pack'], None, bool] | tuple[Literal['given', 'weight'], int, bool]:
"""
Return a new options tuple for use in a Pile's .contents list.
@@ -1976,11 +2067,11 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
raise ColumnsError(f'invalid width_type: {width_type!r}')
return (width_type, width_amount, box_widget)
- def _invalidate(self):
+ def _invalidate(self) -> None:
self._cache_maxcol = None
super()._invalidate()
- def set_focus_column(self, num):
+ def set_focus_column(self, num: int) -> None:
"""
Set the column in focus by its index in :attr:`widget_list`.
@@ -1992,7 +2083,7 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
"""
self._set_focus_position(num)
- def get_focus_column(self):
+ def get_focus_column(self) -> int:
"""
Return the focus column index.
@@ -2001,7 +2092,7 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
"""
return self.focus_position
- def set_focus(self, item):
+ def set_focus(self, item: Widget | int) -> None:
"""
Set the item in focus
@@ -2017,7 +2108,7 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
return
raise ValueError(f"Widget not found in Columns contents: {item!r}")
- def get_focus(self):
+ def get_focus(self) -> Widget | None:
"""
Return the widget in focus, for backwards compatibility. You may
also use the new standard container property .focus to get the
@@ -2029,7 +2120,7 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
focus = property(get_focus,
doc="the child widget in focus or None when Columns is empty")
- def _get_focus_position(self):
+ def _get_focus_position(self) -> int | None:
"""
Return the index of the widget in focus or None if this Columns is
empty.
@@ -2037,7 +2128,8 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
if not self.widget_list:
raise IndexError("No focus_position, Columns is empty")
return self.contents.focus
- def _set_focus_position(self, position):
+
+ def _set_focus_position(self, position: int) -> None:
"""
Set the widget in focus.
@@ -2062,7 +2154,7 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
standard container property :attr:`focus_position` to get the focus.
""")
- def column_widths(self, size, focus=False):
+ def column_widths(self, size: tuple[int] | tuple[int, int], focus: bool = False) -> list[int]:
"""
Return a list of column widths.
@@ -2123,7 +2215,7 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
self._cache_column_widths = widths
return widths
- def render(self, size, focus=False):
+ def render(self, size: tuple[int] | tuple[int, int], focus: bool = False) -> SolidCanvas | CompositeCanvas:
"""
Render columns and return canvas.
@@ -2155,8 +2247,7 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
else:
sub_size = (mc,) + size[1:]
- canv = w.render(sub_size,
- focus = focus and self.focus_position == i)
+ canv = w.render(sub_size, focus=focus and self.focus_position == i)
if i < len(widths) - 1:
mc += self.dividechars
@@ -2195,7 +2286,7 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
for wc in widths[:self.focus_position] if wc > 0])
return x, y
- def move_cursor_to_coords(self, size, col, row):
+ def move_cursor_to_coords(self, size: tuple[int] | tuple[int, int], col: int, row: int) -> bool:
"""
Choose a selectable column to focus based on the coords.
@@ -2242,7 +2333,15 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
self.pref_col = col
return True
- def mouse_event(self, size, event, button, col, row, focus):
+ def mouse_event(
+ self,
+ size: tuple[int] | tuple[int, int],
+ event,
+ button: int,
+ col: int,
+ row: int,
+ focus: bool,
+ ) -> bool | None:
"""
Send event to appropriate column.
May change focus on button 1 press.
@@ -2261,21 +2360,18 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
continue
focus = focus and self.focus_col == i
- if is_mouse_press(event) and button == 1:
- if w.selectable():
- self.focus_position = i
+ if is_mouse_press(event) and button == 1 and w.selectable():
+ self.focus_position = i
if not hasattr(w, 'mouse_event'):
return False
if len(size) == 1 and b:
- return w.mouse_event((end - x, self.rows(size)), event, button,
- col - x, row, focus)
- return w.mouse_event((end - x,) + size[1:], event, button,
- col - x, row, focus)
+ return w.mouse_event((end - x, self.rows(size)), event, button, col - x, row, focus)
+ return w.mouse_event((end - x,) + size[1:], event, button, col - x, row, focus)
return False
- def get_pref_col(self, size):
+ def get_pref_col(self, size: tuple[int] | tuple[int, int]) -> int:
"""Return the pref col from the column in focus."""
widths = self.column_widths(size)
@@ -2300,7 +2396,7 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
col += sum(widths[:self.focus_position] )
return col
- def rows(self, size, focus=False):
+ def rows(self, size: tuple[int] | tuple[int, int], focus: bool = False) -> int:
"""
Return the number of rows required by the columns.
This only makes sense if :attr:`widget_list` contains flow widgets.
@@ -2313,11 +2409,10 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
for i, (mc, (w, (t, n, b))) in enumerate(zip(widths, self.contents)):
if b:
continue
- rows = max(rows,
- w.rows((mc,), focus=focus and self.focus_position == i))
+ rows = max(rows, w.rows((mc,), focus=focus and self.focus_position == i))
return rows
- def keypress(self, size, key):
+ def keypress(self, size: tuple[int] | tuple[int, int], key: str) -> str | None:
"""
Pass keypress to the focus column.
@@ -2360,13 +2455,10 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
return key
-
-
-
-
def _test():
import doctest
doctest.testmod()
+
if __name__=='__main__':
_test()
diff --git a/urwid/curses_display.py b/urwid/curses_display.py
index 3a79ec7..bc13125 100755
--- a/urwid/curses_display.py
+++ b/urwid/curses_display.py
@@ -31,12 +31,7 @@ import curses
import _curses
from urwid import escape
-from urwid.display_common import (
- UNPRINTABLE_TRANS_TABLE,
- AttrSpec,
- BaseScreen,
- RealTerminal,
-)
+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
@@ -80,7 +75,7 @@ class Screen(BaseScreen, RealTerminal):
self.register_palette_entry(None, 'default','default')
- def set_mouse_tracking(self, enable=True):
+ def set_mouse_tracking(self, enable: bool = True) -> None:
"""
Enable mouse tracking.
@@ -108,7 +103,7 @@ class Screen(BaseScreen, RealTerminal):
self._mouse_tracking_enabled = enable
- def _start(self):
+ def _start(self) -> None:
"""
Initialize the screen and input mode.
"""
@@ -136,7 +131,7 @@ class Screen(BaseScreen, RealTerminal):
super()._start()
- def _stop(self):
+ def _stop(self) -> None:
"""
Restore the screen.
"""
@@ -152,8 +147,7 @@ class Screen(BaseScreen, RealTerminal):
super()._stop()
-
- def _setup_colour_pairs(self):
+ def _setup_colour_pairs(self) -> None:
"""
Initialize all 63 color pairs based on the term:
bg * 8 + 7 - fg
@@ -172,8 +166,8 @@ class Screen(BaseScreen, RealTerminal):
curses.init_pair(bg * 8 + 7 - fg, fg, bg)
- def _curs_set(self,x):
- if self.cursor_state== "fixed" or x == self.cursor_state:
+ def _curs_set(self, x):
+ if self.cursor_state == "fixed" or x == self.cursor_state:
return
try:
curses.curs_set(x)
@@ -181,14 +175,12 @@ class Screen(BaseScreen, RealTerminal):
except _curses.error:
self.cursor_state = "fixed"
-
- def _clear(self):
+ def _clear(self) -> None:
self.s.clear()
self.s.refresh()
-
- def _getch(self, wait_tenths):
- if wait_tenths==0:
+ def _getch(self, wait_tenths: int | None):
+ if wait_tenths == 0:
return self._getch_nodelay()
if wait_tenths is None:
curses.cbreak()
@@ -209,8 +201,12 @@ class Screen(BaseScreen, RealTerminal):
return self.s.getch()
- def set_input_timeouts(self, max_wait=None, complete_wait=0.1,
- resize_wait=0.1):
+ def set_input_timeouts(
+ self,
+ max_wait: int | float | None = None,
+ complete_wait: int | float = 0.1,
+ resize_wait: int | float = 0.1,
+ ):
"""
Set the get_input timeout values. All values have a granularity
of 0.1s, ie. any value between 0.15 and 0.05 will be treated as
@@ -285,7 +281,7 @@ class Screen(BaseScreen, RealTerminal):
# Avoid pegging CPU at 100% when slowly resizing, and work
# around a bug with some braindead curses implementations that
# return "no key" between "window resize" commands
- if keys==['window resize'] and self.prev_input_resize:
+ if keys == ['window resize'] and self.prev_input_resize:
while True:
keys, raw2 = self._get_input(self.resize_tenths)
raw += raw2
@@ -299,7 +295,7 @@ class Screen(BaseScreen, RealTerminal):
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
@@ -310,7 +306,6 @@ class Screen(BaseScreen, RealTerminal):
return keys, raw
return keys
-
def _get_input(self, wait_tenths):
# this works around a strange curses bug with window resizing
# not being reported correctly with repeated calls to this
@@ -358,7 +353,6 @@ class Screen(BaseScreen, RealTerminal):
return processed, raw
-
def _encode_mouse_event(self):
# convert to escape sequence
last = next = self.last_bstate
@@ -420,8 +414,7 @@ class Screen(BaseScreen, RealTerminal):
self.last_bstate = next
return l
-
- def _dbg_instr(self): # messy input string (intended for debugging)
+ def _dbg_instr(self): # messy input string (intended for debugging)
curses.echo()
self.s.nodelay(0)
curses.halfdelay(100)
@@ -429,26 +422,23 @@ class Screen(BaseScreen, RealTerminal):
curses.noecho()
return str
- def _dbg_out(self,str): # messy output function (intended for debugging)
+ def _dbg_out(self, str) -> None: # messy output function (intended for debugging)
self.s.clrtoeol()
self.s.addstr(str)
self.s.refresh()
self._curs_set(1)
- def _dbg_query(self,question): # messy query (intended for debugging)
+ def _dbg_query(self,question): # messy query (intended for debugging)
self._dbg_out(question)
return self._dbg_instr()
- def _dbg_refresh(self):
+ def _dbg_refresh(self) -> None:
self.s.refresh()
-
-
- def get_cols_rows(self):
+ def get_cols_rows(self) -> tuple[int, int]:
"""Return the terminal dimensions (num columns, num rows)."""
rows,cols = self.s.getmaxyx()
- return cols,rows
-
+ return cols, rows
def _setattr(self, a):
if a is None:
@@ -487,7 +477,7 @@ class Screen(BaseScreen, RealTerminal):
self.s.attrset(attr)
- def draw_screen(self, size, r ):
+ def draw_screen(self, size: tuple[int, int], r):
"""Paint screen with rendered canvas."""
assert self._started
@@ -548,8 +538,7 @@ class Screen(BaseScreen, RealTerminal):
self.s.refresh()
self.keep_cache_alive_link = r
-
- def clear(self):
+ def clear(self) -> None:
"""
Force the screen to be completely repainted on the next
call to draw_screen().
@@ -557,8 +546,6 @@ class Screen(BaseScreen, RealTerminal):
self.s.clear()
-
-
class _test:
def __init__(self):
self.ui = Screen()
@@ -570,13 +557,15 @@ class _test:
(f"{c} on dark blue",c, 'dark blue', 'bold'),
(f"{c} on light gray",c,'light gray', 'standout'),
])
- self.ui.run_wrapper(self.run)
- def run(self):
+ with self.ui.start():
+ self.run()
+
+ def run(self) -> None:
class FakeRender: pass
r = FakeRender()
text = [f" has_color = {self.ui.has_color!r}",""]
- attr = [[],[]]
+ attr = [[], []]
r.coords = {}
r.cursor = None
@@ -606,10 +595,11 @@ class _test:
t = ""
a = []
for k in keys:
- if type(k) == str: k = k.encode("utf-8")
+ if isinstance(k, str):
+ k = k.encode("utf-8")
+
t += f"'{k}' "
- a += [(None,1), ('yellow on dark blue',len(k)),
- (None,2)]
+ a += [(None, 1), ('yellow on dark blue', len(k)), (None, 2)]
text.append(f"{t}: {raw!r}")
attr.append(a)
@@ -617,7 +607,5 @@ class _test:
attr = attr[-rows:]
-
-
-if '__main__'==__name__:
+if '__main__' == __name__:
_test()
diff --git a/urwid/decoration.py b/urwid/decoration.py
index 28096ab..9722c5a 100755
--- a/urwid/decoration.py
+++ b/urwid/decoration.py
@@ -22,6 +22,9 @@
from __future__ import annotations
+import typing
+from collections.abc import Hashable, Mapping
+
from urwid.canvas import CompositeCanvas, SolidCanvas
from urwid.split_repr import remove_defaults
from urwid.util import int_scale
@@ -48,6 +51,9 @@ from urwid.widget import ( # doctests
delegate_to_widget_mixin,
)
+if typing.TYPE_CHECKING:
+ from typing_extensions import Literal
+
class WidgetDecoration(Widget): # "decorator" was already taken
"""
@@ -65,13 +71,15 @@ class WidgetDecoration(Widget): # "decorator" was already taken
>>> WidgetDecoration(Text(u"hi"))
<WidgetDecoration flow widget <Text flow widget 'hi'>>
"""
- def __init__(self, original_widget):
+ def __init__(self, original_widget: Widget) -> None:
self._original_widget = original_widget
+
def _repr_words(self):
return super()._repr_words() + [repr(self._original_widget)]
- def _get_original_widget(self):
+ def _get_original_widget(self) -> Widget:
return self._original_widget
+
def _set_original_widget(self, original_widget):
self._original_widget = original_widget
self._invalidate()
@@ -98,15 +106,14 @@ class WidgetDecoration(Widget): # "decorator" was already taken
base_widget = property(_get_base_widget)
- def selectable(self):
+ def selectable(self) -> bool:
return self._original_widget.selectable()
def sizing(self):
return self._original_widget.sizing()
-class WidgetPlaceholder(delegate_to_widget_mixin('_original_widget'),
- WidgetDecoration):
+class WidgetPlaceholder(delegate_to_widget_mixin('_original_widget'), WidgetDecoration):
"""
This is a do-nothing decoration widget that can be used for swapping
between widgets without modifying the container of this widget.
@@ -123,13 +130,14 @@ class WidgetPlaceholder(delegate_to_widget_mixin('_original_widget'),
class AttrMapError(WidgetError):
pass
+
class AttrMap(delegate_to_widget_mixin('_original_widget'), WidgetDecoration):
"""
AttrMap is a decoration that maps one set of attributes to another.
This object will pass all function calls and variable references to the
wrapped widget.
"""
- def __init__(self, w, attr_map, focus_map=None):
+ def __init__(self, w: Widget, attr_map, focus_map=None):
"""
:param w: widget to wrap (stored as self.original_widget)
:type w: widget
@@ -161,15 +169,17 @@ class AttrMap(delegate_to_widget_mixin('_original_widget'), WidgetDecoration):
"""
super().__init__(w)
- if type(attr_map) != dict:
- self.set_attr_map({None: attr_map})
+ if isinstance(attr_map, Mapping):
+ self.attr_map = dict(attr_map)
else:
- self.set_attr_map(attr_map)
+ self.attr_map = {None: attr_map}
- if focus_map is not None and type(focus_map) != dict:
- self.set_focus_map({None: focus_map})
+ if isinstance(focus_map, Mapping):
+ self.focus_map = dict(focus_map)
+ elif focus_map is None:
+ self.focus_map = focus_map
else:
- self.set_focus_map(focus_map)
+ self.focus_map = {None: focus_map}
def _repr_attrs(self):
# only include the focus_attr when it takes effect (not None)
@@ -178,11 +188,12 @@ class AttrMap(delegate_to_widget_mixin('_original_widget'), WidgetDecoration):
d['focus_map'] = self._focus_map
return d
- def get_attr_map(self):
+ def get_attr_map(self) -> dict[Hashable | None, Hashable]:
# make a copy so ours is not accidentally modified
# FIXME: a dictionary that detects modifications would be better
return dict(self._attr_map)
- def set_attr_map(self, attr_map):
+
+ def set_attr_map(self, attr_map: dict[Hashable | None, Hashable]) -> None:
"""
Set the attribute mapping dictionary {from_attr: to_attr, ...}
@@ -195,19 +206,22 @@ class AttrMap(delegate_to_widget_mixin('_original_widget'), WidgetDecoration):
<AttrMap flow widget <Text flow widget 'hi'> attr_map={'a': 'b'}>
"""
for from_attr, to_attr in attr_map.items():
- if not from_attr.__hash__ or not to_attr.__hash__:
- raise AttrMapError("%r:%r attribute mapping is invalid. "
- "Attributes must be hashable" % (from_attr, to_attr))
+ if not isinstance(from_attr, Hashable) or not isinstance(to_attr, Hashable):
+ raise AttrMapError(
+ f"{from_attr!r}:{to_attr!r} attribute mapping is invalid. Attributes must be hashable"
+ )
+
self._attr_map = attr_map
self._invalidate()
attr_map = property(get_attr_map, set_attr_map)
- def get_focus_map(self):
+ def get_focus_map(self) -> dict[Hashable | None, Hashable] | None:
# make a copy so ours is not accidentally modified
# FIXME: a dictionary that detects modifications would be better
if self._focus_map:
return dict(self._focus_map)
- def set_focus_map(self, focus_map):
+
+ def set_focus_map(self, focus_map: dict[Hashable | None, Hashable]) -> None:
"""
Set the focus attribute mapping dictionary
{from_attr: to_attr, ...}
@@ -228,14 +242,15 @@ class AttrMap(delegate_to_widget_mixin('_original_widget'), WidgetDecoration):
"""
if focus_map is not None:
for from_attr, to_attr in focus_map.items():
- if not from_attr.__hash__ or not to_attr.__hash__:
- raise AttrMapError("%r:%r attribute mapping is invalid. "
- "Attributes must be hashable" % (from_attr, to_attr))
+ if not isinstance(from_attr, Hashable) or not isinstance(to_attr, Hashable):
+ raise AttrMapError(
+ f"{from_attr!r}:{to_attr!r} attribute mapping is invalid. Attributes must be hashable"
+ )
self._focus_map = focus_map
self._invalidate()
focus_map = property(get_focus_map, set_focus_map)
- def render(self, size, focus=False):
+ def render(self, size, focus: bool = False) -> CompositeCanvas:
"""
Render wrapped widget and apply attribute. Return canvas.
"""
@@ -248,9 +263,8 @@ class AttrMap(delegate_to_widget_mixin('_original_widget'), WidgetDecoration):
return canv
-
class AttrWrap(AttrMap):
- def __init__(self, w, attr, focus_attr=None):
+ def __init__(self, w: Widget, attr, focus_attr=None):
"""
w -- widget to wrap (stored as self.original_widget)
attr -- attribute to apply to w
@@ -291,6 +305,7 @@ class AttrWrap(AttrMap):
def get_attr(self):
return self.attr_map[None]
+
def set_attr(self, attr):
"""
Set the attribute to apply to the wrapped widget
@@ -307,6 +322,7 @@ class AttrWrap(AttrMap):
focus_map = self.focus_map
if focus_map:
return focus_map[None]
+
def set_focus_attr(self, focus_attr):
"""
Set the attribute to apply to the wapped widget when it is in
@@ -326,7 +342,7 @@ class AttrWrap(AttrMap):
self.set_focus_map({None: focus_attr})
focus_attr = property(get_focus_attr, set_focus_attr)
- def __getattr__(self,name):
+ def __getattr__(self, name: str):
"""
Call getattr on wrapped widget. This has been the longstanding
behaviour of AttrWrap, but is discouraged. New code should be
@@ -334,7 +350,6 @@ class AttrWrap(AttrMap):
"""
return getattr(self._original_widget, name)
-
def sizing(self):
return self._original_widget.sizing()
@@ -342,6 +357,7 @@ class AttrWrap(AttrMap):
class BoxAdapterError(Exception):
pass
+
class BoxAdapter(WidgetDecoration):
"""
Adapter for using a box widget where a flow widget would usually go
@@ -362,7 +378,7 @@ class BoxAdapter(WidgetDecoration):
"""
if hasattr(box_widget, 'sizing') and BOX not in box_widget.sizing():
raise BoxAdapterError(f"{box_widget!r} is not a box widget")
- WidgetDecoration.__init__(self,box_widget)
+ super().__init__(box_widget)
self.height = height
@@ -370,13 +386,12 @@ class BoxAdapter(WidgetDecoration):
return dict(super()._repr_attrs(), height=self.height)
# originally stored as box_widget, keep for compatibility
- box_widget = property(WidgetDecoration._get_original_widget,
- WidgetDecoration._set_original_widget)
+ box_widget = property(WidgetDecoration._get_original_widget, WidgetDecoration._set_original_widget)
def sizing(self):
return {FLOW}
- def rows(self, size, focus=False):
+ def rows(self, size: tuple[int], focus: bool = False) -> int:
"""
Return the predetermined height (behave like a flow widget)
@@ -387,43 +402,49 @@ class BoxAdapter(WidgetDecoration):
# The next few functions simply tack-on our height and pass through
# to self._original_widget
- def get_cursor_coords(self, size):
+ def get_cursor_coords(self, size: tuple[int]) -> int | None:
(maxcol,) = size
if not hasattr(self._original_widget,'get_cursor_coords'):
return None
return self._original_widget.get_cursor_coords((maxcol, self.height))
- def get_pref_col(self, size):
+ def get_pref_col(self, size: tuple[int]) -> int | None:
(maxcol,) = size
if not hasattr(self._original_widget,'get_pref_col'):
return None
return self._original_widget.get_pref_col((maxcol, self.height))
- def keypress(self, size, key):
+ def keypress(self, size: tuple[int], key):
(maxcol,) = size
return self._original_widget.keypress((maxcol, self.height), key)
- def move_cursor_to_coords(self, size, col, row):
+ def move_cursor_to_coords(self, size: tuple[int], col: int, row: int):
(maxcol,) = size
if not hasattr(self._original_widget,'move_cursor_to_coords'):
return True
- return self._original_widget.move_cursor_to_coords((maxcol,
- self.height), col, row )
-
- def mouse_event(self, size, event, button, col, row, focus):
+ return self._original_widget.move_cursor_to_coords((maxcol, self.height), col, row )
+
+ def mouse_event(
+ self,
+ size: tuple[int],
+ event,
+ button: int,
+ col: int,
+ row: int,
+ focus: bool,
+ ) -> bool:
(maxcol,) = size
if not hasattr(self._original_widget,'mouse_event'):
return False
- return self._original_widget.mouse_event((maxcol, self.height),
- event, button, col, row, focus)
+ return self._original_widget.mouse_event((maxcol, self.height), event, button, col, row, focus)
- def render(self, size, focus=False):
+ def render(self, size: tuple[int], focus: bool = False) -> CompositeCanvas:
(maxcol,) = size
canv = self._original_widget.render((maxcol, self.height), focus)
canv = CompositeCanvas(canv)
return canv
- def __getattr__(self, name):
+ def __getattr__(self, name: str):
"""
Pass calls to box widget.
"""
@@ -434,9 +455,17 @@ class BoxAdapter(WidgetDecoration):
class PaddingError(Exception):
pass
+
class Padding(WidgetDecoration):
- def __init__(self, w, align=LEFT, width=RELATIVE_100, min_width=None,
- left=0, right=0):
+ def __init__(
+ self,
+ w: Widget,
+ align: Literal["left", "center", "right"] = LEFT,
+ width: int | Literal['pack', 'clip'] | tuple[Literal['relative'], int] = RELATIVE_100,
+ min_width: int | None = None,
+ left: int = 0,
+ right: int = 0,
+ ):
"""
:param w: a box, flow or fixed widget to pad on the left and/or right
this widget is stored as self.original_widget
@@ -479,7 +508,7 @@ class Padding(WidgetDecoration):
>>> size = (7,)
>>> def pr(w):
... for t in w.render(size).text:
- ... print("|%s|" % (t.decode('ascii'),))
+ ... print(f"|{t.decode('ascii')}|" )
>>> pr(Padding(Text(u"Head"), ('relative', 20), 'pack'))
| Head |
>>> pr(Padding(Divider(u"-"), left=2, right=1))
@@ -503,17 +532,15 @@ class Padding(WidgetDecoration):
super().__init__(w)
# convert obsolete parameters 'fixed left' and 'fixed right':
- if type(align) == tuple and align[0] in ('fixed left',
- 'fixed right'):
- if align[0]=='fixed left':
+ if isinstance(align, tuple) and align[0] in ('fixed left', 'fixed right'):
+ if align[0] == 'fixed left':
left = align[1]
align = LEFT
else:
right = align[1]
align = RIGHT
- if type(width) == tuple and width[0] in ('fixed left',
- 'fixed right'):
- if width[0]=='fixed left':
+ if isinstance(width, tuple) and width[0] in ('fixed left', 'fixed right'):
+ if width[0] == 'fixed left':
left = width[1]
else:
right = width[1]
@@ -525,10 +552,8 @@ class Padding(WidgetDecoration):
self.left = left
self.right = right
- self._align_type, self._align_amount = normalize_align(align,
- PaddingError)
- self._width_type, self._width_amount = normalize_width(width,
- PaddingError)
+ self._align_type, self._align_amount = normalize_align(align, PaddingError)
+ self._width_type, self._width_amount = normalize_width(width, PaddingError)
self.min_width = min_width
def sizing(self):
@@ -545,35 +570,39 @@ class Padding(WidgetDecoration):
min_width=self.min_width)
return remove_defaults(attrs, Padding.__init__)
- def _get_align(self):
+ def _get_align(self) -> Literal['left', 'center', 'right'] | tuple[Literal['relative'], int]:
"""
Return the padding alignment setting.
"""
return simplify_align(self._align_type, self._align_amount)
- def _set_align(self, align):
+
+ def _set_align(self, align: Literal['left', 'center', 'right'] | tuple[Literal['relative'], int]) -> None:
"""
Set the padding alignment.
"""
- self._align_type, self._align_amount = normalize_align(align,
- PaddingError)
+ self._align_type, self._align_amount = normalize_align(align, PaddingError)
self._invalidate()
align = property(_get_align, _set_align)
- def _get_width(self):
+ def _get_width(self) -> Literal['clip', 'pack'] | int | tuple[Literal['relative'], int]:
"""
Return the padding width.
"""
return simplify_width(self._width_type, self._width_amount)
- def _set_width(self, width):
+
+ def _set_width(self, width: Literal['clip', 'pack'] | int | tuple[Literal['relative'], int]) -> None:
"""
Set the padding width.
"""
- self._width_type, self._width_amount = normalize_width(width,
- PaddingError)
+ self._width_type, self._width_amount = normalize_width(width, PaddingError)
self._invalidate()
width = property(_get_width, _set_width)
- def render(self, size, focus=False):
+ def render(
+ self,
+ size: tuple[int] | tuple[int, int],
+ focus: bool = False,
+ ) -> CompositeCanvas:
left, right = self.padding_values(size, focus)
maxcol = size[0]
@@ -595,21 +624,27 @@ class Padding(WidgetDecoration):
return canv
- def padding_values(self, size, focus):
+ def padding_values(self, size: tuple[int] | tuple[int, int], focus: bool) -> tuple[int, int]:
"""Return the number of columns to pad on the left and right.
Override this method to define custom padding behaviour."""
maxcol = size[0]
if self._width_type == CLIP:
width, ignore = self._original_widget.pack((), focus=focus)
- return calculate_left_right_padding(maxcol,
- self._align_type, self._align_amount,
- CLIP, width, None, self.left, self.right)
+ return calculate_left_right_padding(
+ maxcol,
+ self._align_type,
+ self._align_amount,
+ CLIP,
+ width,
+ None,
+ self.left,
+ self.right,
+ )
if self._width_type == PACK:
maxwidth = max(maxcol - self.left - self.right,
self.min_width or 0)
- (width, ignore) = self._original_widget.pack((maxwidth,),
- focus=focus)
+ (width, ignore) = self._original_widget.pack((maxwidth,), focus=focus)
return calculate_left_right_padding(maxcol,
self._align_type, self._align_amount,
GIVEN, width, self.min_width,
@@ -632,16 +667,16 @@ class Padding(WidgetDecoration):
return frows
return self._original_widget.rows((maxcol-left-right,), focus=focus)
- def keypress(self, size, key):
+ def keypress(self, size: tuple[int] | tuple[int, int], key):
"""Pass keypress to self._original_widget."""
maxcol = size[0]
left, right = self.padding_values(size, True)
maxvals = (maxcol-left-right,)+size[1:]
return self._original_widget.keypress(maxvals, key)
- def get_cursor_coords(self,size):
+ def get_cursor_coords(self, size: tuple[int] | tuple[int, int]) -> tuple[int, int] | None:
"""Return the (x,y) coordinates of cursor within self._original_widget."""
- if not hasattr(self._original_widget,'get_cursor_coords'):
+ if not hasattr(self._original_widget, 'get_cursor_coords'):
return None
left, right = self.padding_values(size, True)
maxcol = size[0]
@@ -654,17 +689,20 @@ class Padding(WidgetDecoration):
x, y = coords
return x+left, y
- def move_cursor_to_coords(self, size, x, y):
+ def move_cursor_to_coords(
+ self,
+ size: tuple[int] | tuple[int, int], x: int, y: int,
+ ) -> bool:
"""Set the cursor position with (x,y) coordinates of self._original_widget.
Returns True if move succeeded, False otherwise.
"""
- if not hasattr(self._original_widget,'move_cursor_to_coords'):
+ if not hasattr(self._original_widget, 'move_cursor_to_coords'):
return True
left, right = self.padding_values(size, True)
maxcol = size[0]
- maxvals = (maxcol-left-right,)+size[1:]
- if type(x)==int:
+ maxvals = (maxcol - left - right,) + size[1:]
+ if isinstance(x, int):
if x < left:
x = left
elif x >= maxcol-right:
@@ -672,38 +710,54 @@ class Padding(WidgetDecoration):
x -= left
return self._original_widget.move_cursor_to_coords(maxvals, x, y)
- def mouse_event(self, size, event, button, x, y, focus):
+ def mouse_event(
+ self,
+ size: tuple[int] | tuple[int, int],
+ event,
+ button: int,
+ x: int,
+ y: int,
+ focus: bool,
+ ):
"""Send mouse event if position is within self._original_widget."""
- if not hasattr(self._original_widget,'mouse_event'):
+ if not hasattr(self._original_widget, 'mouse_event'):
return False
+
left, right = self.padding_values(size, focus)
maxcol = size[0]
if x < left or x >= maxcol-right:
return False
maxvals = (maxcol-left-right,)+size[1:]
- return self._original_widget.mouse_event(maxvals, event, button, x-left, y,
- focus)
-
+ return self._original_widget.mouse_event(maxvals, event, button, x-left, y, focus)
- def get_pref_col(self, size):
+ def get_pref_col(self, size: tuple[int] | tuple[int, int]) -> int | None:
"""Return the preferred column from self._original_widget, or None."""
- if not hasattr(self._original_widget,'get_pref_col'):
+ if not hasattr(self._original_widget, 'get_pref_col'):
return None
+
left, right = self.padding_values(size, True)
maxcol = size[0]
maxvals = (maxcol-left-right,)+size[1:]
x = self._original_widget.get_pref_col(maxvals)
- if type(x) == int:
- return x+left
+ if isinstance(x, int):
+ return x + left
return x
class FillerError(Exception):
pass
+
class Filler(WidgetDecoration):
- def __init__(self, body, valign=MIDDLE, height=PACK, min_height=None,
- top=0, bottom=0):
+ def __init__(
+ self,
+ body: Widget,
+ valign: Literal['top', 'middle', 'bottom'] | tuple[Literal['relative'], int] = MIDDLE,
+ height: int | Literal['pack'] | tuple[Literal['relative'], int] = PACK,
+ min_height: int | None = None,
+ top: int = 0,
+ bottom: int = 0,
+ ) -> None:
"""
:param body: a flow widget or box widget to be filled around (stored
as self.original_widget)
@@ -750,14 +804,12 @@ class Filler(WidgetDecoration):
if isinstance(height, tuple):
if height[0] == 'fixed top':
if not isinstance(valign, tuple) or valign[0] != 'fixed bottom':
- raise FillerError("fixed top height may only be used "
- "with fixed bottom valign")
+ raise FillerError("fixed top height may only be used with fixed bottom valign")
top = height[1]
height = RELATIVE_100
elif height[0] == 'fixed bottom':
if not isinstance(valign, tuple) or valign[0] != 'fixed top':
- raise FillerError("fixed bottom height may only be used "
- "with fixed top valign")
+ raise FillerError("fixed bottom height may only be used with fixed top valign")
bottom = height[1]
height = RELATIVE_100
if isinstance(valign, tuple):
@@ -774,10 +826,8 @@ class Filler(WidgetDecoration):
self.top = top
self.bottom = bottom
- self.valign_type, self.valign_amount = normalize_valign(valign,
- FillerError)
- self.height_type, self.height_amount = normalize_height(height,
- FillerError)
+ self.valign_type, self.valign_amount = normalize_valign(valign, FillerError)
+ self.height_type, self.height_amount = normalize_height(height, FillerError)
if self.height_type not in (GIVEN, PACK):
self.min_height = min_height
@@ -785,7 +835,7 @@ class Filler(WidgetDecoration):
self.min_height = None
def sizing(self):
- return {BOX} # always a box widget
+ return {BOX} # always a box widget
def _repr_attrs(self):
attrs = dict(super()._repr_attrs(),
@@ -801,11 +851,11 @@ class Filler(WidgetDecoration):
set_body = WidgetDecoration._set_original_widget
body = property(get_body, set_body)
- def selectable(self):
+ def selectable(self) -> bool:
"""Return selectable from body."""
return self._original_widget.selectable()
- def filler_values(self, size, focus):
+ def filler_values(self, size: tuple[int, int], focus: bool) -> tuple[int, int]:
"""
Return the number of rows to pad on the top and bottom.
@@ -826,7 +876,7 @@ class Filler(WidgetDecoration):
self.min_height, self.top, self.bottom)
- def render(self, size, focus=False):
+ def render(self, size: tuple[int, int], focus: bool = False) -> CompositeCanvas:
"""Render self.original_widget with space above and/or below."""
(maxcol, maxrow) = size
top, bottom = self.filler_values(size, focus)
@@ -847,17 +897,16 @@ class Filler(WidgetDecoration):
canv.pad_trim_top_bottom(top, bottom)
return canv
-
- def keypress(self, size, key):
+ def keypress(self, size: tuple[int, int], key):
"""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)
+ top, bottom = self.filler_values((maxcol, maxrow), True)
return self._original_widget.keypress((maxcol,maxrow-top-bottom), key)
- def get_cursor_coords(self, size):
+ def get_cursor_coords(self, size: tuple[int, int]) -> tuple[int, int] | None:
"""Return cursor coords from self.original_widget if any."""
(maxcol, maxrow) = size
if not hasattr(self._original_widget, 'get_cursor_coords'):
@@ -876,7 +925,7 @@ class Filler(WidgetDecoration):
y = maxrow-1
return x, y+top
- def get_pref_col(self, size):
+ def get_pref_col(self, size: tuple[int, int]) -> int:
"""Return pref_col from self.original_widget if any."""
(maxcol, maxrow) = size
if not hasattr(self._original_widget, 'get_pref_col'):
@@ -891,7 +940,7 @@ class Filler(WidgetDecoration):
return x
- def move_cursor_to_coords(self, size, col, row):
+ def move_cursor_to_coords(self, size: tuple[int, int], col:int, row: int) -> bool:
"""Pass to self.original_widget."""
(maxcol, maxrow) = size
if not hasattr(self._original_widget, 'move_cursor_to_coords'):
@@ -907,7 +956,15 @@ class Filler(WidgetDecoration):
return self._original_widget.move_cursor_to_coords(
(maxcol, maxrow-top-bottom), col, row-top)
- def mouse_event(self, size, event, button, col, row, focus):
+ def mouse_event(
+ self,
+ size: tuple[int, int],
+ event,
+ button: int,
+ col: int,
+ row: int,
+ focus: bool,
+ ) -> bool:
"""Pass to self.original_widget."""
(maxcol, maxrow) = size
if not hasattr(self._original_widget, 'mouse_event'):
@@ -923,6 +980,7 @@ class Filler(WidgetDecoration):
return self._original_widget.mouse_event((maxcol, maxrow-top-bottom),
event, button,col, row-top, focus)
+
class WidgetDisable(WidgetDecoration):
"""
A decoration widget that disables interaction with the widget it
@@ -932,32 +990,44 @@ class WidgetDisable(WidgetDecoration):
no_cache = ["rows"]
ignore_focus = True
- def selectable(self):
+ def selectable(self) -> Literal[False]:
return False
- def rows(self, size, focus=False):
+
+ def rows(self, size, focus: bool = False) -> int:
return self._original_widget.rows(size, False)
+
def sizing(self):
return self._original_widget.sizing()
- def pack(self, size, focus=False):
+
+ def pack(self, size, focus: bool = False) -> tuple[int, int]:
return self._original_widget.pack(size, False)
- def render(self, size, focus=False):
+
+ def render(self, size, focus: bool = False) -> CompositeCanvas:
canv = self._original_widget.render(size, False)
return CompositeCanvas(canv)
-def normalize_align(align, err):
+
+def normalize_align(
+ align: Literal['left', 'center', 'right'] | tuple[Literal['relative'], int],
+ err: type[BaseException],
+) -> tuple[Literal['left', 'center', 'right'], None] | tuple[Literal['relative'], int]:
"""
Split align into (align_type, align_amount). Raise exception err
if align doesn't match a valid alignment.
"""
if align in (LEFT, CENTER, RIGHT):
return (align, None)
- elif type(align) == tuple and len(align) == 2 and align[0] == RELATIVE:
+ elif isinstance(align, tuple) and len(align) == 2 and align[0] == RELATIVE:
return align
- raise err("align value %r is not one of 'left', 'center', "
- "'right', ('relative', percentage 0=left 100=right)"
- % (align,))
+ raise err(
+ f"align value {align!r} is not one of 'left', 'center', 'right', ('relative', percentage 0=left 100=right)"
+ )
-def simplify_align(align_type, align_amount):
+
+def simplify_align(
+ align_type: Literal['left', 'center', 'right', 'relative'],
+ align_amount: int | None,
+) -> Literal['left', 'center', 'right'] | tuple[Literal['relative'], int]:
"""
Recombine (align_type, align_amount) into an align value.
Inverse of normalize_align.
@@ -966,22 +1036,31 @@ def simplify_align(align_type, align_amount):
return (align_type, align_amount)
return align_type
-def normalize_width(width, err):
+
+def normalize_width(
+ width: Literal['clip', 'pack'] | int | tuple[Literal['relative'], int],
+ err: type[BaseException],
+) -> tuple[Literal['clip', 'pack'], None] | tuple[Literal['given', 'relative'], int]:
"""
Split width into (width_type, width_amount). Raise exception err
if width doesn't match a valid alignment.
"""
if width in (CLIP, PACK):
return (width, None)
- elif type(width) == int:
+ elif isinstance(width, int):
return (GIVEN, width)
- elif type(width) == tuple and len(width) == 2 and width[0] == RELATIVE:
+ elif isinstance(width, tuple) and len(width) == 2 and width[0] == RELATIVE:
return width
- raise err("width value %r is not one of fixed number of columns, "
- "'pack', ('relative', percentage of total width), 'clip'"
- % (width,))
+ raise err(
+ f"width value {width!r} is not one of"
+ f"fixed number of columns, 'pack', ('relative', percentage of total width), 'clip'"
+ )
+
-def simplify_width(width_type, width_amount):
+def simplify_width(
+ width_type: Literal['clip', 'pack', 'given', 'relative'],
+ width_amount: int | None,
+) -> Literal['clip', 'pack'] | int | tuple[Literal['relative'], int]:
"""
Recombine (width_type, width_amount) into an width value.
Inverse of normalize_width.
@@ -992,21 +1071,28 @@ def simplify_width(width_type, width_amount):
return width_amount
return (width_type, width_amount)
-def normalize_valign(valign, err):
+
+def normalize_valign(
+ valign: Literal["top", "middle", "bottom"] | tuple[Literal["relative"], int],
+ err: type[BaseException],
+) -> tuple[Literal["top", "middle", "bottom"], None] | tuple[Literal["relative"], int]:
"""
Split align into (valign_type, valign_amount). Raise exception err
if align doesn't match a valid alignment.
"""
if valign in (TOP, MIDDLE, BOTTOM):
return (valign, None)
- elif (isinstance(valign, tuple) and len(valign) == 2 and
- valign[0] == RELATIVE):
+ elif isinstance(valign, tuple) and len(valign) == 2 and valign[0] == RELATIVE:
return valign
- raise err("valign value %r is not one of 'top', 'middle', "
- "'bottom', ('relative', percentage 0=left 100=right)"
- % (valign,))
+ raise err(
+ f"valign value {valign!r} is not one of 'top', 'middle', 'bottom', ('relative', percentage 0=left 100=right)"
+ )
+
-def simplify_valign(valign_type, valign_amount):
+def simplify_valign(
+ valign_type: Literal["top", "middle", "bottom", "relative"],
+ valign_amount: int | None,
+) -> Literal["top", "middle", "bottom"] | tuple[Literal["relative"], int]:
"""
Recombine (valign_type, valign_amount) into an valign value.
Inverse of normalize_valign.
@@ -1015,23 +1101,31 @@ def simplify_valign(valign_type, valign_amount):
return (valign_type, valign_amount)
return valign_type
-def normalize_height(height, err):
+
+def normalize_height(
+ height: int | Literal['flow', 'pack'] | tuple[Literal['relative'], int],
+ err: type[BaseException],
+) -> tuple[Literal['flow', 'pack'], None] | tuple[Literal['relative', 'given'], int]:
"""
Split height into (height_type, height_amount). Raise exception err
if height isn't valid.
"""
if height in (FLOW, PACK):
return (height, None)
- elif (isinstance(height, tuple) and len(height) == 2 and
- height[0] == RELATIVE):
+ elif (isinstance(height, tuple) and len(height) == 2 and height[0] == RELATIVE):
return height
elif isinstance(height, int):
return (GIVEN, height)
- raise err("height value %r is not one of fixed number of columns, "
- "'pack', ('relative', percentage of total height)"
- % (height,))
+ raise err(
+ f"height value {height!r} is not one of "
+ f"fixed number of columns, 'pack', ('relative', percentage of total height)"
+ )
-def simplify_height(height_type, height_amount):
+
+def simplify_height(
+ height_type: Literal['flow', 'pack', 'relative', 'given'],
+ height_amount: int | None,
+) -> int | Literal['flow', 'pack'] | tuple[Literal['relative'], int]:
"""
Recombine (height_type, height_amount) into an height value.
Inverse of normalize_height.
@@ -1043,8 +1137,16 @@ def simplify_height(height_type, height_amount):
return (height_type, height_amount)
-def calculate_top_bottom_filler(maxrow, valign_type, valign_amount, height_type,
- height_amount, min_height, top, bottom):
+def calculate_top_bottom_filler(
+ maxrow: int,
+ valign_type: Literal['top', 'middle', 'bottom', 'relative'],
+ valign_amount: int,
+ height_type: Literal['given', 'relative', 'clip'],
+ height_amount: int,
+ min_height: int | None,
+ top: int,
+ bottom: int,
+) -> tuple[int, int]:
"""
Return the amount of filler (or clipping) on the top and
bottom part of maxrow rows to satisfy the following:
@@ -1109,8 +1211,16 @@ def calculate_top_bottom_filler(maxrow, valign_type, valign_amount, height_type,
return top, bottom
-def calculate_left_right_padding(maxcol, align_type, align_amount,
- width_type, width_amount, min_width, left, right):
+def calculate_left_right_padding(
+ maxcol: int,
+ align_type: Literal['left', 'center', 'right'],
+ align_amount: int,
+ width_type: Literal['fixed', 'relative', 'clip'],
+ width_amount: int,
+ min_width: int | None,
+ left: int,
+ right: int,
+) -> tuple[int, int]:
"""
Return the amount of padding (or clipping) on the left and
right part of maxcol columns to satisfy the following:
@@ -1154,7 +1264,7 @@ def calculate_left_right_padding(maxcol, align_type, align_amount,
else:
width = width_amount
- standard_alignments = {LEFT:0, CENTER:50, RIGHT:100}
+ standard_alignments = {LEFT: 0, CENTER: 50, RIGHT: 100}
align = standard_alignments.get(align_type, align_amount)
# add the remainder of left/right the padding
@@ -1163,11 +1273,11 @@ def calculate_left_right_padding(maxcol, align_type, align_amount,
left = maxcol - width - right
# reduce padding if we are clipping an edge
- if right < 0 and left > 0:
+ if right < 0 < left:
shift = min(left, -right)
left -= shift
right += shift
- elif left < 0 and right > 0:
+ elif left < 0 < right:
shift = min(right, -left)
right -= shift
left += shift
@@ -1180,10 +1290,10 @@ def calculate_left_right_padding(maxcol, align_type, align_amount,
return left, right
-
def _test():
import doctest
doctest.testmod()
+
if __name__=='__main__':
_test()
diff --git a/urwid/display_common.py b/urwid/display_common.py
index b135a5f..cceaf07 100755
--- a/urwid/display_common.py
+++ b/urwid/display_common.py
@@ -964,7 +964,7 @@ class BaseScreen(metaclass=signals.MetaSignals):
"""
basic = AttrSpec(foreground, background, 16)
- if type(mono) == tuple:
+ if isinstance(mono, tuple):
# old style of specifying mono attributes was to put them
# in a tuple. convert to comma-separated string
mono = ",".join(mono)
@@ -1004,5 +1004,6 @@ def _test():
import doctest
doctest.testmod()
+
if __name__=='__main__':
_test()
diff --git a/urwid/escape.py b/urwid/escape.py
index ec8da4e..c475da1 100644
--- a/urwid/escape.py
+++ b/urwid/escape.py
@@ -27,6 +27,7 @@ Terminal Escape Sequences for input and display
from __future__ import annotations
import re
+from collections.abc import MutableMapping, Sequence
try:
from urwid import str_util
@@ -137,36 +138,36 @@ input_sequences = [
('[0n', 'status ok')
]
+
class KeyqueueTrie:
- def __init__( self, sequences ):
+ def __init__(self, sequences) -> None:
self.data = {}
for s, result in sequences:
- assert type(result) != dict
+ assert not isinstance(result, dict)
self.add(self.data, s, result)
def add(self, root, s, result):
- assert type(root) == dict, "trie conflict detected"
+ assert isinstance(root, MutableMapping), "trie conflict detected"
assert len(s) > 0, "trie conflict detected"
if ord(s[0]) in root:
return self.add(root[ord(s[0])], s[1:], result)
- if len(s)>1:
+ if len(s) > 1:
d = {}
root[ord(s[0])] = d
return self.add(d, s[1:], result)
root[ord(s)] = result
- def get(self, keys, more_available):
+ def get(self, keys, more_available: bool):
result = self.get_recurse(self.data, keys, more_available)
if not result:
result = self.read_cursor_position(keys, more_available)
return result
- def get_recurse(self, root, keys, more_available):
- if type(root) != dict:
+ def get_recurse(self, root, keys, more_available: bool):
+ if not isinstance(root, MutableMapping):
if root == "mouse":
- return self.read_mouse_info(keys,
- more_available)
+ return self.read_mouse_info(keys, more_available)
elif root == "sgrmouse":
return self.read_sgrmouse_info (keys, more_available)
return (root, keys)
@@ -179,7 +180,7 @@ class KeyqueueTrie:
return None
return self.get_recurse(root[keys[0]], keys[1:], more_available)
- def read_mouse_info(self, keys, more_available):
+ def read_mouse_info(self, keys, more_available: bool):
if len(keys) < 3:
if more_available:
raise MoreInputRequired()
@@ -212,7 +213,7 @@ class KeyqueueTrie:
return ( (f"{prefix}mouse {action}", button, x, y), keys[3:] )
- def read_sgrmouse_info(self, keys, more_available):
+ def read_sgrmouse_info(self, keys, more_available: bool):
# Helpful links:
# https://stackoverflow.com/questions/5966903/how-to-get-mousemove-and-mouseclick-in-bash
# http://invisible-island.net/xterm/ctlseqs/ctlseqs.pdf
@@ -227,7 +228,7 @@ class KeyqueueTrie:
found_m = False
for k in keys:
value = value + chr(k)
- if ((k is ord('M')) or (k is ord('m'))):
+ if (k is ord('M')) or (k is ord('m')):
found_m = True
break
pos_m += 1
@@ -261,8 +262,7 @@ class KeyqueueTrie:
return ( (f"mouse {action}", button, x, y), keys[pos_m + 1:] )
-
- def read_cursor_position(self, keys, more_available):
+ def read_cursor_position(self, keys, more_available: bool):
"""
Interpret cursor position information being sent by the
user's terminal. Returned as ('cursor position', x, y)
@@ -305,14 +305,11 @@ class KeyqueueTrie:
if not x and k == ord('0'):
return None
x = x * 10 + k - ord('0')
- if not keys[i:]:
- if more_available:
- raise MoreInputRequired()
+ if not keys[i:] and more_available:
+ raise MoreInputRequired()
return None
-
-
# This is added to button value to signal mouse release by curses_display
# and raw_display when we know which button was released. NON-STANDARD
MOUSE_RELEASE_FLAG = 2048
@@ -366,8 +363,7 @@ _keyconv = {
}
-
-def process_keyqueue(codes, more_available):
+def process_keyqueue(codes: Sequence[int], more_available: bool):
"""
codes -- list of key codes
more_available -- if True then raise MoreInputRequired when in the
@@ -377,46 +373,44 @@ def process_keyqueue(codes, more_available):
returns (list of input, list of remaining key codes).
"""
code = codes[0]
- if code >= 32 and code <= 126:
+ if 32 <= code <= 126:
key = chr(code)
return [key], codes[1:]
if code in _keyconv:
return [_keyconv[code]], codes[1:]
- if code >0 and code <27:
+ if 0 < code < 27:
return [f"ctrl {chr(ord('a') + code - 1)}"], codes[1:]
- if code >27 and code <32:
+ if 27 < code < 32:
return [f"ctrl {chr(ord('A') + code - 1)}"], codes[1:]
em = str_util.get_byte_encoding()
- if (em == 'wide' and code < 256 and
- within_double_byte(chr(code),0,0)):
- if not codes[1:]:
- if more_available:
- raise MoreInputRequired()
+ if em == 'wide' and code < 256 and within_double_byte(chr(code), 0, 0):
+ if not codes[1:] and more_available:
+ raise MoreInputRequired()
if codes[1:] and codes[1] < 256:
db = chr(code)+chr(codes[1])
if within_double_byte(db, 0, 1):
return [db], codes[2:]
- if em == 'utf8' and code>127 and code<256:
- if code & 0xe0 == 0xc0: # 2-byte form
+ if em == 'utf8' and 127 < code < 256:
+ if code & 0xe0 == 0xc0: # 2-byte form
need_more = 1
- elif code & 0xf0 == 0xe0: # 3-byte form
+ elif code & 0xf0 == 0xe0: # 3-byte form
need_more = 2
- elif code & 0xf8 == 0xf0: # 4-byte form
+ elif code & 0xf8 == 0xf0: # 4-byte form
need_more = 3
else:
- return ["<%d>"%code], codes[1:]
+ return [f"<{code:d}>"], codes[1:]
for i in range(need_more):
if len(codes)-1 <= i:
if more_available:
raise MoreInputRequired()
else:
- return ["<%d>"%code], codes[1:]
+ return [f"<{code:d}>"], codes[1:]
k = codes[i+1]
if k>256 or k&0xc0 != 0x80:
- return ["<%d>"%code], codes[1:]
+ return [f"<{code:d}>"], codes[1:]
s = bytes(codes[:need_more+1])
@@ -424,13 +418,13 @@ def process_keyqueue(codes, more_available):
try:
return [s.decode("utf-8")], codes[need_more+1:]
except UnicodeDecodeError:
- return ["<%d>"%code], codes[1:]
+ return [f"<{code:d}>"], codes[1:]
- if code >127 and code <256:
+ if 127 < code < 256:
key = chr(code)
return [key], codes[1:]
if code != 27:
- return ["<%d>"%code], codes[1:]
+ return [f"<{code:d}>"], codes[1:]
result = input_trie.get(codes[1:], more_available)
@@ -440,8 +434,7 @@ def process_keyqueue(codes, more_available):
if codes[1:]:
# Meta keys -- ESC+Key form
- run, remaining_codes = process_keyqueue(codes[1:],
- more_available)
+ run, remaining_codes = process_keyqueue(codes[1:], more_available)
if urwid.util.is_mouse_event(run[0]):
return ['esc'] + run, remaining_codes
if run[0] == "esc" or run[0].find("meta ") >= 0:
@@ -475,22 +468,30 @@ REPORT_CURSOR_POSITION = f"{ESC}[6n"
INSERT_ON = f"{ESC}[4h"
INSERT_OFF = f"{ESC}[4l"
-def set_cursor_position( x, y ):
- assert type(x) == int
- assert type(y) == int
- return ESC+"[%d;%dH" %(y+1, x+1)
-def move_cursor_right(x):
- if x < 1: return ""
- return ESC+"[%dC" % x
+def set_cursor_position(x: int, y: int) -> str:
+ assert isinstance(x, int)
+ assert isinstance(y, int)
+ return ESC + f"[{y + 1:d};{x + 1:d}H"
+
+
+def move_cursor_right(x: int) -> str:
+ if x < 1:
+ return ""
+ return ESC + f"[{x:d}C"
+
+
+def move_cursor_up(x: int) -> str:
+ if x < 1:
+ return ""
+ return ESC + f"[{x:d}A"
+
-def move_cursor_up(x):
- if x < 1: return ""
- return ESC+"[%dA" % x
+def move_cursor_down(x: int) -> str:
+ if x < 1:
+ return ""
+ return ESC + f"[{x:d}B"
-def move_cursor_down(x):
- if x < 1: return ""
- return ESC+"[%dB" % x
HIDE_CURSOR = f"{ESC}[?25l"
SHOW_CURSOR = f"{ESC}[?25h"
diff --git a/urwid/font.py b/urwid/font.py
index 42dc647..295bcf1 100755
--- a/urwid/font.py
+++ b/urwid/font.py
@@ -80,13 +80,17 @@ def separate_glyphs(gdata, height):
c = None
return dout, utf8_required
+
_all_fonts = []
+
+
def get_all_fonts():
"""
Return a list of (font name, font class) tuples.
"""
return _all_fonts[:]
+
def add_font(name, cls):
_all_fonts.append((name, cls))
@@ -103,7 +107,7 @@ class Font:
self.add_glyphs(gdata)
@staticmethod
- def _to_text(obj, encoding='utf-8', errors='strict'):
+ def _to_text(obj, encoding='utf-8', errors='strict') -> str:
if isinstance(obj, str):
return obj
elif isinstance(obj, bytes):
@@ -114,12 +118,12 @@ class Font:
self.char.update(d)
self.utf8_required |= utf8_required
- def characters(self):
+ def characters(self) -> str:
l = sorted(self.char)
return "".join(l)
- def char_width(self, c):
+ def char_width(self, c) -> int:
if c in self.char:
return self.char[c][0]
return 0
@@ -143,7 +147,6 @@ class Font:
return canv
-
#safe_palette = u"┘┐┌└┼─├┤┴┬│"
#more_palette = u"═║╒╓╔╕╖╗╘╙╚╛╜╝╞╟╠╡╢╣╤╥╦╧╨╩╪╫╬○"
#block_palette = u"▄#█#▀#▌#▐#▖#▗#▘#▙#▚#▛#▜#▝#▞#▟"
@@ -436,7 +439,9 @@ tttttuuuuuuuvvvvvvvwwwwwwwwxxxxxxxyyyyyyyzzzzzzz
█▌ ▀███▀ ▐█▌ ▀█▌▐█▀ ▐█ █▌ ▀▀▀█▌▐█████▌
▀███▀
"""]
-add_font("Half Block 7x7",HalfBlock7x7Font)
+
+
+add_font("Half Block 7x7", HalfBlock7x7Font)
if __name__ == "__main__":
@@ -449,7 +454,7 @@ if __name__ == "__main__":
u = ""
if f.utf8_required:
u = "(U)"
- print(("%-20s %3s " % (n,u)), end=' ')
+ print(f"{n:<20} {u:>3} ", end=' ')
c = f.characters()
if c == all_ascii:
print("Full ASCII")
diff --git a/urwid/graphics.py b/urwid/graphics.py
index 71c0791..f671c81 100755
--- a/urwid/graphics.py
+++ b/urwid/graphics.py
@@ -22,13 +22,9 @@
from __future__ import annotations
-from urwid.canvas import (
- CanvasCombine,
- CanvasJoin,
- CompositeCanvas,
- SolidCanvas,
- TextCanvas,
-)
+import typing
+
+from urwid.canvas import CanvasCombine, CanvasJoin, CompositeCanvas, SolidCanvas, TextCanvas
from urwid.container import Columns, Pile
from urwid.decoration import WidgetDecoration
from urwid.display_common import AttrSpec
@@ -50,6 +46,9 @@ from urwid.widget import (
nocache_widget_render_instance,
)
+if typing.TYPE_CHECKING:
+ from typing_extensions import Literal
+
class BigText(Widget):
_sizing = frozenset([FIXED])
@@ -116,11 +115,21 @@ class BigText(Widget):
class LineBox(WidgetDecoration, WidgetWrap):
- def __init__(self, original_widget, title="",
- title_align="center", title_attr=None,
- tlcorner='┌', tline='─', lline='│',
- trcorner='┐', blcorner='└', rline='│',
- bline='─', brcorner='┘'):
+ def __init__(
+ self,
+ original_widget: Widget,
+ title: str = "",
+ title_align: Literal['left', 'center', 'right'] = "center",
+ title_attr=None,
+ tlcorner: str = '┌',
+ tline: str = '─',
+ lline: str = '│',
+ trcorner: str = '┐',
+ blcorner: str = '└',
+ rline: str = '│',
+ bline: str = '─',
+ brcorner: str = '┘',
+ ) -> None:
"""
Draw a line around original_widget.
@@ -232,7 +241,7 @@ class LineBox(WidgetDecoration, WidgetWrap):
self.title_widget.set_text(self.format_title(text))
self.tline_widget._invalidate()
- def pack(self, size=None, focus=False):
+ def pack(self, size=None, focus: bool = False):
"""
Return the number of screen columns and rows required for
this Linebox widget to be displayed without wrapping or
@@ -275,9 +284,11 @@ def nocache_bargraph_get_data(self, get_data_fn):
self.render = nocache_widget_render_instance(self)
self._get_data = get_data_fn
+
class BarGraphError(Exception):
pass
+
class BarGraph(Widget, metaclass=BarGraphMeta):
_sizing = frozenset([BOX])
@@ -337,7 +348,7 @@ class BarGraph(Widget, metaclass=BarGraphMeta):
raise BarGraphError(f"attlist must include at least background and seg1: {attlist!r}")
assert len(attlist) >= 2, 'must at least specify bg and fg!'
for a in attlist:
- if type(a) != tuple:
+ if not isinstance(a, tuple):
self.attr.append(a)
self.char.append(' ')
else:
@@ -348,7 +359,7 @@ class BarGraph(Widget, metaclass=BarGraphMeta):
self.hatt = []
if hatt is None:
hatt = [self.attr[0]]
- elif type(hatt) != list:
+ elif not isinstance(hatt, list):
hatt = [hatt]
self.hatt = hatt
@@ -359,9 +370,9 @@ class BarGraph(Widget, metaclass=BarGraphMeta):
(fg, bg), attr = i
except ValueError:
raise BarGraphError(f"satt not in (fg,bg:attr) form: {i!r}")
- if type(fg) != int or fg >= len(attlist):
+ if not isinstance(fg, int) or fg >= len(attlist):
raise BarGraphError(f"fg not valid integer: {fg!r}")
- if type(bg) != int or bg >= len(attlist):
+ if not isinstance(bg, int) or bg >= len(attlist):
raise BarGraphError(f"bg not valid integer: {fg!r}")
if fg <= bg:
raise BarGraphError(f"fg ({fg}) not > bg ({bg})")
@@ -391,7 +402,7 @@ class BarGraph(Widget, metaclass=BarGraphMeta):
self.data = bardata, top, hlines
self._invalidate()
- def _get_data(self, size):
+ def _get_data(self, size: tuple[int, int]):
"""
Return (bardata, top, hlines)
@@ -410,7 +421,7 @@ class BarGraph(Widget, metaclass=BarGraphMeta):
return bardata, top, hlines
- def set_bar_width(self, width):
+ def set_bar_width(self, width: int | None):
"""
Set a preferred bar width for calculate_bar_widths to use.
@@ -420,7 +431,7 @@ class BarGraph(Widget, metaclass=BarGraphMeta):
self.bar_width = width
self._invalidate()
- def calculate_bar_widths(self, size, bardata):
+ def calculate_bar_widths(self, size: tuple[int, int], bardata):
"""
Return a list of bar widths, one for each bar in data.
@@ -446,16 +457,16 @@ class BarGraph(Widget, metaclass=BarGraphMeta):
remain -= 1
return widths
- def selectable(self):
+ def selectable(self) -> Literal[False]:
"""
Return False.
"""
return False
- def use_smoothed(self):
+ def use_smoothed(self) -> bool:
return self.satt and get_encoding_mode() == "utf8"
- def calculate_display(self, size):
+ def calculate_display(self, size: tuple[int, int]):
"""
Calculate display data.
"""
@@ -477,7 +488,7 @@ class BarGraph(Widget, metaclass=BarGraphMeta):
return disp
- def hlines_display(self, disp, top, hlines, maxrow):
+ def hlines_display(self, disp, top: int, hlines, maxrow: int):
"""
Add hlines to display structure represented as bar_type tuple
values:
@@ -523,8 +534,7 @@ class BarGraph(Widget, metaclass=BarGraphMeta):
def fill_row(row, chnum):
rout = []
for bar_type, width in row:
- if (type(bar_type) == int and
- len(self.hatt) > bar_type):
+ if (isinstance(bar_type, int) and len(self.hatt) > bar_type):
rout.append(((bar_type, chnum), width))
continue
rout.append((bar_type, width))
@@ -574,7 +584,7 @@ class BarGraph(Widget, metaclass=BarGraphMeta):
l1 = (bt1, w1 - w2)
elif w2 > w1:
l2 = (bt2, w2 - w1)
- if type(bt1) == tuple:
+ if isinstance(bt1, tuple):
return (bt1, wmin), l1, l2
if (bt2, bt1) not in self.satt:
if r < 4:
@@ -582,7 +592,7 @@ class BarGraph(Widget, metaclass=BarGraphMeta):
return (bt1, wmin), l1, l2
return ((bt2, bt1, 8 - r), wmin), l1, l2
- def row_combine_last(count, row):
+ def row_combine_last(count: int, row):
o_count, o_row = o[-1]
row = row[:] # shallow copy, so we don't destroy orig.
o_row = o_row[:]
@@ -623,7 +633,7 @@ class BarGraph(Widget, metaclass=BarGraphMeta):
r = y_count
return [(y // 8, row) for (y, row) in o]
- def render(self, size, focus=False):
+ def render(self, size: tuple[int, int], focus: bool = False) -> CompositeCanvas:
"""
Render BarGraph.
"""
@@ -634,7 +644,7 @@ class BarGraph(Widget, metaclass=BarGraphMeta):
for y_count, row in disp:
l = []
for bar_type, width in row:
- if type(bar_type) == tuple:
+ if isinstance(bar_type, tuple):
if len(bar_type) == 3:
# vertical eighths
fg, bg, k = bar_type
@@ -657,7 +667,7 @@ class BarGraph(Widget, metaclass=BarGraphMeta):
return canv
-def calculate_bargraph_display(bardata, top, bar_widths, maxrow):
+def calculate_bargraph_display(bardata, top, bar_widths, maxrow: int):
"""
Calculate a rendering of the bar graph described by data, bar_widths
and height.
@@ -689,7 +699,7 @@ def calculate_bargraph_display(bardata, top, bar_widths, maxrow):
# build intermediate data structure
rows = [None] * maxrow
- def add_segment(seg_num, col, row, width, rows=rows):
+ def add_segment(seg_num: int, col: int, row: int, width: int, rows=rows) -> None:
if rows[row]:
last_seg, last_col, last_end = rows[row][-1]
if last_end > col:
@@ -832,13 +842,13 @@ class GraphVScale(Widget):
self.txt.append(Text(markup))
self.top = top
- def selectable(self):
+ def selectable(self) -> Literal[False]:
"""
Return False.
"""
return False
- def render(self, size, focus=False):
+ def render(self, size: tuple[int, int], focus: bool = False):
"""
Render GraphVScale.
"""
@@ -870,7 +880,7 @@ class GraphVScale(Widget):
-def scale_bar_values( bar, top, maxrow ):
+def scale_bar_values(bar, top, maxrow: int):
"""
Return a list of bar values aliased to integer values of maxrow.
"""
@@ -884,7 +894,7 @@ class ProgressBar(Widget):
text_align = CENTER
- def __init__(self, normal, complete, current=0, done=100, satt=None):
+ def __init__(self, normal, complete, current: int = 0, done: int = 100, satt=None):
"""
:param normal: display attribute for incomplete part of progress bar
:param complete: display attribute for complete part of progress bar
@@ -939,10 +949,10 @@ class ProgressBar(Widget):
self._invalidate()
done = property(lambda self: self._done, _set_done)
- def rows(self, size, focus=False):
+ def rows(self, size, focus: bool = False) -> int:
return 1
- def get_text(self):
+ def get_text(self) -> str:
"""
Return the progress bar percentage text.
You can override this method to display custom text.
@@ -950,7 +960,7 @@ class ProgressBar(Widget):
percent = min(100, max(0, int(self.current * 100 / self.done)))
return f"{str(percent)} %"
- def render(self, size, focus=False):
+ def render(self, size: tuple[int], focus: bool = False):
"""
Render the progress bar.
"""
@@ -993,7 +1003,7 @@ class ProgressBar(Widget):
class PythonLogo(Widget):
_sizing = frozenset([FIXED])
- def __init__(self):
+ def __init__(self) -> None:
"""
Create canvas containing an ASCII version of the Python
Logo and store it.
@@ -1008,22 +1018,24 @@ class PythonLogo(Widget):
(blu, " |__| "), (yel, "______|\n"),
(yel, " |____o_|")]).render((width,))
- def pack(self, size=None, focus=False):
+ def pack(self, size=None, focus: bool = False):
"""
Return the size from our pre-rendered canvas.
"""
return self._canvas.cols(), self._canvas.rows()
- def render(self, size, focus=False):
+ def render(self, size, focus: bool = False):
"""
Return the pre-rendered canvas.
"""
fixed_size(size)
return self._canvas
+
def _test():
import doctest
doctest.testmod()
+
if __name__=='__main__':
_test()
diff --git a/urwid/html_fragment.py b/urwid/html_fragment.py
index b14c57f..c5ce1f2 100755
--- a/urwid/html_fragment.py
+++ b/urwid/html_fragment.py
@@ -220,16 +220,16 @@ def screenshot_init( sizes, keys ):
"""
try:
for (row,col) in sizes:
- assert type(row) == int
- assert row>0 and col>0
+ assert isinstance(row, int)
+ assert row > 0 and col > 0
except (AssertionError, ValueError):
raise Exception("sizes must be in the form [ (col1,row1), (col2,row2), ...]")
try:
for l in keys:
- assert type(l) == list
+ assert isinstance(l, list)
for k in l:
- assert type(k) == str
+ assert isinstance(k, str)
except (AssertionError, ValueError):
raise Exception("keys must be in the form [ [keyA1, keyA2, ..], [keyB1, ..], ...]")
diff --git a/urwid/listbox.py b/urwid/listbox.py
index 4a5b5ae..d4dddc4 100644
--- a/urwid/listbox.py
+++ b/urwid/listbox.py
@@ -22,6 +22,9 @@
from __future__ import annotations
+import typing
+from collections.abc import MutableSequence
+
from urwid import signals
from urwid.canvas import CanvasCombine, SolidCanvas
from urwid.command_map import (
@@ -39,10 +42,16 @@ from urwid.signals import connect_signal, disconnect_signal
from urwid.util import is_mouse_press
from urwid.widget import BOX, GIVEN, Widget, nocache_widget_render_instance
+if typing.TYPE_CHECKING:
+ from typing_extensions import Literal
+
+ from urwid.canvas import CompositeCanvas
+
class ListWalkerError(Exception):
pass
+
class ListWalker(metaclass=signals.MetaSignals):
signals = ["modified"]
@@ -88,8 +97,9 @@ class ListWalker(metaclass=signals.MetaSignals):
except (IndexError, KeyError):
return None, None
+
class SimpleListWalker(MonitoredList, ListWalker):
- def __init__(self, contents, wrap_around=False):
+ def __init__(self, contents, wrap_around: bool = False):
"""
contents -- list to copy into this object
@@ -102,7 +112,7 @@ class SimpleListWalker(MonitoredList, ListWalker):
detected automatically and will cause ListBox objects using
this list walker to be updated.
"""
- if not getattr(contents, '__getitem__', None):
+ if not isinstance(contents, MutableSequence):
raise ListWalkerError(f"SimpleListWalker expecting list like object, got: {contents!r}")
MonitoredList.__init__(self, contents)
self.focus = 0
@@ -122,17 +132,16 @@ class SimpleListWalker(MonitoredList, ListWalker):
self.focus = max(0, len(self)-1)
ListWalker._modified(self)
- def set_modified_callback(self, callback):
+ def set_modified_callback(self, callback) -> typing.NoReturn:
"""
This function inherited from MonitoredList is not
implemented in SimpleListWalker.
Use connect_signal(list_walker, "modified", ...) instead.
"""
- raise NotImplementedError('Use connect_signal('
- 'list_walker, "modified", ...) instead.')
+ raise NotImplementedError('Use connect_signal(list_walker, "modified", ...) instead.')
- def set_focus(self, position):
+ def set_focus(self, position: int) -> None:
"""Set focus position."""
try:
if position < 0 or position >= len(self):
@@ -142,7 +151,7 @@ class SimpleListWalker(MonitoredList, ListWalker):
self.focus = position
self._modified()
- def next_position(self, position):
+ def next_position(self, position: int) -> int:
"""
Return position after start_from.
"""
@@ -152,7 +161,7 @@ class SimpleListWalker(MonitoredList, ListWalker):
raise IndexError
return position + 1
- def prev_position(self, position):
+ def prev_position(self, position: int) -> int:
"""
Return position before start_from.
"""
@@ -162,7 +171,7 @@ class SimpleListWalker(MonitoredList, ListWalker):
raise IndexError
return position - 1
- def positions(self, reverse=False):
+ def positions(self, reverse: bool = False):
"""
Optional method for returning an iterable of positions.
"""
@@ -189,28 +198,26 @@ class SimpleFocusListWalker(ListWalker, MonitoredFocusList):
normal list methods will cause the focus to be updated
intelligently.
"""
- if not getattr(contents, '__getitem__', None):
- raise ListWalkerError("SimpleFocusListWalker expecting list like "
- "object, got: %r"%(contents,))
+ if not isinstance(contents, MutableSequence):
+ raise ListWalkerError(f"SimpleFocusListWalker expecting list like object, got: {contents!r}")
MonitoredFocusList.__init__(self, contents)
self.wrap_around = wrap_around
- def set_modified_callback(self, callback):
+ def set_modified_callback(self, callback) -> typing.NoReturn:
"""
This function inherited from MonitoredList is not
implemented in SimpleFocusListWalker.
Use connect_signal(list_walker, "modified", ...) instead.
"""
- raise NotImplementedError('Use connect_signal('
- 'list_walker, "modified", ...) instead.')
+ raise NotImplementedError('Use connect_signal(list_walker, "modified", ...) instead.')
- def set_focus(self, position):
+ def set_focus(self, position: int) -> None:
"""Set focus position."""
self.focus = position
self._modified()
- def next_position(self, position):
+ def next_position(self, position: int) -> int:
"""
Return position after start_from.
"""
@@ -220,7 +227,7 @@ class SimpleFocusListWalker(ListWalker, MonitoredFocusList):
raise IndexError
return position + 1
- def prev_position(self, position):
+ def prev_position(self, position: int) -> int:
"""
Return position before start_from.
"""
@@ -230,7 +237,7 @@ class SimpleFocusListWalker(ListWalker, MonitoredFocusList):
raise IndexError
return position - 1
- def positions(self, reverse=False):
+ def positions(self, reverse: bool = False):
"""
Optional method for returning an iterable of positions.
"""
@@ -242,6 +249,7 @@ class SimpleFocusListWalker(ListWalker, MonitoredFocusList):
class ListBoxError(Exception):
pass
+
class ListBox(Widget, WidgetContainerMixin):
"""
a horizontally stacked list of widgets
@@ -276,7 +284,6 @@ class ListBox(Widget, WidgetContainerMixin):
# variable for delayed valign change used by set_focus_valign
self.set_focus_valign_pending = None
-
def _get_body(self):
return self._body
@@ -304,8 +311,7 @@ class ListBox(Widget, WidgetContainerMixin):
widgets to be displayed inside the list box
""")
-
- def calculate_visible(self, size, focus=False ):
+ def calculate_visible(self, size: tuple[int, int], focus: bool = False):
"""
Returns the widgets that would be displayed in
the ListBox given the current *size* and *focus*.
@@ -439,8 +445,7 @@ class ListBox(Widget, WidgetContainerMixin):
focus_pos, focus_rows, cursor ),
(trim_top, fill_above), (trim_bottom, fill_below))
-
- def render(self, size, focus=False ):
+ def render(self, size: tuple[int, int], focus: bool = False) -> CompositeCanvas | SolidCanvas:
"""
Render ListBox and return canvas.
@@ -459,21 +464,32 @@ class ListBox(Widget, WidgetContainerMixin):
combinelist = []
rows = 0
- fill_above.reverse() # fill_above is in bottom-up order
+ fill_above.reverse() # fill_above is in bottom-up order
for widget,w_pos,w_rows in fill_above:
canvas = widget.render((maxcol,))
if w_rows != canvas.rows():
- raise ListBoxError("Widget %r at position %r within listbox calculated %d rows but rendered %d!"% (widget,w_pos,w_rows, canvas.rows()))
+ raise ListBoxError(
+ f"Widget {widget!r} at position {w_pos!r} "
+ f"within listbox calculated {w_rows:d} rows "
+ f"but rendered {canvas.rows():d}!"
+ )
rows += w_rows
combinelist.append((canvas, w_pos, False))
focus_canvas = focus_widget.render((maxcol,), focus=focus)
if focus_canvas.rows() != focus_rows:
- raise ListBoxError("Focus Widget %r at position %r within listbox calculated %d rows but rendered %d!"% (focus_widget,focus_pos,focus_rows, focus_canvas.rows()))
+ raise ListBoxError(
+ f"Focus Widget {focus_widget!r} at position {focus_pos!r} "
+ f"within listbox calculated {focus_rows:d} rows "
+ f"but rendered {focus_canvas.rows():d}!"
+ )
c_cursor = focus_canvas.cursor
if cursor is not None and cursor != c_cursor:
- raise ListBoxError(f"Focus Widget {focus_widget!r} at position {focus_pos!r} within listbox calculated cursor coords {cursor!r} but rendered cursor coords {c_cursor!r}!")
+ raise ListBoxError(
+ f"Focus Widget {focus_widget!r} at position {focus_pos!r} "
+ f"within listbox calculated cursor coords {cursor!r} "
+ f"but rendered cursor coords {c_cursor!r}!")
rows += focus_rows
combinelist.append((focus_canvas, focus_pos, True))
@@ -481,7 +497,10 @@ class ListBox(Widget, WidgetContainerMixin):
for widget,w_pos,w_rows in fill_below:
canvas = widget.render((maxcol,))
if w_rows != canvas.rows():
- raise ListBoxError("Widget %r at position %r within listbox calculated %d rows but rendered %d!"% (widget,w_pos,w_rows, canvas.rows()))
+ raise ListBoxError(
+ f"Widget {widget!r} at position {w_pos!r} "
+ f"within listbox calculated {w_rows:d} "
+ f"rows but rendered {canvas.rows():d}!")
rows += w_rows
combinelist.append((canvas, w_pos, False))
@@ -495,19 +514,22 @@ class ListBox(Widget, WidgetContainerMixin):
rows -= trim_bottom
if rows > maxrow:
- raise ListBoxError(f"Listbox contents too long! Probably urwid's fault (please report): {top, middle, bottom!r}")
+ raise ListBoxError(
+ f"Listbox contents too long! Probably urwid's fault (please report): {top, middle, bottom!r}"
+ )
if rows < maxrow:
bottom_pos = focus_pos
if fill_below: bottom_pos = fill_below[-1][1]
if trim_bottom != 0 or self._body.get_next(bottom_pos) != (None,None):
- raise ListBoxError(f"Listbox contents too short! Probably urwid's fault (please report): {top, middle, bottom!r}")
+ raise ListBoxError(
+ f"Listbox contents too short! Probably urwid's fault (please report): {top, middle, bottom!r}"
+ )
final_canvas.pad_trim_top_bottom(0, maxrow - rows)
return final_canvas
-
- def get_cursor_coords(self, size):
+ def get_cursor_coords(self, size: tuple[int, int]) -> tuple[int, int] | None:
"""
See :meth:`Widget.get_cursor_coords` for details
"""
@@ -528,8 +550,10 @@ class ListBox(Widget, WidgetContainerMixin):
return None
return (x, y)
-
- def set_focus_valign(self, valign):
+ def set_focus_valign(
+ self,
+ valign: Literal['top', 'middle', 'bottom'] | tuple[Literal['fixed top', 'fixed bottom', 'relative'], int],
+ ):
"""Set the focus widget's display offset and inset.
:param valign: one of:
@@ -538,11 +562,10 @@ class ListBox(Widget, WidgetContainerMixin):
('fixed bottom', rows)
('relative', percentage 0=top 100=bottom)
"""
- vt, va = normalize_valign(valign,ListBoxError)
+ vt, va = normalize_valign(valign, ListBoxError)
self.set_focus_valign_pending = vt, va
-
- def set_focus(self, position, coming_from=None):
+ def set_focus(self, position, coming_from: Literal['above', 'below'] | None = None) -> None:
"""
Set the focus position and try to keep the old focus in view.
@@ -597,6 +620,7 @@ class ListBox(Widget, WidgetContainerMixin):
class ListBoxContents:
__getitem__ = self._contents__getitem__
return ListBoxContents()
+
def _contents__getitem__(self, key):
# try list walker protocol v2 first
getitem = getattr(self._body, '__getitem__', None)
@@ -640,13 +664,13 @@ class ListBox(Widget, WidgetContainerMixin):
"""
return None
- def _set_focus_valign_complete(self, size, focus):
+ def _set_focus_valign_complete(self, size: tuple[int, int], focus: bool) -> None:
"""
Finish setting the offset and inset now that we have have a
maxcol & maxrow.
"""
(maxcol, maxrow) = size
- vt,va = self.set_focus_valign_pending
+ vt, va = self.set_focus_valign_pending
self.set_focus_valign_pending = None
self.set_focus_pending = None
@@ -655,12 +679,11 @@ class ListBox(Widget, WidgetContainerMixin):
return
rows = focus_widget.rows((maxcol,), focus)
- rtop, rbot = calculate_top_bottom_filler(maxrow,
- vt, va, GIVEN, rows, None, 0, 0)
+ rtop, rbot = calculate_top_bottom_filler(maxrow, vt, va, GIVEN, rows, None, 0, 0)
self.shift_focus((maxcol, maxrow), rtop)
- def _set_focus_first_selectable(self, size, focus):
+ def _set_focus_first_selectable(self, size: tuple[int, int], focus: bool) -> None:
"""
Choose the first visible, selectable widget below the
current focus as the focus widget.
@@ -668,8 +691,7 @@ class ListBox(Widget, WidgetContainerMixin):
(maxcol, maxrow) = size
self.set_focus_valign_pending = None
self.set_focus_pending = None
- middle, top, bottom = self.calculate_visible(
- (maxcol, maxrow), focus=focus)
+ middle, top, bottom = self.calculate_visible((maxcol, maxrow), focus=focus)
if middle is None:
return
@@ -691,18 +713,16 @@ class ListBox(Widget, WidgetContainerMixin):
return
new_row_offset += rows
- def _set_focus_complete(self, size, focus):
+ def _set_focus_complete(self, size: tuple[int, int], focus: bool) -> None:
"""
Finish setting the position now that we have maxcol & maxrow.
"""
(maxcol, maxrow) = size
self._invalidate()
if self.set_focus_pending == "first selectable":
- return self._set_focus_first_selectable(
- (maxcol,maxrow), focus)
+ return self._set_focus_first_selectable((maxcol, maxrow), focus)
if self.set_focus_valign_pending is not None:
- return self._set_focus_valign_complete(
- (maxcol,maxrow), focus)
+ return self._set_focus_valign_complete((maxcol,maxrow), focus)
coming_from, focus_widget, focus_pos = self.set_focus_pending
self.set_focus_pending = None
@@ -715,8 +735,8 @@ class ListBox(Widget, WidgetContainerMixin):
# restore old focus temporarily
self._body.set_focus(focus_pos)
- middle,top,bottom=self.calculate_visible((maxcol,maxrow),focus)
- focus_offset, focus_widget, focus_pos, focus_rows, cursor=middle
+ middle, top, bottom=self.calculate_visible((maxcol,maxrow), focus)
+ focus_offset, focus_widget, focus_pos, focus_rows, cursor = middle
trim_top, fill_above = top
trim_bottom, fill_below = bottom
@@ -724,15 +744,13 @@ class ListBox(Widget, WidgetContainerMixin):
for widget, pos, rows in fill_above:
offset -= rows
if pos == position:
- self.change_focus((maxcol, maxrow), pos,
- offset, 'below' )
+ self.change_focus((maxcol, maxrow), pos, offset, 'below')
return
offset = focus_offset + focus_rows
for widget, pos, rows in fill_below:
if pos == position:
- self.change_focus((maxcol, maxrow), pos,
- offset, 'above' )
+ self.change_focus((maxcol, maxrow), pos, offset, 'above')
return
offset += rows
@@ -741,16 +759,15 @@ class ListBox(Widget, WidgetContainerMixin):
widget, position = self._body.get_focus()
rows = widget.rows((maxcol,), focus)
- if coming_from=='below':
+ if coming_from == 'below':
offset = 0
- elif coming_from=='above':
+ elif coming_from == 'above':
offset = maxrow-rows
else:
offset = (maxrow-rows) // 2
self.shift_focus((maxcol, maxrow), offset)
-
- def shift_focus(self, size, offset_inset):
+ def shift_focus(self, size: tuple[int, int], offset_inset: int) -> None:
"""
Move the location of the current focus relative to the top.
This is used internally by methods that know the widget's *size*.
@@ -782,7 +799,7 @@ class ListBox(Widget, WidgetContainerMixin):
self.inset_fraction = (-offset_inset,tgt_rows)
self._invalidate()
- def update_pref_col_from_focus(self, size):
+ def update_pref_col_from_focus(self, size: tuple[int, int]):
"""Update self.pref_col from the focus widget."""
# TODO: should this not be private?
(maxcol, maxrow) = size
@@ -795,15 +812,20 @@ class ListBox(Widget, WidgetContainerMixin):
pref_col = widget.get_pref_col((maxcol,))
if pref_col is None and hasattr(widget,'get_cursor_coords'):
coords = widget.get_cursor_coords((maxcol,))
- if type(coords) == tuple:
- pref_col,y = coords
+ if isinstance(coords, tuple):
+ pref_col, y = coords
if pref_col is not None:
self.pref_col = pref_col
-
- def change_focus(self, size, position,
- offset_inset = 0, coming_from = None,
- cursor_coords = None, snap_rows = None):
+ def change_focus(
+ self,
+ size: tuple[int, int],
+ position,
+ offset_inset: int = 0,
+ coming_from: Literal['above', 'below'] | None = None,
+ cursor_coords: tuple[int, int] | None = None,
+ snap_rows: int | None = None,
+ ) -> None:
"""
Change the current focus widget.
This is used internally by methods that know the widget's *size*.
@@ -915,7 +937,7 @@ class ListBox(Widget, WidgetContainerMixin):
if target.move_cursor_to_coords((maxcol,),pref_col,row):
break
- def get_focus_offset_inset(self, size):
+ def get_focus_offset_inset(self, size: tuple[int, int]) -> tuple[int, int]:
"""Return (offset rows, inset rows) for focus widget."""
(maxcol, maxrow) = size
focus_widget, pos = self._body.get_focus()
@@ -931,8 +953,7 @@ class ListBox(Widget, WidgetContainerMixin):
raise ListBoxError("urwid inset_fraction error (please report)")
return offset_rows, inset_rows
-
- def make_cursor_visible(self, size):
+ def make_cursor_visible(self, size: tuple[int, int]) -> None:
"""Shift the focus widget so that its cursor is visible."""
(maxcol, maxrow) = size
@@ -941,14 +962,13 @@ class ListBox(Widget, WidgetContainerMixin):
return
if not focus_widget.selectable():
return
- if not hasattr(focus_widget,'get_cursor_coords'):
+ if not hasattr(focus_widget, 'get_cursor_coords'):
return
cursor = focus_widget.get_cursor_coords((maxcol,))
if cursor is None:
return
cx, cy = cursor
- offset_rows, inset_rows = self.get_focus_offset_inset(
- (maxcol, maxrow))
+ offset_rows, inset_rows = self.get_focus_offset_inset((maxcol, maxrow))
if cy < inset_rows:
self.shift_focus( (maxcol,maxrow), - (cy) )
@@ -958,8 +978,7 @@ class ListBox(Widget, WidgetContainerMixin):
self.shift_focus( (maxcol,maxrow), maxrow-cy-1 )
return
-
- def keypress(self, size, key):
+ def keypress(self, size: tuple[int, int], key: str) -> str | None:
"""Move selection through the list elements scrolling when
necessary. Keystrokes are first passed to widget in focus
in case that widget can handle them.
@@ -973,7 +992,7 @@ class ListBox(Widget, WidgetContainerMixin):
(maxcol, maxrow) = size
if self.set_focus_pending or self.set_focus_valign_pending:
- self._set_focus_complete( (maxcol,maxrow), focus=True )
+ self._set_focus_complete( (maxcol,maxrow), focus=True)
focus_widget, pos = self._body.get_focus()
if focus_widget is None: # empty listbox, can't do anything
@@ -1010,22 +1029,22 @@ class ListBox(Widget, WidgetContainerMixin):
return key
- def _keypress_max_left(self, size):
+ def _keypress_max_left(self, size: tuple[int, int]) -> bool:
self.focus_position = next(iter(self.body.positions()))
self.set_focus_valign('top')
return True
- def _keypress_max_right(self, size):
+ def _keypress_max_right(self, size: tuple[int, int]) -> bool:
self.focus_position = next(iter(self.body.positions(reverse=True)))
self.set_focus_valign('bottom')
return True
- def _keypress_up(self, size):
+ def _keypress_up(self, size: tuple[int, int]) -> bool | None:
(maxcol, maxrow) = size
- middle, top, bottom = self.calculate_visible(
- (maxcol,maxrow), True)
- if middle is None: return True
+ middle, top, bottom = self.calculate_visible((maxcol,maxrow), True)
+ if middle is None:
+ return True
focus_row_offset,focus_widget,focus_pos,_ignore,cursor = middle
trim_top, fill_above = top
@@ -1039,8 +1058,7 @@ class ListBox(Widget, WidgetContainerMixin):
row_offset -= rows
if rows and widget.selectable():
# this one will do
- self.change_focus((maxcol,maxrow), pos,
- row_offset, 'below')
+ self.change_focus((maxcol,maxrow), pos, row_offset, 'below')
return
# at this point we must scroll
@@ -1067,8 +1085,7 @@ class ListBox(Widget, WidgetContainerMixin):
if widget is None:
self.shift_focus((maxcol,maxrow), row_offset)
return
- self.change_focus((maxcol,maxrow), pos,
- row_offset, 'below')
+ self.change_focus((maxcol,maxrow), pos, row_offset, 'below')
return
# check if cursor will stop scroll from taking effect
@@ -1089,20 +1106,18 @@ class ListBox(Widget, WidgetContainerMixin):
# must scroll further than 1 line
row_offset = - (rows-1)
- self.change_focus((maxcol,maxrow),pos,
- row_offset, 'below')
+ self.change_focus((maxcol,maxrow),pos,row_offset, 'below')
return
# if all else fails, just shift the current focus.
self.shift_focus((maxcol,maxrow), focus_row_offset+1)
-
- def _keypress_down(self, size):
+ def _keypress_down(self, size: tuple[int, int]) -> bool | None:
(maxcol, maxrow) = size
- middle, top, bottom = self.calculate_visible(
- (maxcol,maxrow), True)
- if middle is None: return True
+ middle, top, bottom = self.calculate_visible((maxcol,maxrow), True)
+ if middle is None:
+ return True
focus_row_offset,focus_widget,focus_pos,focus_rows,cursor=middle
trim_bottom, fill_below = bottom
@@ -1116,8 +1131,7 @@ class ListBox(Widget, WidgetContainerMixin):
for widget, pos, rows in fill_below:
if rows and widget.selectable():
# this one will do
- self.change_focus((maxcol,maxrow), pos,
- row_offset, 'above')
+ self.change_focus((maxcol,maxrow), pos, row_offset, 'above')
return
row_offset += rows
@@ -1134,8 +1148,7 @@ class ListBox(Widget, WidgetContainerMixin):
rows = widget.rows((maxcol,))
if rows and widget.selectable():
# this one will do
- self.change_focus((maxcol,maxrow), pos,
- row_offset, 'above')
+ self.change_focus((maxcol,maxrow), pos, row_offset, 'above')
return
row_offset += rows
@@ -1143,14 +1156,12 @@ class ListBox(Widget, WidgetContainerMixin):
# just take bottom one if current is not selectable
# or if focus has moved out of view
if widget is None:
- self.shift_focus((maxcol,maxrow),
- row_offset-rows)
+ self.shift_focus((maxcol,maxrow),row_offset-rows)
return
# FIXME: catch this bug in testcase
#self.change_focus((maxcol,maxrow), pos,
# row_offset+rows, 'above')
- self.change_focus((maxcol,maxrow), pos,
- row_offset-rows, 'above')
+ self.change_focus((maxcol,maxrow), pos,row_offset-rows, 'above')
return
# check if cursor will stop scroll from taking effect
@@ -1178,13 +1189,13 @@ class ListBox(Widget, WidgetContainerMixin):
# if all else fails, keep the current focus.
self.shift_focus((maxcol,maxrow), focus_row_offset-1)
-
- def _keypress_page_up(self, size):
+ def _keypress_page_up(self, size: tuple[int, int]) -> bool | None:
(maxcol, maxrow) = size
middle, top, bottom = self.calculate_visible(
(maxcol,maxrow), True)
- if middle is None: return True
+ if middle is None:
+ return True
row_offset, focus_widget, focus_pos, focus_rows, cursor = middle
trim_top, fill_above = top
@@ -1218,7 +1229,6 @@ class ListBox(Widget, WidgetContainerMixin):
# not used below:
scroll_from_row = topmost_visible = None
-
# gather potential target widgets
t = []
# add current focus
@@ -1257,8 +1267,7 @@ class ListBox(Widget, WidgetContainerMixin):
# choose the topmost selectable and (newly) visible widget
# search within snap_rows then visible region
- search_order = (list(range(snap_region_start, len(t)))
- + list(range(snap_region_start-1, -1, -1)))
+ search_order = (list(range(snap_region_start, len(t))) + list(range(snap_region_start-1, -1, -1)))
#assert 0, repr((t, search_order))
bad_choices = []
cut_off_selectable_chosen = 0
@@ -1275,23 +1284,30 @@ class ListBox(Widget, WidgetContainerMixin):
# if completely within snap region, adjust row_offset
if rows + row_offset <= 0:
- self.change_focus( (maxcol,maxrow), pos,
- -(rows-1), 'below',
+ self.change_focus(
+ (maxcol,maxrow),
+ pos,
+ -(rows-1),
+ 'below',
(self.pref_col, rows-1),
- snap_rows-((-row_offset)-(rows-1)))
+ snap_rows-((-row_offset)-(rows-1)),
+ )
else:
- self.change_focus( (maxcol,maxrow), pos,
- row_offset, 'below',
- (self.pref_col, pref_row), snap_rows )
+ self.change_focus(
+ (maxcol,maxrow),
+ pos,
+ row_offset,
+ 'below',
+ (self.pref_col, pref_row),
+ snap_rows,
+ )
# if we're as far up as we can scroll, take this one
- if (fill_above and self._body.get_prev(fill_above[-1][1])
- == (None,None) ):
+ if fill_above and self._body.get_prev(fill_above[-1][1]) == (None, None):
pass #return
# find out where that actually puts us
- middle, top, bottom = self.calculate_visible(
- (maxcol,maxrow), True)
+ middle, top, bottom = self.calculate_visible((maxcol,maxrow), True)
act_row_offset, _ign1, _ign2, _ign3, _ign4 = middle
# discard chosen widget if it will reduce scroll amount
@@ -1360,16 +1376,14 @@ class ListBox(Widget, WidgetContainerMixin):
return
# bring in only one row if possible
rows = widget.rows((maxcol,), True)
- self.change_focus((maxcol,maxrow), pos, -(rows-1),
- 'below', (self.pref_col, rows-1), 0 )
-
+ self.change_focus((maxcol,maxrow), pos, -(rows-1), 'below', (self.pref_col, rows-1), 0 )
- def _keypress_page_down(self, size):
+ def _keypress_page_down(self, size: tuple[int, int]) -> bool | None:
(maxcol, maxrow) = size
- middle, top, bottom = self.calculate_visible(
- (maxcol,maxrow), True)
- if middle is None: return True
+ middle, top, bottom = self.calculate_visible((maxcol,maxrow), True)
+ if middle is None:
+ return True
row_offset, focus_widget, focus_pos, focus_rows, cursor = middle
trim_bottom, fill_below = bottom
@@ -1402,7 +1416,6 @@ class ListBox(Widget, WidgetContainerMixin):
# not used below:
scroll_from_row = bottom_edge = None
-
# gather potential target widgets
t = []
# add current focus
@@ -1442,8 +1455,7 @@ class ListBox(Widget, WidgetContainerMixin):
# choose the bottommost selectable and (newly) visible widget
# search within snap_rows then visible region
- search_order = (list(range(snap_region_start, len(t)))
- + list(range(snap_region_start-1, -1, -1)))
+ search_order = (list(range(snap_region_start, len(t))) + list(range(snap_region_start-1, -1, -1)))
#assert 0, repr((t, search_order))
bad_choices = []
cut_off_selectable_chosen = 0
@@ -1460,18 +1472,26 @@ class ListBox(Widget, WidgetContainerMixin):
# if completely within snap region, adjust row_offset
if row_offset >= maxrow:
- self.change_focus( (maxcol,maxrow), pos,
- maxrow-1, 'above',
+ self.change_focus(
+ (maxcol,maxrow),
+ pos,
+ maxrow-1,
+ 'above',
(self.pref_col, 0),
- snap_rows+maxrow-row_offset-1 )
+ snap_rows+maxrow-row_offset-1,
+ )
else:
- self.change_focus( (maxcol,maxrow), pos,
- row_offset, 'above',
- (self.pref_col, pref_row), snap_rows )
+ self.change_focus(
+ (maxcol,maxrow),
+ pos,
+ row_offset,
+ 'above',
+ (self.pref_col, pref_row),
+ snap_rows,
+ )
# find out where that actually puts us
- middle, top, bottom = self.calculate_visible(
- (maxcol,maxrow), True)
+ middle, top, bottom = self.calculate_visible((maxcol,maxrow), True)
act_row_offset, _ign1, _ign2, _ign3, _ign4 = middle
# discard chosen widget if it will reduce scroll amount
@@ -1499,7 +1519,8 @@ class ListBox(Widget, WidgetContainerMixin):
good_choices = [j for j in search_order if j not in bad_choices]
for i in good_choices + search_order:
row_offset, widget, pos, rows = t[i]
- if pos == focus_pos: continue
+ if pos == focus_pos:
+ continue
if not rows: # never focus a 0-height widget
continue
@@ -1514,7 +1535,6 @@ class ListBox(Widget, WidgetContainerMixin):
snap_rows )
return
-
# no choices available, just shift current one
self.shift_focus((maxcol, maxrow), max(1-focus_rows,row_offset))
@@ -1536,10 +1556,9 @@ class ListBox(Widget, WidgetContainerMixin):
return
# bring in only one row if possible
rows = widget.rows((maxcol,), True)
- self.change_focus((maxcol,maxrow), pos, maxrow-1,
- 'above', (self.pref_col, 0), 0 )
+ self.change_focus((maxcol,maxrow), pos, maxrow-1, 'above', (self.pref_col, 0), 0 )
- def mouse_event(self, size, event, button, col, row, focus):
+ def mouse_event(self, size: tuple[int, int], event, button: int, col: int, row: int, focus: bool) -> bool | None:
"""
Pass the event to the contained widgets.
May change focus on button 1 press.
@@ -1554,10 +1573,8 @@ class ListBox(Widget, WidgetContainerMixin):
trim_top, fill_above = top
_ignore, fill_below = bottom
- fill_above.reverse() # fill_above is in bottom-up order
- w_list = ( fill_above +
- [ (focus_widget, focus_pos, focus_rows) ] +
- fill_below )
+ fill_above.reverse() # fill_above is in bottom-up order
+ w_list = ( fill_above + [(focus_widget, focus_pos, focus_rows)] + fill_below)
wrow = -trim_top
for w, w_pos, w_rows in w_list:
@@ -1575,11 +1592,9 @@ class ListBox(Widget, WidgetContainerMixin):
if not hasattr(w,'mouse_event'):
return False
- return w.mouse_event((maxcol,), event, button, col, row-wrow,
- focus)
+ return w.mouse_event((maxcol,), event, button, col, row-wrow,focus)
-
- def ends_visible(self, size, focus=False):
+ def ends_visible(self, size: tuple[int, int], focus: bool = False):
"""
Return a list that may contain ``'top'`` and/or ``'bottom'``.
@@ -1674,4 +1689,5 @@ class ListBox(Widget, WidgetContainerMixin):
while True:
yield pos
w, pos = self._body.get_next(pos)
- if not w: break
+ if not w:
+ break
diff --git a/urwid/main_loop.py b/urwid/main_loop.py
index 0d91a60..8cee637 100755
--- a/urwid/main_loop.py
+++ b/urwid/main_loop.py
@@ -507,8 +507,7 @@ class MainLoop:
if is_mouse_event(k):
event, button, col, row = k
if hasattr(self._topmost_widget, "mouse_event"):
- if self._topmost_widget.mouse_event(self.screen_size,
- event, button, col, row, focus=True):
+ if self._topmost_widget.mouse_event(self.screen_size, event, button, col, row, focus=True):
k = None
elif self._topmost_widget.selectable():
k = self._topmost_widget.keypress(self.screen_size, k)
@@ -1185,7 +1184,7 @@ class TwistedInputDescriptor(FileDescriptor):
def __init__(self, reactor, fd, cb):
self._fileno = fd
self.cb = cb
- FileDescriptor.__init__(self, reactor)
+ super().__init__(reactor)
def fileno(self):
return self._fileno
diff --git a/urwid/monitored_list.py b/urwid/monitored_list.py
index 8a04615..13700c8 100755
--- a/urwid/monitored_list.py
+++ b/urwid/monitored_list.py
@@ -19,19 +19,32 @@
#
# Urwid web site: https://urwid.org/
+from __future__ import annotations
-def _call_modified(fn):
- def call_modified_wrapper(self, *args, **kwargs):
+import typing
+from collections.abc import Callable
+
+if typing.TYPE_CHECKING:
+ from typing_extensions import ParamSpec
+
+ ArgSpec = ParamSpec("ArgSpec")
+ Ret = typing.TypeVar("Ret")
+
+
+def _call_modified(fn: Callable[ArgSpec, Ret]) -> Callable[ArgSpec, Ret]:
+ def call_modified_wrapper(self: MonitoredList, *args, **kwargs):
rval = fn(self, *args, **kwargs)
self._modified()
return rval
return call_modified_wrapper
+
class MonitoredList(list):
"""
This class can trigger a callback any time its contents are changed
with the usual list operations append, extend, etc.
"""
+
def _modified(self):
pass
@@ -116,7 +129,7 @@ class MonitoredFocusList(MonitoredList):
def __repr__(self):
return f"{self.__class__.__name__}({list(self)!r}, focus={self.focus!r})"
- def _get_focus(self):
+ def _get_focus(self) -> int | None:
"""
Return the index of the item "in focus" or None if
the list is empty.
@@ -129,7 +142,7 @@ class MonitoredFocusList(MonitoredList):
return None
return self._focus
- def _set_focus(self, index):
+ def _set_focus(self, index: int) -> None:
"""
index -- index into this list, any index out of range will
raise an IndexError, except when the list is empty and
@@ -169,10 +182,10 @@ class MonitoredFocusList(MonitoredList):
or an IndexError will be raised.
""")
- def _focus_changed(self, new_focus):
+ def _focus_changed(self, new_focus: int):
pass
- def set_focus_changed_callback(self, callback):
+ def set_focus_changed_callback(self, callback: Callable[[int], typing.Any]) -> None:
"""
Assign a callback to be called when the focus index changes
for any reason. The callback is in the form:
@@ -222,7 +235,7 @@ class MonitoredFocusList(MonitoredList):
"""
self._validate_contents_modified = callback
- def _adjust_focus_on_contents_modified(self, slc, new_items=()):
+ def _adjust_focus_on_contents_modified(self, slc: slice, new_items=()) -> int:
"""
Default behaviour is to move the focus to the item following
any removed items, unless that item was simply replaced.
@@ -260,7 +273,7 @@ class MonitoredFocusList(MonitoredList):
# override all the list methods that modify the list
- def __delitem__(self, y):
+ def __delitem__(self, y: int | slice):
"""
>>> ml = MonitoredFocusList([0,1,2,3,4], focus=2)
>>> del ml[3]; ml
@@ -289,13 +302,12 @@ class MonitoredFocusList(MonitoredList):
if isinstance(y, slice):
focus = self._adjust_focus_on_contents_modified(y)
else:
- focus = self._adjust_focus_on_contents_modified(slice(y,
- y+1 or None))
+ focus = self._adjust_focus_on_contents_modified(slice(y, y+1 or None))
rval = super().__delitem__(y)
self._set_focus(focus)
return rval
- def __setitem__(self, i, y):
+ def __setitem__(self, i: int | slice, y):
"""
>>> def modified(indices, new_items):
... print("range%r <- %r" % (indices, new_items))
@@ -332,7 +344,7 @@ class MonitoredFocusList(MonitoredList):
self._set_focus(focus)
return rval
- def __imul__(self, n):
+ def __imul__(self, n: int):
"""
>>> def modified(indices, new_items):
... print("range%r <- %r" % (indices, list(new_items)))
@@ -348,9 +360,8 @@ class MonitoredFocusList(MonitoredList):
None
"""
if n > 0:
- focus = self._adjust_focus_on_contents_modified(
- slice(len(self), len(self)), list(self)*(n-1))
- else: # all contents are being removed
+ focus = self._adjust_focus_on_contents_modified(slice(len(self), len(self)), list(self) * (n-1))
+ else: # all contents are being removed
focus = self._adjust_focus_on_contents_modified(slice(0, len(self)))
rval = super().__imul__(n)
self._set_focus(focus)
@@ -386,7 +397,7 @@ class MonitoredFocusList(MonitoredList):
self._set_focus(focus)
return rval
- def insert(self, index, item):
+ def insert(self, index: int, item):
"""
>>> ml = MonitoredFocusList([0,1,2,3], focus=2)
>>> ml.insert(-1, -1); ml
@@ -396,13 +407,12 @@ class MonitoredFocusList(MonitoredList):
>>> ml.insert(3, -3); ml
MonitoredFocusList([-2, 0, 1, -3, 2, -1, 3], focus=4)
"""
- focus = self._adjust_focus_on_contents_modified(slice(index, index),
- [item])
+ focus = self._adjust_focus_on_contents_modified(slice(index, index), [item])
rval = super().insert(index, item)
self._set_focus(focus)
return rval
- def pop(self, index=-1):
+ def pop(self, index: int = -1):
"""
>>> ml = MonitoredFocusList([-2,0,1,-3,2,3], focus=4)
>>> ml.pop(3); ml
@@ -418,8 +428,7 @@ class MonitoredFocusList(MonitoredList):
2
MonitoredFocusList([0, 1], focus=1)
"""
- focus = self._adjust_focus_on_contents_modified(slice(index,
- index+1 or None))
+ focus = self._adjust_focus_on_contents_modified(slice(index, index + 1 or None))
rval = super().pop(index)
self._set_focus(focus)
return rval
@@ -472,13 +481,11 @@ class MonitoredFocusList(MonitoredList):
return rval
-
-
-
def _test():
import doctest
doctest.testmod()
+
if __name__=='__main__':
_test()
diff --git a/urwid/numedit.py b/urwid/numedit.py
index ff372d5..a2cf32d 100644
--- a/urwid/numedit.py
+++ b/urwid/numedit.py
@@ -22,6 +22,7 @@
from __future__ import annotations
import re
+from collections.abc import Container
from decimal import Decimal
from urwid import Edit
@@ -39,18 +40,24 @@ class NumEdit(Edit):
"""
ALLOWED = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
- def __init__(self, allowed, caption, default, trimLeadingZeros=True):
+ def __init__(
+ self,
+ allowed: Container[str],
+ caption,
+ default: str | bytes,
+ trimLeadingZeros: bool = True,
+ ):
super().__init__(caption, default)
self._allowed = allowed
self.trimLeadingZeros = trimLeadingZeros
- def valid_char(self, ch):
+ def valid_char(self, ch: str) -> bool:
"""
Return true for allowed characters.
"""
return len(ch) == 1 and ch.upper() in self._allowed
- def keypress(self, size, key):
+ def keypress(self, size: tuple[int], key: str) -> str | None:
"""
Handle editing keystrokes. Remove leading zeros.
@@ -71,12 +78,11 @@ class NumEdit(Edit):
(maxcol,) = size
unhandled = Edit.keypress(self, (maxcol,), key)
- if not unhandled:
- if self.trimLeadingZeros:
- # trim leading zeros
- while self.edit_pos > 0 and self.edit_text[:1] == "0":
- self.set_edit_pos(self.edit_pos - 1)
- self.set_edit_text(self.edit_text[1:])
+ if not unhandled and self.trimLeadingZeros:
+ # trim leading zeros
+ while self.edit_pos > 0 and self.edit_text[:1] == "0":
+ self.set_edit_pos(self.edit_pos - 1)
+ self.set_edit_text(self.edit_text[1:])
return unhandled
@@ -84,7 +90,12 @@ class NumEdit(Edit):
class IntegerEdit(NumEdit):
"""Edit widget for integer values"""
- def __init__(self, caption="", default=None, base=10):
+ def __init__(
+ self,
+ caption="",
+ default: int | str | Decimal | None = None,
+ base: int = 10,
+ ) -> None:
"""
caption -- caption markup
default -- default edit value
@@ -144,8 +155,7 @@ class IntegerEdit(NumEdit):
allowed_chars = self.ALLOWED[:self.base]
if default is not None:
if not isinstance(default, (int, str, Decimal)):
- raise ValueError("default: Only 'str', 'int', "
- "'long' or Decimal input allowed")
+ raise ValueError("default: Only 'str', 'int' or Decimal input allowed")
# convert to a long first, this will raise a ValueError
# in case a float is passed or some other error
@@ -163,10 +173,9 @@ class IntegerEdit(NumEdit):
# convert possible int, long or Decimal to str
val = str(default)
- super().__init__(allowed_chars, caption, val,
- trimLeadingZeros=(self.base == 10))
+ super().__init__(allowed_chars, caption, val, trimLeadingZeros=(self.base == 10))
- def value(self):
+ def value(self) -> Decimal | None:
"""
Return the numeric value of self.edit_text.
@@ -184,8 +193,13 @@ class IntegerEdit(NumEdit):
class FloatEdit(NumEdit):
"""Edit widget for float values."""
- def __init__(self, caption="", default=None,
- preserveSignificance=True, decimalSeparator='.'):
+ def __init__(
+ self,
+ caption="",
+ default: str | int | Decimal | None = None,
+ preserveSignificance: bool = True,
+ decimalSeparator: str = '.',
+ ) -> None:
"""
caption -- caption markup
default -- default edit value
@@ -244,8 +258,7 @@ class FloatEdit(NumEdit):
val = ""
if default is not None and default != "":
if not isinstance(default, (int, str, Decimal)):
- raise ValueError("default: Only 'str', 'int', "
- "'long' or Decimal input allowed")
+ raise ValueError("default: Only 'str', 'int' or Decimal input allowed")
if isinstance(default, str) and default:
# check if it is a float, raises a ValueError otherwise
@@ -260,7 +273,7 @@ class FloatEdit(NumEdit):
super().__init__(self.ALLOWED[0:10] + decimalSeparator,
caption, val)
- def value(self):
+ def value(self) -> Decimal | None:
"""
Return the numeric value of self.edit_text.
"""
diff --git a/urwid/old_str_util.py b/urwid/old_str_util.py
index 920b4a8..3a9bd84 100755
--- a/urwid/old_str_util.py
+++ b/urwid/old_str_util.py
@@ -23,6 +23,10 @@
from __future__ import annotations
import re
+import typing
+
+if typing.TYPE_CHECKING:
+ from typing_extensions import Literal
SAFE_ASCII_RE = re.compile("^[ -~]*$")
SAFE_ASCII_BYTES_RE = re.compile(b"^[ -~]*$")
@@ -33,7 +37,7 @@ _byte_encoding = None
# generated from
# http://www.unicode.org/Public/4.0-Update/EastAsianWidth-4.0.0.txt
-widths = [
+widths: list[tuple[int, Literal[0, 1, 2]]] = [
(126, 1),
(159, 0),
(687, 1),
@@ -76,7 +80,8 @@ widths = [
# ACCESSOR FUNCTIONS
-def get_width( o ):
+
+def get_width(o) -> Literal[0, 1, 2]:
"""Return the screen column width for unicode ordinal o."""
global widths
if o == 0xe or o == 0xf:
@@ -86,7 +91,8 @@ def get_width( o ):
return wid
return 1
-def decode_one( text, pos ):
+
+def decode_one(text: bytes, pos: int) -> tuple[int, int]:
"""
Return (ordinal at pos, next position) for UTF-8 encoded text.
"""
@@ -138,13 +144,15 @@ def decode_one( text, pos ):
return o, pos+4
return error
-def decode_one_uni(text, i):
+
+def decode_one_uni(text: str, i: int) -> tuple[int, int]:
"""
decode_one implementation for unicode strings
"""
return ord(text[i]), i+1
-def decode_one_right(text, pos):
+
+def decode_one_right(text: bytes, pos: int) -> tuple[int, int]:
"""
Return (ordinal at pos, next position) for UTF-8 encoded text.
pos is assumed to be on the trailing byte of a utf-8 sequence.
@@ -160,15 +168,18 @@ def decode_one_right(text, pos):
if p == p-4:
return error
-def set_byte_encoding(enc):
+
+def set_byte_encoding(enc: Literal['utf8', 'narrow', 'wide']):
assert enc in ('utf8', 'narrow', 'wide')
global _byte_encoding
_byte_encoding = enc
+
def get_byte_encoding():
return _byte_encoding
-def calc_text_pos(text, start_offs, end_offs, pref_col):
+
+def calc_text_pos(text: str | bytes, start_offs: int, end_offs: int, pref_col: int) -> tuple[int, int]:
"""
Calculate the closest position to the screen column pref_col in text
where start_offs is the offset into text assumed to be screen column 0
@@ -180,12 +191,12 @@ def calc_text_pos(text, start_offs, end_offs, pref_col):
"""
assert start_offs <= end_offs, repr((start_offs, end_offs))
utfs = isinstance(text, bytes) and _byte_encoding == "utf8"
- unis = not isinstance(text, bytes)
+ unis = isinstance(text, str)
if unis or utfs:
decode = [decode_one, decode_one_uni][unis]
i = start_offs
sc = 0
- n = 1 # number to advance by
+ n = 1 # number to advance by
while i < end_offs:
o, n = decode(text, i)
w = get_width(o)
@@ -194,7 +205,8 @@ def calc_text_pos(text, start_offs, end_offs, pref_col):
i = n
sc += w
return i, sc
- assert type(text) == bytes, repr(text)
+
+ assert isinstance(text, bytes), repr(text)
# "wide" and "narrow"
i = start_offs+pref_col
if i >= end_offs:
@@ -204,7 +216,8 @@ def calc_text_pos(text, start_offs, end_offs, pref_col):
i -= 1
return i, i-start_offs
-def calc_width(text, start_offs, end_offs):
+
+def calc_width(text: str | bytes, start_offs: int, end_offs: int) -> int:
"""
Return the screen column width of text between start_offs and end_offs.
@@ -219,8 +232,7 @@ def calc_width(text, start_offs, end_offs):
utfs = isinstance(text, bytes) and _byte_encoding == "utf8"
unis = not isinstance(text, bytes)
- if (unis and not SAFE_ASCII_RE.match(text)
- ) or (utfs and not SAFE_ASCII_BYTES_RE.match(text)):
+ if (unis and not SAFE_ASCII_RE.match(text)) or (utfs and not SAFE_ASCII_BYTES_RE.match(text)):
decode = [decode_one, decode_one_uni][unis]
i = start_offs
sc = 0
@@ -234,7 +246,8 @@ def calc_width(text, start_offs, end_offs):
# "wide", "narrow" or all printable ASCII, just return the character count
return end_offs - start_offs
-def is_wide_char(text, offs):
+
+def is_wide_char(text: str | bytes, offs: int) -> bool:
"""
Test if the character at offs within text is wide.
@@ -251,7 +264,8 @@ def is_wide_char(text, offs):
return within_double_byte(text, offs, offs) == 1
return False
-def move_prev_char(text, start_offs, end_offs):
+
+def move_prev_char(text: str | bytes, start_offs: int, end_offs: int):
"""
Return the position of the character before end_offs.
"""
@@ -269,7 +283,8 @@ def move_prev_char(text, start_offs, end_offs):
return end_offs-2
return end_offs-1
-def move_next_char(text, start_offs, end_offs):
+
+def move_next_char(text: str | bytes, start_offs: int, end_offs: int) -> int:
"""
Return the position of the character after start_offs.
"""
@@ -279,15 +294,15 @@ def move_next_char(text, start_offs, end_offs):
assert isinstance(text, bytes)
if _byte_encoding == "utf8":
o = start_offs+1
- while o<end_offs and text[o]&0xc0 == 0x80:
+ while o < end_offs and text[o] & 0xc0 == 0x80:
o += 1
return o
- if _byte_encoding == "wide" and within_double_byte(text,
- start_offs, start_offs) == 1:
+ if _byte_encoding == "wide" and within_double_byte(text, start_offs, start_offs) == 1:
return start_offs +2
return start_offs+1
-def within_double_byte(text, line_start, pos):
+
+def within_double_byte(text: bytes, line_start: int, pos: int) -> Literal[0, 1, 2]:
"""Return whether pos is within a double-byte encoded character.
text -- byte string in question
@@ -304,16 +319,18 @@ def within_double_byte(text, line_start, pos):
if v >= 0x40 and v < 0x7f:
# might be second half of big5, uhc or gbk encoding
- if pos == line_start: return 0
+ if pos == line_start:
+ return 0
if text[pos-1] >= 0x81:
if within_double_byte(text, line_start, pos-1) == 1:
return 2
return 0
- if v < 0x80: return 0
+ if v < 0x80:
+ return 0
- i = pos -1
+ i = pos - 1
while i >= line_start:
if text[i] < 0x80:
break
@@ -323,9 +340,10 @@ def within_double_byte(text, line_start, pos):
return 1
return 2
+
# TABLE GENERATION CODE
-def process_east_asian_width():
+def process_east_asian_width() -> None:
import sys
out = []
last = None
@@ -362,6 +380,7 @@ def process_east_asian_width():
print(f"\t{o!r},")
print("]")
+
if __name__ == "__main__":
process_east_asian_width()
diff --git a/urwid/raw_display.py b/urwid/raw_display.py
index 7d90f3b..bbdf758 100644
--- a/urwid/raw_display.py
+++ b/urwid/raw_display.py
@@ -26,6 +26,7 @@ Direct terminal UI implementation
from __future__ import annotations
+import io
import os
import select
import signal
diff --git a/urwid/signals.py b/urwid/signals.py
index ea40131..dfbf47f 100644
--- a/urwid/signals.py
+++ b/urwid/signals.py
@@ -32,7 +32,7 @@ class MetaSignals(type):
register the list of signals in the class variable signals,
including signals in superclasses.
"""
- def __init__(cls, name, bases, d):
+ def __init__(cls, name: str, bases, d):
signals = d.get("signals", [])
for superclass in cls.__bases__:
signals.extend(getattr(superclass, 'signals', []))
@@ -48,15 +48,17 @@ def setdefaultattr(obj, name, value):
setattr(obj, name, value)
return value
+
class Key:
"""
Minimal class, whose only purpose is to produce objects with a
unique hash
"""
- __slots__ = []
+ __slots__ = ()
+
class Signals:
- _signal_attr = '_urwid_signals' # attribute to attach to signal senders
+ _signal_attr = '_urwid_signals' # attribute to attach to signal senders
def __init__(self):
self._supported = {}
@@ -74,7 +76,7 @@ class Signals:
"""
self._supported[sig_cls] = signals
- def connect(self, obj, name, callback, user_arg=None, weak_args=None, user_args=None):
+ def connect(self, obj, name: str, callback, user_arg=None, weak_args=None, user_args=None) -> Key:
"""
:param obj: the object sending a signal
:type obj: object
@@ -191,8 +193,7 @@ class Signals:
# Turn weak_args into weakrefs and prepend them to user_args
return [weakref.ref(a, callback) for a in (weak_args or [])] + (user_args or [])
-
- def disconnect(self, obj, name, callback, user_arg=None, weak_args=None, user_args=None):
+ def disconnect(self, obj, name: str, callback, user_arg=None, weak_args=None, user_args=None):
"""
:param obj: the object to disconnect the signal from
:type obj: object
@@ -226,7 +227,7 @@ class Signals:
if h[1:] == (callback, user_arg, user_args):
return self.disconnect_by_key(obj, name, h[0])
- def disconnect_by_key(self, obj, name, key):
+ def disconnect_by_key(self, obj, name: str, key: Key):
"""
:param obj: the object to disconnect the signal from
:type obj: object
@@ -247,7 +248,7 @@ class Signals:
handlers = signals.get(name, [])
handlers[:] = [h for h in handlers if h[0] is not key]
- def emit(self, obj, name, *args):
+ def emit(self, obj, name: str, *args):
"""
:param obj: the object sending a signal
:type obj: object
diff --git a/urwid/text_layout.py b/urwid/text_layout.py
index 5fcb407..aacb36d 100644
--- a/urwid/text_layout.py
+++ b/urwid/text_layout.py
@@ -22,23 +22,22 @@
from __future__ import annotations
-from urwid.util import (
- calc_text_pos,
- calc_trim_text,
- calc_width,
- is_wide_char,
- move_next_char,
- move_prev_char,
-)
+import typing
+from urwid.util import calc_text_pos, calc_trim_text, calc_width, is_wide_char, move_next_char, move_prev_char
+
+if typing.TYPE_CHECKING:
+ from typing_extensions import Literal
class TextLayout:
def supports_align_mode(self, align):
"""Return True if align is a supported align mode."""
return True
+
def supports_wrap_mode(self, wrap):
"""Return True if wrap is a supported wrap mode."""
return True
+
def layout(self, text, width, align, wrap ):
"""
Return a layout structure for text.
@@ -61,12 +60,15 @@ class TextLayout:
text offset. If the offset is None when inserting spaces
then no attribute will be used.
"""
- raise NotImplementedError("This function must be overridden by a real"
- " text layout class. (see StandardTextLayout)")
+ raise NotImplementedError(
+ "This function must be overridden by a real text layout class. (see StandardTextLayout)"
+ )
+
class CanNotDisplayText(Exception):
pass
+
class StandardTextLayout(TextLayout):
def __init__(self):#, tab_stops=(), tab_stop_every=8):
pass
@@ -79,13 +81,22 @@ class StandardTextLayout(TextLayout):
# self.tab_stops = (tab_stop_every,)
#self.tab_stops = tab_stops
#self.tab_stop_every = tab_stop_every
- def supports_align_mode(self, align):
+
+ def supports_align_mode(self, align: str) -> bool:
"""Return True if align is 'left', 'center' or 'right'."""
return align in ('left', 'center', 'right')
- def supports_wrap_mode(self, wrap):
+
+ def supports_wrap_mode(self, wrap: str) -> bool:
"""Return True if wrap is 'any', 'space', 'clip' or 'ellipsis'."""
return wrap in ('any', 'space', 'clip', 'ellipsis')
- def layout(self, text, width, align, wrap ):
+
+ def layout(
+ self,
+ text,
+ width: int,
+ align: Literal['left', 'center', 'right'],
+ wrap: Literal['any', 'space', 'clip', 'ellipsis'],
+ ):
"""Return a layout structure for text."""
try:
segs = self.calculate_text_segments( text, width, wrap )
@@ -93,7 +104,7 @@ class StandardTextLayout(TextLayout):
except CanNotDisplayText:
return [[]]
- def pack(self, maxcol, layout):
+ def pack(self, maxcol: int, layout) -> int:
"""
Return a minimal maxcol value that would result in the same
number of lines for layout. layout must be a layout structure
@@ -108,7 +119,14 @@ class StandardTextLayout(TextLayout):
maxwidth = max(maxwidth, lw)
return maxwidth
- def align_layout( self, text, width, segs, wrap, align ):
+ def align_layout(
+ self,
+ text,
+ width: int,
+ segs,
+ wrap: Literal['any', 'space', 'clip', 'ellipsis'],
+ align: Literal['left', 'center', 'right'],
+ ):
"""Convert the layout segs to an aligned layout."""
out = []
for l in segs:
@@ -124,8 +142,12 @@ class StandardTextLayout(TextLayout):
out.append([((width-sc+1) // 2, None)] + l)
return out
-
- def calculate_text_segments(self, text, width, wrap):
+ def calculate_text_segments(
+ self,
+ text,
+ width: int,
+ wrap: Literal['any', 'space', 'clip', 'ellipsis'],
+ ):
"""
Calculate the segments of text to display given width screen
columns to display them.
@@ -138,14 +160,14 @@ class StandardTextLayout(TextLayout):
"""
nl, nl_o, sp_o = "\n", "\n", " "
if isinstance(text, bytes):
- nl = nl.encode('iso8859-1') # can only find bytes in python3 bytestrings
- nl_o = ord(nl_o) # + an item of a bytestring is the ordinal value
+ nl = nl.encode('iso8859-1') # can only find bytes in python3 bytestrings
+ nl_o = ord(nl_o) # + an item of a bytestring is the ordinal value
sp_o = ord(sp_o)
b = []
p = 0
if wrap in ('clip', 'ellipsis'):
# no wrapping to calculate, so it's easy.
- while p<=len(text):
+ while p <= len(text):
n_cr = text.find(nl, p)
if n_cr == -1:
n_cr = len(text)
@@ -175,8 +197,7 @@ class StandardTextLayout(TextLayout):
p = n_cr+1
return b
-
- while p<=len(text):
+ while p <= len(text):
# look for next eligible line break
n_cr = text.find(nl, p)
if n_cr == -1:
@@ -184,89 +205,82 @@ class StandardTextLayout(TextLayout):
sc = calc_width(text, p, n_cr)
if sc == 0:
# removed character hint
- b.append([(0,n_cr)])
- p = n_cr+1
+ b.append([(0, n_cr)])
+ p = n_cr + 1
continue
if sc <= width:
# this segment fits
- b.append([(sc,p,n_cr),
- # removed character hint
- (0,n_cr)])
+ b.append([(sc, p, n_cr), (0, n_cr)])
+ # removed character hint
- p = n_cr+1
+ p = n_cr + 1
continue
pos, sc = calc_text_pos( text, p, n_cr, width )
- if pos == p: # pathological width=1 double-byte case
- raise CanNotDisplayText(
- "Wide character will not fit in 1-column width")
+ if pos == p: # pathological width=1 double-byte case
+ raise CanNotDisplayText("Wide character will not fit in 1-column width")
if wrap == 'any':
- b.append([(sc,p,pos)])
+ b.append([(sc, p, pos)])
p = pos
continue
assert wrap == 'space'
if text[pos] == sp_o:
# perfect space wrap
- b.append([(sc,p,pos),
- # removed character hint
- (0,pos)])
- p = pos+1
+ b.append([(sc, p, pos), (0,pos)])
+ # removed character hint
+
+ p = pos + 1
continue
if is_wide_char(text, pos):
# perfect next wide
- b.append([(sc,p,pos)])
+ b.append([(sc, p, pos)])
p = pos
continue
prev = pos
while prev > p:
prev = move_prev_char(text, p, prev)
if text[prev] == sp_o:
- sc = calc_width(text,p,prev)
- l = [(0,prev)]
- if p!=prev:
- l = [(sc,p,prev)] + l
+ sc = calc_width(text, p, prev)
+ l = [(0, prev)]
+ if p != prev:
+ l = [(sc, p, prev)] + l
b.append(l)
p = prev+1
break
- if is_wide_char(text,prev):
+ if is_wide_char(text, prev):
# wrap after wide char
next = move_next_char(text, prev, pos)
- sc = calc_width(text,p,next)
- b.append([(sc,p,next)])
+ sc = calc_width(text, p, next)
+ b.append([(sc, p, next)])
p = next
break
else:
# unwrap previous line space if possible to
# fit more text (we're breaking a word anyway)
- if b and (len(b[-1]) == 2 or ( len(b[-1])==1
- and len(b[-1][0])==2 )):
+ if b and (len(b[-1]) == 2 or (len(b[-1]) == 1 and len(b[-1][0]) == 2)):
# look for removed space above
if len(b[-1]) == 1:
[(h_sc, h_off)] = b[-1]
p_sc = 0
p_off = p_end = h_off
else:
- [(p_sc, p_off, p_end),
- (h_sc, h_off)] = b[-1]
- if (p_sc < width and h_sc==0 and
- text[h_off] == sp_o):
+ [(p_sc, p_off, p_end), (h_sc, h_off)] = b[-1]
+ if p_sc < width and h_sc == 0 and text[h_off] == sp_o:
# combine with previous line
del b[-1]
p = p_off
pos, sc = calc_text_pos(
text, p, n_cr, width )
- b.append([(sc,p,pos)])
+ b.append([(sc, p, pos)])
# check for trailing " " or "\n"
p = pos
- if p < len(text) and (
- text[p] in (sp_o, nl_o)):
+ if p < len(text) and (text[p] in (sp_o, nl_o)):
# removed character hint
- b[-1].append((0,p))
+ b[-1].append((0, p))
p += 1
continue
-
# force any char wrap
- b.append([(sc,p,pos)])
+ b.append([(sc, p, pos)])
p = pos
return b
@@ -279,35 +293,35 @@ default_layout = StandardTextLayout()
class LayoutSegment:
- def __init__(self, seg):
+ def __init__(self, seg: tuple[int, int, bytes | int] | tuple[int, int | None]) -> None:
"""Create object from line layout segment structure"""
- assert type(seg) == tuple, repr(seg)
- assert len(seg) in (2,3), repr(seg)
+ assert isinstance(seg, tuple), repr(seg)
+ assert len(seg) in (2, 3), repr(seg)
self.sc, self.offs = seg[:2]
- assert type(self.sc) == int, repr(self.sc)
+ assert isinstance(self.sc, int), repr(self.sc)
- if len(seg)==3:
- assert type(self.offs) == int, repr(self.offs)
+ if len(seg) == 3:
+ assert isinstance(self.offs, int), repr(self.offs)
assert self.sc > 0, repr(seg)
t = seg[2]
- if type(t) == bytes:
- self.text = t
+ if isinstance(t, bytes):
+ self.text: bytes | None = t
self.end = None
else:
- assert type(t) == int, repr(t)
+ assert isinstance(t, int), repr(t)
self.text = None
self.end = t
else:
assert len(seg) == 2, repr(seg)
if self.offs is not None:
assert self.sc >= 0, repr(seg)
- assert type(self.offs)==int
+ assert isinstance(self.offs, int)
self.text = self.end = None
- def subseg(self, text, start, end):
+ def subseg(self, text, start: int, end: int):
"""
Return a "sub-segment" list containing segment structures
that make up a portion of this segment.
@@ -316,33 +330,32 @@ class LayoutSegment:
need to be replaced with a space character at either edge
so two or three segments will be returned.
"""
- if start < 0: start = 0
- if end > self.sc: end = self.sc
+ if start < 0:
+ start = 0
+ if end > self.sc:
+ end = self.sc
if start >= end:
- return [] # completely gone
+ return [] # completely gone
if self.text:
# use text stored in segment (self.text)
- spos, epos, pad_left, pad_right = calc_trim_text(
- self.text, 0, len(self.text), start, end )
- return [ (end-start, self.offs, b''.ljust(pad_left) +
- self.text[spos:epos] + b''.ljust(pad_right)) ]
+ spos, epos, pad_left, pad_right = calc_trim_text(self.text, 0, len(self.text), start, end)
+ return [(end-start, self.offs, b''.ljust(pad_left) + self.text[spos:epos] + b''.ljust(pad_right))]
elif self.end:
# use text passed as parameter (text)
- spos, epos, pad_left, pad_right = calc_trim_text(
- text, self.offs, self.end, start, end )
+ spos, epos, pad_left, pad_right = calc_trim_text(text, self.offs, self.end, start, end)
l = []
if pad_left:
- l.append((1,spos-1))
+ l.append((1, spos-1))
l.append((end-start-pad_left-pad_right, spos, epos))
if pad_right:
- l.append((1,epos))
+ l.append((1, epos))
return l
else:
# simple padding adjustment
- return [(end-start,self.offs)]
+ return [(end-start ,self.offs)]
-def line_width( segs ):
+def line_width(segs):
"""
Return the screen column width of one line of a text layout structure.
@@ -351,21 +364,22 @@ def line_width( segs ):
"""
sc = 0
seglist = segs
- if segs and len(segs[0])==2 and segs[0][1] is None:
+ if segs and len(segs[0]) == 2 and segs[0][1] is None:
seglist = segs[1:]
for s in seglist:
sc += s[0]
return sc
-def shift_line( segs, amount ):
+
+def shift_line(segs, amount: int):
"""
Return a shifted line from a layout structure to the left or right.
segs -- line of a layout structure
amount -- screen columns to shift right (+ve) or left (-ve)
"""
- assert type(amount)==int, repr(amount)
+ assert isinstance(amount, int), repr(amount)
- if segs and len(segs[0])==2 and segs[0][1] is None:
+ if segs and len(segs[0]) == 2 and segs[0][1] is None:
# existing shift
amount += segs[0][0]
if amount:
@@ -377,7 +391,7 @@ def shift_line( segs, amount ):
return segs
-def trim_line( segs, text, start, end ):
+def trim_line(segs, text, start: int, end: int):
"""
Return a trimmed line of a text layout structure.
text -- text to which this layout structure applies
@@ -411,8 +425,7 @@ def trim_line( segs, text, start, end ):
return l
-
-def calc_line_pos( text, line_layout, pref_col ):
+def calc_line_pos(text, line_layout, pref_col: Literal['left', 'right'] | int):
"""
Calculate the closest linear position to pref_col given a
line layout structure. Returns None if no position found.
@@ -437,24 +450,20 @@ def calc_line_pos( text, line_layout, pref_col ):
return
if s.end is None:
return s.offs
- return calc_text_pos( text, s.offs, s.end, s.sc-1)[0]
+ return calc_text_pos(text, s.offs, s.end, s.sc-1)[0]
for seg in line_layout:
s = LayoutSegment(seg)
if s.offs is not None:
if s.end is not None:
- if (current_sc <= pref_col and
- pref_col < current_sc + s.sc):
+ if current_sc <= pref_col < current_sc + s.sc:
# exact match within this segment
- return calc_text_pos( text,
- s.offs, s.end,
- pref_col - current_sc )[0]
+ return calc_text_pos(text, s.offs, s.end, pref_col - current_sc)[0]
elif current_sc <= pref_col:
closest_sc = current_sc + s.sc - 1
closest_pos = s
- if closest_sc is None or ( abs(pref_col-current_sc)
- < abs(pref_col-closest_sc) ):
+ if closest_sc is None or (abs(pref_col-current_sc) < abs(pref_col-closest_sc)):
# this screen column is closer
closest_sc = current_sc
closest_pos = s.offs
@@ -463,41 +472,44 @@ def calc_line_pos( text, line_layout, pref_col ):
break
current_sc += s.sc
- if closest_pos is None or type(closest_pos) == int:
+ if closest_pos is None or isinstance(closest_pos, int):
return closest_pos
# return the last positions in the segment "closest_pos"
s = closest_pos
return calc_text_pos( text, s.offs, s.end, s.sc-1)[0]
-def calc_pos( text, layout, pref_col, row ):
+
+def calc_pos(text, layout, pref_col: Literal['left', 'right'] | int, row: int) -> int:
"""
Calculate the closest linear position to pref_col and row given a
layout structure.
"""
if row < 0 or row >= len(layout):
- raise Exception("calculate_pos: out of layout row range")
+ raise ValueError("calculate_pos: out of layout row range")
- pos = calc_line_pos( text, layout[row], pref_col )
+ pos = calc_line_pos(text, layout[row], pref_col)
if pos is not None:
return pos
- rows_above = list(range(row-1,-1,-1))
- rows_below = list(range(row+1,len(layout)))
+ rows_above = list(range(row-1, -1, -1))
+ rows_below = list(range(row+1, len(layout)))
while rows_above and rows_below:
if rows_above:
r = rows_above.pop(0)
pos = calc_line_pos(text, layout[r], pref_col)
- if pos is not None: return pos
+ if pos is not None:
+ return pos
if rows_below:
r = rows_below.pop(0)
pos = calc_line_pos(text, layout[r], pref_col)
- if pos is not None: return pos
+ if pos is not None:
+ return pos
return 0
-def calc_coords( text, layout, pos, clamp=1 ):
+def calc_coords(text, layout, pos, clamp: int = 1):
"""
Calculate the coordinates closest to position pos in text with layout.
@@ -516,18 +528,18 @@ def calc_coords( text, layout, pos, clamp=1 ):
x += s.sc
continue
if s.offs == pos:
- return x,y
- if s.end is not None and s.offs<=pos and s.end>pos:
- x += calc_width( text, s.offs, pos )
- return x,y
+ return x, y
+ if s.end is not None and s.offs <= pos < s.end:
+ x += calc_width(text, s.offs, pos)
+ return x, y
distance = abs(s.offs - pos)
if s.end is not None and s.end<pos:
distance = pos - (s.end-1)
if closest is None or distance < closest[0]:
- closest = distance, (x,y)
+ closest = distance, (x, y)
x += s.sc
y += 1
if closest:
return closest[1]
- return 0,0
+ return 0, 0
diff --git a/urwid/treetools.py b/urwid/treetools.py
index 769da49..94d7c69 100644
--- a/urwid/treetools.py
+++ b/urwid/treetools.py
@@ -281,7 +281,7 @@ class TreeNode:
class ParentNode(TreeNode):
"""Maintain sort order for TreeNodes."""
def __init__(self, value, parent=None, key=None, depth=None):
- TreeNode.__init__(self, value, parent=parent, key=key, depth=depth)
+ super().__init__(value, parent=parent, key=key, depth=depth)
self._child_keys = None
self._children = {}
diff --git a/urwid/util.py b/urwid/util.py
index 9def762..de5bae2 100644
--- a/urwid/util.py
+++ b/urwid/util.py
@@ -23,9 +23,13 @@
from __future__ import annotations
import codecs
+import typing
from urwid import escape
+if typing.TYPE_CHECKING:
+ from typing_extensions import Self
+
str_util = escape.str_util
# bring str_util functions into our namespace
@@ -116,15 +120,15 @@ def get_encoding_mode():
return str_util.get_byte_encoding()
-def apply_target_encoding( s ):
+def apply_target_encoding(s: str | bytes):
"""
Return (encoded byte string, character set rle).
"""
- if _use_dec_special and type(s) == str:
+ if _use_dec_special and isinstance(s, str):
# first convert drawing characters
s = s.translate(escape.DEC_SPECIAL_CHARMAP)
- if type(s) == str:
+ if isinstance(s, str):
s = s.replace(escape.SI+escape.SO, "") # remove redundant shifts
s = codecs.encode(s, _target_encoding, 'replace')
@@ -183,10 +187,7 @@ def supports_unicode():
return _target_encoding and _target_encoding != 'ascii'
-
-
-
-def calc_trim_text( text, start_offs, end_offs, start_col, end_col ):
+def calc_trim_text( text, start_offs: int, end_offs: int, start_col: int, end_col: int):
"""
Calculate the result of trimming text.
start_offs -- offset into text to treat as screen column 0
@@ -215,14 +216,11 @@ def calc_trim_text( text, start_offs, end_offs, start_col, end_col ):
return ( spos, pos, pad_left, pad_right )
-
-
-def trim_text_attr_cs( text, attr, cs, start_col, end_col ):
+def trim_text_attr_cs(text, attr, cs, start_col: int, end_col: int):
"""
Return ( trimmed text, trimmed attr, trimmed cs ).
"""
- spos, epos, pad_left, pad_right = calc_trim_text(
- text, 0, len(text), start_col, end_col )
+ spos, epos, pad_left, pad_right = calc_trim_text(text, 0, len(text), start_col, end_col )
attrtr = rle_subseg( attr, spos, epos )
cstr = rle_subseg( cs, spos, epos )
if pad_left:
@@ -238,7 +236,7 @@ def trim_text_attr_cs( text, attr, cs, start_col, end_col ):
b''.rjust(pad_right), attrtr, cstr)
-def rle_get_at( rle, pos ):
+def rle_get_at(rle, pos: int):
"""
Return the attribute at offset pos.
"""
@@ -252,7 +250,7 @@ def rle_get_at( rle, pos ):
return None
-def rle_subseg( rle, start, end ):
+def rle_subseg(rle, start: int, end: int):
"""Return a sub segment of an rle list."""
l = []
x = 0
@@ -274,7 +272,7 @@ def rle_subseg( rle, start, end ):
return l
-def rle_len( rle ):
+def rle_len(rle) -> int:
"""
Return the number of characters covered by a run length
encoded attribute list.
@@ -282,12 +280,13 @@ def rle_len( rle ):
run = 0
for v in rle:
- assert type(v) == tuple, repr(rle)
+ assert isinstance(v, tuple), repr(rle)
a, r = v
run += r
return run
-def rle_prepend_modify(rle, a_r):
+
+def rle_prepend_modify(rle, a_r) -> None:
"""
Append (a, r) (unpacked from *a_r*) to BEGINNING of rle.
Merge with first run when possible
@@ -319,6 +318,7 @@ def rle_append_modify(rle, a_r):
la,lr = rle[-1]
rle[-1] = (a, lr+r)
+
def rle_join_modify( rle, rle2 ):
"""
Append attribute list rle2 to rle.
@@ -331,6 +331,7 @@ def rle_join_modify( rle, rle2 ):
rle_append_modify(rle, rle2[0])
rle += rle2[1:]
+
def rle_product( rle1, rle2 ):
"""
Merge the runs of rle1 and rle2 like this:
@@ -375,6 +376,7 @@ def rle_factor( rle ):
class TagMarkupException(Exception): pass
+
def decompose_tagmarkup(tm):
"""Return (text string, attribute list) for tagmarkup passed."""
@@ -387,18 +389,19 @@ def decompose_tagmarkup(tm):
return text, al
-def _tagmarkup_recurse( tm, attr ):
+
+def _tagmarkup_recurse(tm, attr):
"""Return (text list, attribute list) for tagmarkup passed.
tm -- tagmarkup
attr -- current attribute or None"""
- if type(tm) == list:
+ if isinstance(tm, list):
# for lists recurse to process each subelement
rtl = []
ral = []
for element in tm:
- tl, al = _tagmarkup_recurse( element, attr )
+ tl, al = _tagmarkup_recurse(element, attr)
if ral:
# merge attributes when possible
last_attr, last_run = ral[-1]
@@ -410,7 +413,7 @@ def _tagmarkup_recurse( tm, attr ):
ral += al
return rtl, ral
- if type(tm) == tuple:
+ if isinstance(tm, tuple):
# tuples mark a new attribute boundary
if len(tm) != 2:
raise TagMarkupException(f"Tuples must be in the form (attribute, tagmarkup): {tm!r}")
@@ -425,25 +428,24 @@ def _tagmarkup_recurse( tm, attr ):
return [tm], [(attr, len(tm))]
+def is_mouse_event(ev: str) -> bool:
+ return isinstance(ev, tuple) and len(ev) == 4 and "mouse" in ev[0]
-def is_mouse_event( ev ):
- return type(ev) == tuple and len(ev)==4 and ev[0].find("mouse")>=0
-def is_mouse_press( ev ):
- return ev.find("press")>=0
+def is_mouse_press(ev: str) -> bool:
+ return "press" in ev
class MetaSuper(type):
"""adding .__super"""
- def __init__(cls, name, bases, d):
+ def __init__(cls, name: str, bases, d):
super().__init__(name, bases, d)
if hasattr(cls, f"_{name}__super"):
raise AttributeError("Class has same name as one of its super classes")
setattr(cls, f"_{name}__super", super(cls))
-
-def int_scale(val, val_range, out_range):
+def int_scale(val: int, val_range: int, out_range: int):
"""
Scale val in the range [0, val_range-1] to an integer in the range
[0, out_range-1]. This implementation uses the "round-half-up" rounding
@@ -472,7 +474,7 @@ class StoppingContext:
def __init__(self, wrapped):
self._wrapped = wrapped
- def __enter__(self):
+ def __enter__(self) -> Self:
return self
def __exit__(self, *exc_info):
diff --git a/urwid/vterm.py b/urwid/vterm.py
index aee0bae..34c29c6 100644
--- a/urwid/vterm.py
+++ b/urwid/vterm.py
@@ -242,7 +242,7 @@ class TermCanvas(Canvas):
cacheable = False
def __init__(self, width, height, widget):
- Canvas.__init__(self)
+ super().__init__()
self.width, self.height = width, height
self.widget = widget
@@ -1360,7 +1360,7 @@ class Terminal(Widget):
``utf8`` with ``urwid.set_encoding("utf8")``. See
:ref:`text-encodings` for more details.
"""
- Widget.__init__(self)
+ super().__init__()
self.escape_sequence = escape_sequence or "ctrl a"
diff --git a/urwid/widget.py b/urwid/widget.py
index d0ae1fb..9de17e0 100644
--- a/urwid/widget.py
+++ b/urwid/widget.py
@@ -23,6 +23,7 @@
from __future__ import annotations
import functools
+import typing
import warnings
from operator import attrgetter
@@ -39,14 +40,13 @@ from urwid.command_map import (
)
from urwid.split_repr import remove_defaults, split_repr
from urwid.text_layout import calc_coords, calc_pos, shift_line
-from urwid.util import (
- MetaSuper,
- calc_width,
- decompose_tagmarkup,
- is_wide_char,
- move_next_char,
- move_prev_char,
-)
+from urwid.util import MetaSuper, calc_width, decompose_tagmarkup, is_wide_char, move_next_char, move_prev_char
+
+if typing.TYPE_CHECKING:
+ from typing_extensions import Literal
+
+ from urwid.canvas import TextCanvas
+
# define some names for these constants to avoid misspellings in the source
# and to document the constant strings we are using
@@ -454,14 +454,14 @@ class Widget(metaclass=WidgetMeta):
_sizing = frozenset([FLOW, BOX, FIXED])
_command_map = command_map
- def _invalidate(self):
+ def _invalidate(self) -> None:
"""
Mark cached canvases rendered by this widget as dirty so that
they will not be used again.
"""
CanvasCache.invalidate(self)
- def _emit(self, name, *args):
+ def _emit(self, name: str, *args):
"""
Convenience function to emit signals with self as first
argument.
@@ -686,6 +686,7 @@ def fixed_size(size):
if size != ():
raise ValueError(f"FixedWidget takes only () for size.passed: {size!r}")
+
class FixedWidget(Widget):
"""
Deprecated. Inherit from Widget and add:
@@ -731,7 +732,12 @@ class Divider(Widget):
ignore_focus = True
- def __init__(self,div_char=" ",top=0,bottom=0):
+ def __init__(
+ self,
+ div_char: str | bytes = " ",
+ top: int = 0,
+ bottom: int = 0,
+ ) -> None:
"""
:param div_char: character to repeat across line
:type div_char: bytes or unicode
@@ -764,7 +770,7 @@ class Divider(Widget):
if self.bottom: attrs['bottom'] = self.bottom
return attrs
- def rows(self, size, focus=False):
+ def rows(self, size: tuple[int], focus: bool = False) -> int:
"""
Return the number of lines that will be rendered.
@@ -776,7 +782,7 @@ class Divider(Widget):
(maxcol,) = size
return self.top + 1 + self.bottom
- def render(self, size, focus=False):
+ def render(self, size: tuple[int], focus: bool = False):
"""
Render the divider as a canvas and return it.
@@ -802,7 +808,7 @@ class SolidFill(BoxWidget):
_selectable = False
ignore_focus = True
- def __init__(self, fill_char=" "):
+ def __init__(self, fill_char: str = " ") -> None:
"""
:param fill_char: character to fill area with
:type fill_char: bytes or unicode
@@ -816,7 +822,7 @@ class SolidFill(BoxWidget):
def _repr_words(self):
return super()._repr_words() + [repr(self.fill_char)]
- def render(self, size, focus=False ):
+ def render(self, size: tuple[int, int], focus: bool = False) -> SolidCanvas:
"""
Render the Fill as a canvas and return it.
@@ -828,9 +834,11 @@ class SolidFill(BoxWidget):
maxcol, maxrow = size
return SolidCanvas(self.fill_char, maxcol, maxrow)
+
class TextError(Exception):
pass
+
class Text(Widget):
"""
a horizontally resizeable text widget
@@ -840,7 +848,13 @@ class Text(Widget):
ignore_focus = True
_repr_content_length_max = 140
- def __init__(self, markup, align=LEFT, wrap=SPACE, layout=None):
+ def __init__(
+ self,
+ markup,
+ align: Literal['left', 'center', 'right'] = LEFT,
+ wrap: Literal['space', 'any', 'clip', 'ellipsis'] = SPACE,
+ layout=None,
+ ) -> None:
"""
:param markup: content of text widget, one of:
@@ -896,11 +910,11 @@ class Text(Widget):
wrap=self._wrap_mode)
return remove_defaults(attrs, Text.__init__)
- def _invalidate(self):
+ def _invalidate(self) -> None:
self._cache_maxcol = None
super()._invalidate()
- def set_text(self,markup):
+ def set_text(self, markup):
"""
Set content of text widget.
@@ -941,7 +955,7 @@ class Text(Widget):
return self._text, self._attrib
@property
- def text(self):
+ def text(self) -> str:
"""
Read-only property returning the complete bytes/unicode content
of this widget
@@ -956,7 +970,7 @@ class Text(Widget):
"""
return self.get_text()[1]
- def set_align_mode(self, mode):
+ def set_align_mode(self, mode: Literal['left', 'center', 'right']):
"""
Set text alignment mode. Supported modes depend on text layout
object in use but defaults to a :class:`StandardTextLayout` instance
@@ -982,7 +996,7 @@ class Text(Widget):
self._align_mode = mode
self._invalidate()
- def set_wrap_mode(self, mode):
+ def set_wrap_mode(self, mode: Literal['space', 'any', 'clip', 'ellipsis']):
"""
Set text wrapping mode. Supported modes depend on text layout
object in use but defaults to a :class:`StandardTextLayout` instance
@@ -1010,7 +1024,12 @@ class Text(Widget):
self._wrap_mode = mode
self._invalidate()
- def set_layout(self, align, wrap, layout=None):
+ def set_layout(
+ self,
+ align: Literal['left', 'center', 'right'],
+ wrap: Literal['space', 'any', 'clip', 'ellipsis'],
+ layout=None,
+ ):
"""
Set the text layout object, alignment and wrapping modes at
the same time.
@@ -1032,14 +1051,14 @@ class Text(Widget):
self.set_align_mode(align)
self.set_wrap_mode(wrap)
- align = property(lambda self:self._align_mode, set_align_mode)
- wrap = property(lambda self:self._wrap_mode, set_wrap_mode)
+ align = property(lambda self: self._align_mode, set_align_mode)
+ wrap = property(lambda self: self._wrap_mode, set_wrap_mode)
@property
def layout(self):
return self._layout
- def render(self, size, focus=False):
+ def render(self, size: tuple[int], focus: bool = False) -> TextCanvas:
"""
Render contents with wrapping and alignment. Return canvas.
@@ -1053,10 +1072,10 @@ class Text(Widget):
(maxcol,) = size
text, attr = self.get_text()
#assert isinstance(text, unicode)
- trans = self.get_line_translation( maxcol, (text,attr) )
+ trans = self.get_line_translation(maxcol, (text, attr))
return apply_text_layout(text, attr, trans, maxcol)
- def rows(self, size, focus=False):
+ def rows(self, size: tuple[int], focus: bool = False) -> int:
"""
Return the number of rows the rendered text requires.
@@ -1070,7 +1089,7 @@ class Text(Widget):
(maxcol,) = size
return len(self.get_line_translation(maxcol))
- def get_line_translation(self, maxcol, ta=None):
+ def get_line_translation(self, maxcol: int, ta=None):
"""
Return layout structure used to map self.text to a canvas.
This method is used internally, but may be useful for
@@ -1086,7 +1105,7 @@ class Text(Widget):
self._update_cache_translation(maxcol, ta)
return self._cache_translation
- def _update_cache_translation(self,maxcol, ta):
+ def _update_cache_translation(self,maxcol: int, ta):
if ta:
text, attr = ta
else:
@@ -1095,7 +1114,7 @@ class Text(Widget):
self._cache_translation = self.layout.layout(
text, maxcol, self._align_mode, self._wrap_mode)
- def pack(self, size=None, focus=False):
+ def pack(self, size: tuple[int] | None = None, focus: bool = False) -> tuple[int, int]:
"""
Return the number of screen columns and rows required for
this Text widget to be displayed without wrapping or
@@ -1175,9 +1194,18 @@ class Edit(Text):
"""
return is_wide_char(ch,0) or (len(ch)==1 and ord(ch) >= 32)
- def __init__(self, caption="", edit_text="", multiline=False,
- align=LEFT, wrap=SPACE, allow_tab=False,
- edit_pos=None, layout=None, mask=None):
+ def __init__(
+ self,
+ caption="",
+ edit_text: str | bytes = "",
+ multiline: bool = False,
+ align: Literal['left', 'center', 'right'] = LEFT,
+ wrap: Literal['space', 'any', 'clip', 'ellipsis'] = SPACE,
+ allow_tab: bool = False,
+ edit_pos: int | None = None,
+ layout=None,
+ mask: str | bytes | None = None,
+ ) -> None:
"""
:param caption: markup for caption preceding edit_text, see
:class:`Text` for description of text markup.
@@ -1273,7 +1301,7 @@ class Edit(Text):
raise EditError("set_text() not supported. Use set_caption()"
" or set_edit_text() instead.")
- def get_pref_col(self, size):
+ def get_pref_col(self, size: tuple[int]) -> int:
"""
Return the preferred column for the cursor, or the
current cursor x value. May also return ``'left'`` or ``'right'``
@@ -1311,7 +1339,7 @@ class Edit(Text):
else:
return pref_col
- def update_text(self):
+ def update_text(self) -> typing.NoReturn:
"""
No longer supported.
@@ -1346,13 +1374,13 @@ class Edit(Text):
self._invalidate()
@property
- def caption(self):
+ def caption(self) -> str:
"""
Read-only property returning the caption for this widget.
"""
return self._caption
- def set_edit_pos(self, pos):
+ def set_edit_pos(self, pos: int) -> None:
"""
Set the cursor position with a self.edit_text offset.
Clips pos to [0, len(edit_text)].
@@ -1386,7 +1414,7 @@ class Edit(Text):
Property controlling the edit position for this widget.
""")
- def set_mask(self, mask):
+ def set_mask(self, mask: str | bytes | None):
"""
Set the character for masking text away.
@@ -1397,7 +1425,7 @@ class Edit(Text):
self._mask = mask
self._invalidate()
- def set_edit_text(self, text):
+ def set_edit_text(self, text: str | bytes) -> None:
"""
Set the edit text for this widget.
@@ -1425,7 +1453,7 @@ class Edit(Text):
self._emit("postchange", old_text)
self._invalidate()
- def get_edit_text(self):
+ def get_edit_text(self) -> str:
"""
Return the edit text for this widget.
@@ -1441,7 +1469,7 @@ class Edit(Text):
Property controlling the edit text for this widget.
""")
- def insert_text(self, text):
+ def insert_text(self, text: str | bytes) -> None:
"""
Insert text at the cursor position and update cursor.
This method is used by the keypress() method when inserting
@@ -1466,7 +1494,7 @@ class Edit(Text):
self.set_edit_pos(result_pos)
self.highlight = None
- def _normalize_to_caption(self, text):
+ def _normalize_to_caption(self, text: str | bytes) -> str | bytes:
"""
Return text converted to the same type as self.caption
(bytes or unicode)
@@ -1476,10 +1504,10 @@ class Edit(Text):
if tu == cu:
return text
if tu:
- return text.encode('ascii') # follow python2's implicit conversion
+ return text.encode('ascii') # follow python2's implicit conversion
return text.decode('ascii')
- def insert_text_result(self, text):
+ def insert_text_result(self, text: str | bytes) -> tuple[str | bytes, int]:
"""
Return result of insert_text(text) without actually performing the
insertion. Handy for pre-validation.
@@ -1501,14 +1529,13 @@ class Edit(Text):
result_pos = self.edit_pos
try:
- result_text = (result_text[:result_pos] + text +
- result_text[result_pos:])
+ result_text = (result_text[:result_pos] + text + result_text[result_pos:])
except:
assert 0, repr((self.edit_text, result_text, text))
result_pos += len(text)
return (result_text, result_pos)
- def keypress(self, size, key):
+ def keypress(self, size: tuple[int], key: str | bytes):
"""
Handle editing keystrokes, return others.
@@ -1530,24 +1557,23 @@ class Edit(Text):
p = self.edit_pos
if self.valid_char(key):
- if (isinstance(key, str) and not
- isinstance(self._caption, str)):
+ if isinstance(key, str) and not isinstance(self._caption, str):
# screen is sending us unicode input, must be using utf-8
# encoding because that's all we support, so convert it
# to bytes to match our caption's type
key = key.encode('utf-8')
self.insert_text(key)
- elif key=="tab" and self.allow_tab:
+ elif key == "tab" and self.allow_tab:
key = " "*(8-(self.edit_pos%8))
self.insert_text(key)
- elif key=="enter" and self.multiline:
+ elif key == "enter" and self.multiline:
key = "\n"
self.insert_text(key)
elif self._command_map[key] == CURSOR_LEFT:
- if p==0: return key
+ if p == 0: return key
p = move_prev_char(self.edit_text,0,p)
self.set_edit_pos(p)
@@ -1559,41 +1585,42 @@ class Edit(Text):
elif self._command_map[key] in (CURSOR_UP, CURSOR_DOWN):
self.highlight = None
- x,y = self.get_cursor_coords((maxcol,))
+ x, y = self.get_cursor_coords((maxcol,))
pref_col = self.get_pref_col((maxcol,))
assert pref_col is not None
#if pref_col is None:
# pref_col = x
- if self._command_map[key] == CURSOR_UP: y -= 1
- else: y += 1
+ if self._command_map[key] == CURSOR_UP:
+ y -= 1
+ else:
+ y += 1
- if not self.move_cursor_to_coords((maxcol,),pref_col,y):
+ if not self.move_cursor_to_coords((maxcol,), pref_col, y):
return key
- elif key=="backspace":
+ elif key == "backspace":
self.pref_col_maxcol = None, None
if not self._delete_highlighted():
- if p == 0: return key
- p = move_prev_char(self.edit_text,0,p)
- self.set_edit_text( self.edit_text[:p] +
- self.edit_text[self.edit_pos:] )
- self.set_edit_pos( p )
+ if p == 0:
+ return key
+ p = move_prev_char(self.edit_text, 0, p)
+ self.set_edit_text(self.edit_text[:p] + self.edit_text[self.edit_pos:])
+ self.set_edit_pos(p)
- elif key=="delete":
+ elif key == "delete":
self.pref_col_maxcol = None, None
if not self._delete_highlighted():
if p >= len(self.edit_text):
return key
p = move_next_char(self.edit_text,p,len(self.edit_text))
- self.set_edit_text( self.edit_text[:self.edit_pos] +
- self.edit_text[p:] )
+ self.set_edit_text( self.edit_text[:self.edit_pos] + self.edit_text[p:] )
elif self._command_map[key] in (CURSOR_MAX_LEFT, CURSOR_MAX_RIGHT):
self.highlight = None
self.pref_col_maxcol = None, None
- x,y = self.get_cursor_coords((maxcol,))
+ x, y = self.get_cursor_coords((maxcol,))
if self._command_map[key] == CURSOR_MAX_LEFT:
self.move_cursor_to_coords((maxcol,), LEFT, y)
@@ -1605,7 +1632,7 @@ class Edit(Text):
# key wasn't handled
return key
- def move_cursor_to_coords(self, size, x, y):
+ def move_cursor_to_coords(self, size: tuple[int], x: int, y: int) -> bool:
"""
Set the cursor position with (x,y) coordinates.
Returns True if move succeeded, False otherwise.
@@ -1631,14 +1658,16 @@ class Edit(Text):
pos = calc_pos( self.get_text()[0], trans, x, y )
e_pos = pos - len(self.caption)
- if e_pos < 0: e_pos = 0
- if e_pos > len(self.edit_text): e_pos = len(self.edit_text)
+ if e_pos < 0:
+ e_pos = 0
+ if e_pos > len(self.edit_text):
+ e_pos = len(self.edit_text)
self.edit_pos = e_pos
self.pref_col_maxcol = x, maxcol
self._invalidate()
return True
- 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:
"""
Move the cursor to the location clicked for button 1.
@@ -1650,25 +1679,25 @@ class Edit(Text):
2
"""
(maxcol,) = size
- if button==1:
- return self.move_cursor_to_coords( (maxcol,), x, y )
-
+ if button == 1:
+ return self.move_cursor_to_coords((maxcol,), x, y)
+ return False
- def _delete_highlighted(self):
+ def _delete_highlighted(self) -> bool:
"""
Delete all highlighted text and update cursor position, if any
text is highlighted.
"""
- if not self.highlight: return
+ if not self.highlight:
+ return False
start, stop = self.highlight
btext, etext = self.edit_text[:start], self.edit_text[stop:]
- self.set_edit_text( btext + etext )
+ self.set_edit_text(btext + etext)
self.edit_pos = start
self.highlight = None
return True
-
- def render(self, size, focus=False):
+ def render(self, size: tuple[int], focus: bool = False) -> TextCanvas | CompositeCanvas:
"""
Render edit widget and return canvas. Include cursor when in
focus.
@@ -1682,7 +1711,7 @@ class Edit(Text):
(maxcol,) = size
self._shift_view_to_cursor = bool(focus)
- canv = Text.render(self,(maxcol,))
+ canv: TextCanvas | CompositeCanvas = Text.render(self,(maxcol,))
if focus:
canv = CompositeCanvas(canv)
canv.cursor = self.get_cursor_coords((maxcol,))
@@ -1693,14 +1722,13 @@ class Edit(Text):
# d.coords['highlight'] = [ hstart, hstop ]
return canv
-
- def get_line_translation(self, maxcol, ta=None ):
+ def get_line_translation(self, maxcol: int, ta=None):
trans = Text.get_line_translation(self, maxcol, ta)
if not self._shift_view_to_cursor:
return trans
text, ignore = self.get_text()
- x,y = calc_coords( text, trans,
+ x, y = calc_coords( text, trans,
self.edit_pos + len(self.caption) )
if x < 0:
return ( trans[:y]
@@ -1712,8 +1740,7 @@ class Edit(Text):
+ trans[y+1:] )
return trans
-
- def get_cursor_coords(self, size):
+ def get_cursor_coords(self, size: tuple[int]) -> tuple[int, int]:
"""
Return the (*x*, *y*) coordinates of cursor within widget.
@@ -1725,8 +1752,7 @@ class Edit(Text):
self._shift_view_to_cursor = True
return self.position_coords(maxcol,self.edit_pos)
-
- def position_coords(self,maxcol,pos):
+ def position_coords(self, maxcol: int, pos) -> tuple[int, int]:
"""
Return (*x*, *y*) coordinates for an offset into self.edit_text.
"""