Skip to content

Commit

Permalink
Merge 76a78d7 into 564021e
Browse files Browse the repository at this point in the history
  • Loading branch information
penguinolog committed Feb 20, 2024
2 parents 564021e + 76a78d7 commit 7412fa4
Show file tree
Hide file tree
Showing 11 changed files with 83 additions and 28 deletions.
24 changes: 16 additions & 8 deletions examples/fib.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,13 @@

from __future__ import annotations

import typing

import urwid

if typing.TYPE_CHECKING:
from typing_extensions import Literal


class FibonacciWalker(urwid.ListWalker):
"""ListWalker-compatible class for browsing fibonacci set.
Expand Down Expand Up @@ -107,22 +112,25 @@ class NumericLayout(urwid.TextLayout):
TextLayout class for bottom-right aligned numbers
"""

def layout(self, text, width: int, align, wrap):
def layout(
self,
text: str | bytes,
width: int,
align: Literal["left", "center", "right"] | urwid.Align,
wrap: Literal["any", "space", "clip", "ellipsis"] | urwid.WrapMode,
) -> list[list[tuple[int, int, int | bytes] | tuple[int, int | None]]]:
"""
Return layout structure for right justified numbers.
"""
lt = len(text)
r = lt % width # remaining segment not full width wide
if r:
linestarts = range(r, lt, width)
return [
# right-align the remaining segment on 1st line
[(width - r, None), (r, 0, r)]
# fill the rest of the lines
] + [[(width, x, x + width)] for x in linestarts]
[(width - r, None), (r, 0, r)], # right-align the remaining segment on 1st line
*([(width, x, x + width)] for x in range(r, lt, width)), # fill the rest of the lines
]

linestarts = range(0, lt, width)
return [[(width, x, x + width)] for x in linestarts]
return [[(width, x, x + width)] for x in range(0, lt, width)]


if __name__ == "__main__":
Expand Down
37 changes: 37 additions & 0 deletions tests/test_text_layout.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
from __future__ import annotations

import typing
import unittest

import urwid
from urwid import text_layout
from urwid.util import get_encoding, set_temporary_encoding

if typing.TYPE_CHECKING:
from typing_extensions import Literal


class CalcBreaksTest(unittest.TestCase):
def cbtest(self, width, exp, mode, text):
Expand Down Expand Up @@ -412,3 +416,36 @@ def test_ellipsis_encoding_support(self):
widget._invalidate()
canvas = widget.render((1,))
self.assertEqual("T", str(canvas))


class NumericLayout(urwid.TextLayout):
"""
TextLayout class for bottom-right aligned numbers
"""

def layout(
self,
text: str | bytes,
width: int,
align: Literal["left", "center", "right"] | urwid.Align,
wrap: Literal["any", "space", "clip", "ellipsis"] | urwid.WrapMode,
) -> list[list[tuple[int, int, int | bytes] | tuple[int, int | None]]]:
"""
Return layout structure for right justified numbers.
"""
lt = len(text)
r = lt % width # remaining segment not full width wide
if r:
return [
[(width - r, None), (r, 0, r)], # right-align the remaining segment on 1st line
*([(width, x, x + width)] for x in range(r, lt, width)), # fill the rest of the lines
]

return [[(width, x, x + width)] for x in range(0, lt, width)]


class TestTextLayoutNoPack(unittest.TestCase):
def test(self):
"""Text widget pack should work also with layout not supporting `pack` method."""
widget = urwid.Text("123", layout=NumericLayout())
self.assertEqual((3, 1), widget.pack((3,)))
4 changes: 3 additions & 1 deletion urwid/display/_raw_display_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -549,11 +549,13 @@ def is_blank_row(row: list[tuple[object, Literal["0", "U"] | None], bytes]) -> b
return False
return True

def attr_to_escape(a: AttrSpec | str) -> str:
def attr_to_escape(a: AttrSpec | str | None) -> str:
if a in self._pal_escape:
return self._pal_escape[a]
if isinstance(a, AttrSpec):
return self._attrspec_to_escape(a)
if a is None:
return self._attrspec_to_escape(AttrSpec("default", "default"))
# undefined attributes use default/default
self.logger.debug(f"Undefined attribute: {a!r}")
return self._attrspec_to_escape(AttrSpec("default", "default"))
Expand Down
2 changes: 1 addition & 1 deletion urwid/display/lcd.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def get_cols_rows(self):
return self.DISPLAY_SIZE


class CFLCDScreen(LCDScreen):
class CFLCDScreen(LCDScreen, abc.ABC):
"""
Common methods for Crystal Fonts LCD displays
"""
Expand Down
7 changes: 4 additions & 3 deletions urwid/event_loop/zmq_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from .abstract_loop import EventLoop, ExitMainLoop

if typing.TYPE_CHECKING:
import io
from collections.abc import Callable
from concurrent.futures import Executor, Future

Expand Down Expand Up @@ -151,10 +152,10 @@ def watch_queue(

def watch_file(
self,
fd: int,
fd: int | io.TextIOWrapper,
callback: Callable[[], typing.Any],
flags: int = zmq.POLLIN,
) -> int:
) -> io.TextIOWrapper:
"""
Call *callback* when *fd* has some data to read. No parameters are
passed to the callback. The *flags* are as for :meth:`watch_queue`.
Expand Down Expand Up @@ -191,7 +192,7 @@ def remove_watch_queue(self, handle: zmq.Socket) -> bool:

return True

def remove_watch_file(self, handle: int) -> bool:
def remove_watch_file(self, handle: io.TextIOWrapper) -> bool:
"""
Remove a file from background polling. Returns ``True`` if the file was
being monitored, ``False`` otherwise.
Expand Down
1 change: 1 addition & 0 deletions urwid/listbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,7 @@ def _get_focus_position(self):
)

def _contents(self):
# noinspection PyMethodParameters
class ListBoxContents:
# pylint: disable=no-self-argument

Expand Down
10 changes: 6 additions & 4 deletions urwid/widget/attr_wrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from .attr_map import AttrMap

if typing.TYPE_CHECKING:
from collections.abc import Hashable

from .constants import Sizing
from .widget import Widget

Expand Down Expand Up @@ -86,10 +88,10 @@ def set_w(self, new_widget: Widget) -> None:
)
self.original_widget = new_widget

def get_attr(self):
def get_attr(self) -> Hashable:
return self.attr_map[None]

def set_attr(self, attr):
def set_attr(self, attr: Hashable) -> None:
"""
Set the attribute to apply to the wrapped widget
Expand All @@ -102,13 +104,13 @@ def set_attr(self, attr):

attr = property(get_attr, set_attr)

def get_focus_attr(self):
def get_focus_attr(self) -> Hashable | None:
focus_map = self.focus_map
if focus_map:
return focus_map[None]
return None

def set_focus_attr(self, focus_attr):
def set_focus_attr(self, focus_attr: Hashable) -> None:
"""
Set the attribute to apply to the wapped widget when it is in
focus
Expand Down
2 changes: 1 addition & 1 deletion urwid/widget/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class _ContainerElementSizingFlag(enum.IntFlag):
WH_GIVEN = enum.auto()

@property
def reverse_flag(self) -> tuple[frozenset(Sizing), WHSettings | None]:
def reverse_flag(self) -> tuple[frozenset[Sizing], WHSettings | None]:
"""Get flag in public API format."""
sizing: set[Sizing] = set()

Expand Down
18 changes: 10 additions & 8 deletions urwid/widget/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from urwid.canvas import CanvasCombine, CompositeCanvas
from urwid.util import is_mouse_press

from .constants import Sizing
from .constants import Sizing, VAlign
from .container import WidgetContainerMixin
from .filler import Filler
from .widget import Widget, WidgetError
Expand Down Expand Up @@ -80,7 +80,7 @@ def header(self) -> Widget | None:
return self._header

@header.setter
def header(self, header: Widget | None):
def header(self, header: Widget | None) -> None:
_check_widget_subclass(header)
self._header = header
if header is None and self.focus_part == "header":
Expand Down Expand Up @@ -260,6 +260,8 @@ def contents(self) -> MutableMapping[Literal["header", "footer", "body"], tuple[
class FrameContents(typing.MutableMapping[str, typing.Tuple[Widget, None]]):
# pylint: disable=no-self-argument

__slots__ = ()

def __len__(inner_self) -> int:
return len(inner_self.keys())

Expand Down Expand Up @@ -295,7 +297,7 @@ def _contents__getitem__(self, key: Literal["header", "footer", "body"]):
return (self._footer, None)
raise KeyError(f"Frame.contents has no key: {key!r}")

def _contents__setitem__(self, key: Literal["header", "footer", "body"], value):
def _contents__setitem__(self, key: Literal["header", "footer", "body"], value) -> None:
if key not in {"body", "header", "footer"}:
raise KeyError(f"Frame.contents has no key: {key!r}")
try:
Expand All @@ -311,7 +313,7 @@ def _contents__setitem__(self, key: Literal["header", "footer", "body"], value):
else:
self.header = value_w

def _contents__delitem__(self, key: Literal["header", "footer", "body"]):
def _contents__delitem__(self, key: Literal["header", "footer", "body"]) -> None:
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) or (key == "footer" and self._footer is None):
Expand Down Expand Up @@ -397,7 +399,7 @@ def render(self, size: tuple[int, int], focus: bool = False) -> CompositeCanvas:

head = None
if htrim and htrim < hrows:
head = Filler(self.header, "top").render((maxcol, htrim), focus and self.focus_part == "header")
head = Filler(self.header, VAlign.TOP).render((maxcol, htrim), focus and self.focus_part == "header")
elif htrim:
head = self.header.render((maxcol,), focus and self.focus_part == "header")
if head.rows() != hrows:
Expand All @@ -413,7 +415,7 @@ def render(self, size: tuple[int, int], focus: bool = False) -> CompositeCanvas:

foot = None
if ftrim and ftrim < frows:
foot = Filler(self.footer, "bottom").render((maxcol, ftrim), focus and self.focus_part == "footer")
foot = Filler(self.footer, VAlign.BOTTOM).render((maxcol, ftrim), focus and self.focus_part == "footer")
elif ftrim:
foot = self.footer.render((maxcol,), focus and self.focus_part == "footer")
if foot.rows() != frows:
Expand Down Expand Up @@ -518,7 +520,7 @@ def get_cursor_coords(self, size: tuple[int, int]) -> tuple[int, int] | None:
x, y = coords
return x, y + row_adjust

def __iter__(self):
def __iter__(self) -> Iterator[Literal["header", "body", "footer"]]:
"""
Return an iterator over the positions in this Frame top to bottom.
"""
Expand All @@ -528,7 +530,7 @@ def __iter__(self):
if self._footer:
yield "footer"

def __reversed__(self):
def __reversed__(self) -> Iterator[Literal["footer", "body", "header"]]:
"""
Return an iterator over the positions in this Frame bottom to top.
"""
Expand Down
3 changes: 2 additions & 1 deletion urwid/widget/scrollable.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,8 @@ def automove_cursor() -> None:

# Disable cursor display if cursor is outside of visible canvas parts
if canv.cursor is not None:
_curscol, cursrow = canv.cursor # pylint: disable=unpacking-non-sequence
# Pylint check acts here a bit weird.
_curscol, cursrow = canv.cursor # pylint: disable=unpacking-non-sequence,useless-suppression
if cursrow >= maxrow or cursrow < 0:
canv.cursor = None

Expand Down
3 changes: 2 additions & 1 deletion urwid/widget/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,8 @@ def pack(self, size: tuple[int] | tuple[()] | None = None, focus: bool = False)
if size:
(maxcol,) = size
if not hasattr(self.layout, "pack"):
return size
return maxcol, self.rows(size, focus)

trans = self.get_line_translation(maxcol, (text, attr))
cols = self.layout.pack(maxcol, trans)
return (cols, len(trans))
Expand Down

0 comments on commit 7412fa4

Please sign in to comment.