urwid 4.2.1__tar.gz → 4.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. {urwid-4.2.1 → urwid-4.2.3}/.pre-commit-config.yaml +1 -1
  2. {urwid-4.2.1/urwid.egg-info → urwid-4.2.3}/PKG-INFO +1 -1
  3. urwid-4.2.3/pylint-requirements.txt +1 -0
  4. {urwid-4.2.1 → urwid-4.2.3}/pyproject.toml +0 -1
  5. {urwid-4.2.1 → urwid-4.2.3}/urwid/canvas.py +5 -0
  6. {urwid-4.2.1 → urwid-4.2.3}/urwid/display/_posix_raw_display.py +2 -1
  7. {urwid-4.2.1 → urwid-4.2.3}/urwid/display/_raw_display_base.py +6 -6
  8. {urwid-4.2.1 → urwid-4.2.3}/urwid/display/common.py +24 -10
  9. {urwid-4.2.1 → urwid-4.2.3}/urwid/display/curses.py +3 -0
  10. {urwid-4.2.1 → urwid-4.2.3}/urwid/display/escape.py +1 -1
  11. {urwid-4.2.1 → urwid-4.2.3}/urwid/display/web.py +21 -10
  12. {urwid-4.2.1 → urwid-4.2.3}/urwid/event_loop/main_loop.py +21 -4
  13. {urwid-4.2.1 → urwid-4.2.3}/urwid/event_loop/trio_loop.py +18 -0
  14. {urwid-4.2.1 → urwid-4.2.3}/urwid/version.py +3 -3
  15. {urwid-4.2.1 → urwid-4.2.3}/urwid/vterm.py +3 -2
  16. {urwid-4.2.1 → urwid-4.2.3}/urwid/widget/ansi_text.py +2 -2
  17. {urwid-4.2.1 → urwid-4.2.3}/urwid/widget/attr_map.py +24 -23
  18. {urwid-4.2.1 → urwid-4.2.3}/urwid/widget/attr_wrap.py +2 -2
  19. {urwid-4.2.1 → urwid-4.2.3}/urwid/widget/columns.py +9 -3
  20. {urwid-4.2.1 → urwid-4.2.3}/urwid/widget/edit.py +12 -1
  21. {urwid-4.2.1 → urwid-4.2.3}/urwid/widget/grid_flow.py +0 -3
  22. {urwid-4.2.1 → urwid-4.2.3}/urwid/widget/line_box.py +1 -1
  23. {urwid-4.2.1 → urwid-4.2.3}/urwid/widget/overlay.py +6 -0
  24. {urwid-4.2.1 → urwid-4.2.3}/urwid/widget/pile.py +9 -3
  25. {urwid-4.2.1 → urwid-4.2.3}/urwid/widget/popup.py +1 -0
  26. {urwid-4.2.1 → urwid-4.2.3}/urwid/widget/text.py +2 -0
  27. {urwid-4.2.1 → urwid-4.2.3/urwid.egg-info}/PKG-INFO +1 -1
  28. urwid-4.2.3/urwid.egg-info/scm_version.json +8 -0
  29. urwid-4.2.1/pylint-requirements.txt +0 -1
  30. urwid-4.2.1/urwid.egg-info/scm_version.json +0 -8
  31. {urwid-4.2.1 → urwid-4.2.3}/.devcontainer/dev.dockerfile +0 -0
  32. {urwid-4.2.1 → urwid-4.2.3}/.devcontainer/devcontainer.json +0 -0
  33. {urwid-4.2.1 → urwid-4.2.3}/.dockerignore +0 -0
  34. {urwid-4.2.1 → urwid-4.2.3}/.editorconfig +0 -0
  35. {urwid-4.2.1 → urwid-4.2.3}/.github/CODE_OF_CONDUCT.md +0 -0
  36. {urwid-4.2.1 → urwid-4.2.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  37. {urwid-4.2.1 → urwid-4.2.3}/.github/ISSUE_TEMPLATE/docs.md +0 -0
  38. {urwid-4.2.1 → urwid-4.2.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  39. {urwid-4.2.1 → urwid-4.2.3}/.github/ISSUE_TEMPLATE/question.md +0 -0
  40. {urwid-4.2.1 → urwid-4.2.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  41. {urwid-4.2.1 → urwid-4.2.3}/.github/dependabot.yml +0 -0
  42. {urwid-4.2.1 → urwid-4.2.3}/.github/labeler.yml +0 -0
  43. {urwid-4.2.1 → urwid-4.2.3}/.github/release.yml +0 -0
  44. {urwid-4.2.1 → urwid-4.2.3}/.github/workflows/documentation.yml +0 -0
  45. {urwid-4.2.1 → urwid-4.2.3}/.github/workflows/isolated_mypy.yml +0 -0
  46. {urwid-4.2.1 → urwid-4.2.3}/.github/workflows/isolated_pylint.yml +0 -0
  47. {urwid-4.2.1 → urwid-4.2.3}/.github/workflows/labels.yml +0 -0
  48. {urwid-4.2.1 → urwid-4.2.3}/.github/workflows/pythonpackage.yml +0 -0
  49. {urwid-4.2.1 → urwid-4.2.3}/.github/workflows/yamllint.yml +0 -0
  50. {urwid-4.2.1 → urwid-4.2.3}/.gitignore +0 -0
  51. {urwid-4.2.1 → urwid-4.2.3}/.yamllint.yml +0 -0
  52. {urwid-4.2.1 → urwid-4.2.3}/COPYING +0 -0
  53. {urwid-4.2.1 → urwid-4.2.3}/MANIFEST.in +0 -0
  54. {urwid-4.2.1 → urwid-4.2.3}/README.rst +0 -0
  55. {urwid-4.2.1 → urwid-4.2.3}/SECURITY.md +0 -0
  56. {urwid-4.2.1 → urwid-4.2.3}/classifiers.txt +0 -0
  57. {urwid-4.2.1 → urwid-4.2.3}/conftest.py +0 -0
  58. {urwid-4.2.1 → urwid-4.2.3}/mypy-requirements.txt +0 -0
  59. {urwid-4.2.1 → urwid-4.2.3}/pytest_requirements.txt +0 -0
  60. {urwid-4.2.1 → urwid-4.2.3}/requirements.txt +0 -0
  61. {urwid-4.2.1 → urwid-4.2.3}/setup.cfg +0 -0
  62. {urwid-4.2.1 → urwid-4.2.3}/tox.ini +0 -0
  63. {urwid-4.2.1 → urwid-4.2.3}/urwid/__init__.py +0 -0
  64. {urwid-4.2.1 → urwid-4.2.3}/urwid/ansi_parser.py +0 -0
  65. {urwid-4.2.1 → urwid-4.2.3}/urwid/command_map.py +0 -0
  66. {urwid-4.2.1 → urwid-4.2.3}/urwid/display/__init__.py +0 -0
  67. {urwid-4.2.1 → urwid-4.2.3}/urwid/display/_web.css +0 -0
  68. {urwid-4.2.1 → urwid-4.2.3}/urwid/display/_web.js +0 -0
  69. {urwid-4.2.1 → urwid-4.2.3}/urwid/display/_win32.py +0 -0
  70. {urwid-4.2.1 → urwid-4.2.3}/urwid/display/_win32_raw_display.py +0 -0
  71. {urwid-4.2.1 → urwid-4.2.3}/urwid/display/html_fragment.py +0 -0
  72. {urwid-4.2.1 → urwid-4.2.3}/urwid/display/lcd.py +0 -0
  73. {urwid-4.2.1 → urwid-4.2.3}/urwid/display/raw.py +0 -0
  74. {urwid-4.2.1 → urwid-4.2.3}/urwid/event_loop/__init__.py +0 -0
  75. {urwid-4.2.1 → urwid-4.2.3}/urwid/event_loop/abstract_loop.py +0 -0
  76. {urwid-4.2.1 → urwid-4.2.3}/urwid/event_loop/asyncio_loop.py +0 -0
  77. {urwid-4.2.1 → urwid-4.2.3}/urwid/event_loop/glib_loop.py +0 -0
  78. {urwid-4.2.1 → urwid-4.2.3}/urwid/event_loop/select_loop.py +0 -0
  79. {urwid-4.2.1 → urwid-4.2.3}/urwid/event_loop/tornado_loop.py +0 -0
  80. {urwid-4.2.1 → urwid-4.2.3}/urwid/event_loop/twisted_loop.py +0 -0
  81. {urwid-4.2.1 → urwid-4.2.3}/urwid/event_loop/zmq_loop.py +0 -0
  82. {urwid-4.2.1 → urwid-4.2.3}/urwid/font.py +0 -0
  83. {urwid-4.2.1 → urwid-4.2.3}/urwid/graphics.py +0 -0
  84. {urwid-4.2.1 → urwid-4.2.3}/urwid/numedit.py +0 -0
  85. {urwid-4.2.1 → urwid-4.2.3}/urwid/py.typed +0 -0
  86. {urwid-4.2.1 → urwid-4.2.3}/urwid/signals.py +0 -0
  87. {urwid-4.2.1 → urwid-4.2.3}/urwid/split_repr.py +0 -0
  88. {urwid-4.2.1 → urwid-4.2.3}/urwid/str_util.py +0 -0
  89. {urwid-4.2.1 → urwid-4.2.3}/urwid/text_layout.py +0 -0
  90. {urwid-4.2.1 → urwid-4.2.3}/urwid/util.py +0 -0
  91. {urwid-4.2.1 → urwid-4.2.3}/urwid/widget/__init__.py +0 -0
  92. {urwid-4.2.1 → urwid-4.2.3}/urwid/widget/bar_graph.py +0 -0
  93. {urwid-4.2.1 → urwid-4.2.3}/urwid/widget/big_text.py +0 -0
  94. {urwid-4.2.1 → urwid-4.2.3}/urwid/widget/box_adapter.py +0 -0
  95. {urwid-4.2.1 → urwid-4.2.3}/urwid/widget/constants.py +0 -0
  96. {urwid-4.2.1 → urwid-4.2.3}/urwid/widget/container.py +0 -0
  97. {urwid-4.2.1 → urwid-4.2.3}/urwid/widget/deque_walker.py +0 -0
  98. {urwid-4.2.1 → urwid-4.2.3}/urwid/widget/divider.py +0 -0
  99. {urwid-4.2.1 → urwid-4.2.3}/urwid/widget/filler.py +0 -0
  100. {urwid-4.2.1 → urwid-4.2.3}/urwid/widget/frame.py +0 -0
  101. {urwid-4.2.1 → urwid-4.2.3}/urwid/widget/list_walker.py +0 -0
  102. {urwid-4.2.1 → urwid-4.2.3}/urwid/widget/listbox.py +0 -0
  103. {urwid-4.2.1 → urwid-4.2.3}/urwid/widget/monitored_deque.py +0 -0
  104. {urwid-4.2.1 → urwid-4.2.3}/urwid/widget/monitored_list.py +0 -0
  105. {urwid-4.2.1 → urwid-4.2.3}/urwid/widget/padding.py +0 -0
  106. {urwid-4.2.1 → urwid-4.2.3}/urwid/widget/progress_bar.py +0 -0
  107. {urwid-4.2.1 → urwid-4.2.3}/urwid/widget/scrollable.py +0 -0
  108. {urwid-4.2.1 → urwid-4.2.3}/urwid/widget/solid_fill.py +0 -0
  109. {urwid-4.2.1 → urwid-4.2.3}/urwid/widget/treetools.py +0 -0
  110. {urwid-4.2.1 → urwid-4.2.3}/urwid/widget/widget.py +0 -0
  111. {urwid-4.2.1 → urwid-4.2.3}/urwid/widget/widget_decoration.py +0 -0
  112. {urwid-4.2.1 → urwid-4.2.3}/urwid/widget/wimp.py +0 -0
  113. {urwid-4.2.1 → urwid-4.2.3}/urwid.egg-info/SOURCES.txt +0 -0
  114. {urwid-4.2.1 → urwid-4.2.3}/urwid.egg-info/dependency_links.txt +0 -0
  115. {urwid-4.2.1 → urwid-4.2.3}/urwid.egg-info/not-zip-safe +0 -0
  116. {urwid-4.2.1 → urwid-4.2.3}/urwid.egg-info/requires.txt +0 -0
  117. {urwid-4.2.1 → urwid-4.2.3}/urwid.egg-info/scm_file_list.json +0 -0
  118. {urwid-4.2.1 → urwid-4.2.3}/urwid.egg-info/top_level.txt +0 -0
  119. {urwid-4.2.1 → urwid-4.2.3}/yamllint-requirements.txt +0 -0
@@ -12,7 +12,7 @@ repos:
12
12
  - id: check-merge-conflict
13
13
 
14
14
  - repo: https://github.com/astral-sh/ruff-pre-commit
15
- rev: v0.16.0
15
+ rev: v0.16.9
16
16
  hooks:
17
17
  - id: ruff-check
18
18
  args: [ --show-fixes, --fix ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: urwid
3
- Version: 4.2.1
3
+ Version: 4.2.3
4
4
  Summary: A full-featured console (xterm et al.) user interface library
5
5
  Author-email: Ian Ward <ian@excess.org>
6
6
  License-Expression: LGPL-2.1-only
@@ -0,0 +1 @@
1
+ pylint==4.1.1
@@ -197,7 +197,6 @@ allow-dunder-method-names = [
197
197
  [tool.pylint]
198
198
  disable = [
199
199
  "arguments-differ",
200
- "attribute-defined-outside-init", # old bad practice, fixed step-by-step
201
200
  "broad-exception-caught",
202
201
  "broad-exception-raised",
203
202
  "consider-using-assignment-expr",
@@ -224,6 +224,8 @@ class CanvasError(Exception):
224
224
  class Canvas:
225
225
  """base class for canvases."""
226
226
 
227
+ __slots__ = ("__dict__", "__weakref__", "_widget_info", "coords", "shortcuts")
228
+
227
229
  cacheable = True
228
230
 
229
231
  _finalized_error = CanvasError(
@@ -601,6 +603,8 @@ class BlankCanvas(Canvas):
601
603
  Only works as part of a composite canvas since it doesn't know its own size.
602
604
  """
603
605
 
606
+ __slots__ = ()
607
+
604
608
  def content(
605
609
  self,
606
610
  trim_left: int = 0,
@@ -733,6 +737,7 @@ class CompositeCanvas(Canvas):
733
737
 
734
738
  # tuples that define the unfinished cviews that are part of shards following the first shard.
735
739
  super().__init__()
740
+ self.depends_on: Sequence[AbstractWidget] | None = None
736
741
 
737
742
  if canv is None:
738
743
  self.shards: list[tuple[int, list[_CView]]] = []
@@ -122,6 +122,7 @@ class Screen(_raw_display_base.Screen):
122
122
  super().__init__(input, output, bracketed_paste_mode=bracketed_paste_mode, focus_reporting=focus_reporting)
123
123
  self.gpm_mev: Popen[str] | None = None
124
124
  self.gpm_event_pending: bool = False
125
+ self._old_termios_settings: list[typing.Any] | None = None
125
126
 
126
127
  # These store the previous signal handlers after setting ours
127
128
  self._prev_sigcont_handler: SignalHandler = None
@@ -308,7 +309,7 @@ class Screen(_raw_display_base.Screen):
308
309
  self._stop_restore_palette()
309
310
 
310
311
  fd = self._input_fileno()
311
- if fd is not None and os.isatty(fd):
312
+ if fd is not None and os.isatty(fd) and self._old_termios_settings is not None:
312
313
  termios.tcsetattr(fd, termios.TCSAFLUSH, self._old_termios_settings)
313
314
 
314
315
  if self._old_signal_keys:
@@ -41,7 +41,7 @@ from . import escape
41
41
  from .common import UNPRINTABLE_TRANS_TABLE, UPDATE_PALETTE_ENTRY, AttrSpec, BaseScreen, RealTerminal
42
42
 
43
43
  if typing.TYPE_CHECKING:
44
- from collections.abc import Callable, Iterable, Mapping, Sequence
44
+ from collections.abc import Callable, Hashable, Iterable, Mapping, Sequence
45
45
  from types import FrameType
46
46
 
47
47
  from typing_extensions import Literal
@@ -353,8 +353,8 @@ class Screen(BaseScreen, RealTerminal):
353
353
 
354
354
  self._partial_codes: list[int] = []
355
355
  self.modes = TermModes(bracketed_paste=bracketed_paste_mode, focus_reporting=focus_reporting)
356
- self._pal_escape: dict[str | None, str] = {}
357
- self._pal_attrspec: dict[str | None, AttrSpec] = {}
356
+ self._pal_escape: dict[Hashable, str] = {}
357
+ self._pal_attrspec: dict[Hashable, AttrSpec] = {}
358
358
  self._modified_palette_entries: set[int] = set()
359
359
  signals.connect_signal(self, UPDATE_PALETTE_ENTRY, self._on_update_palette_entry)
360
360
  self.term = os.environ.get("TERM", "")
@@ -423,7 +423,7 @@ class Screen(BaseScreen, RealTerminal):
423
423
 
424
424
  return None
425
425
 
426
- def _on_update_palette_entry(self, name: str | None, *attrspecs: AttrSpec) -> None:
426
+ def _on_update_palette_entry(self, name: Hashable, *attrspecs: AttrSpec) -> None:
427
427
  # copy the attribute to a dictionary containing the escape seqences
428
428
  a: AttrSpec = attrspecs[_ATTRSPEC_INDEX_BY_COLORS[self.colors]]
429
429
  self._pal_attrspec[name] = a
@@ -1046,7 +1046,7 @@ class Screen(BaseScreen, RealTerminal):
1046
1046
  run[-1:] == b" "
1047
1047
  and self.back_color_erase
1048
1048
  and not (
1049
- isinstance(pal_a := self._pal_attrspec.get(a, a), AttrSpec) # type: ignore[arg-type]
1049
+ isinstance(pal_a := self._pal_attrspec.get(a, a), AttrSpec)
1050
1050
  and (pal_a.standout or pal_a.underline)
1051
1051
  )
1052
1052
  ):
@@ -1152,7 +1152,7 @@ class Screen(BaseScreen, RealTerminal):
1152
1152
 
1153
1153
  def _attr_to_escape(self, a: AttrSpec | str | None) -> str:
1154
1154
  """Convert attribute instance a to an escape sequence for the terminal."""
1155
- if found := self._pal_escape.get(a): # type: ignore[arg-type]
1155
+ if found := self._pal_escape.get(a):
1156
1156
  return found
1157
1157
  if isinstance(a, AttrSpec):
1158
1158
  return self._attrspec_to_escape(a)
@@ -34,12 +34,31 @@ from urwid import signals
34
34
  from urwid.util import StoppingContext, int_scale
35
35
 
36
36
  if typing.TYPE_CHECKING:
37
- from collections.abc import Callable, Iterable, Sequence
37
+ from collections.abc import Callable, Hashable, Iterable, Sequence
38
38
 
39
39
  from typing_extensions import Literal, Self
40
40
 
41
41
  from urwid import Canvas
42
42
 
43
+ # One register_palette() item:
44
+ # (name, like_other_name)
45
+ # or the 3, 4 or 6 positional arguments of register_palette_entry().
46
+ # A name is any hashable value, None being the default attribute.
47
+ # typing.Union rather than "|": the alias is evaluated by the checkers as Python 3.9 code.
48
+ PaletteEntry = typing.Union[
49
+ tuple[Hashable, Hashable],
50
+ tuple[Hashable, str, str],
51
+ tuple[Hashable, str, str, typing.Union[str, tuple[str, ...], None]],
52
+ tuple[
53
+ Hashable,
54
+ str,
55
+ str,
56
+ typing.Union[str, tuple[str, ...], None],
57
+ typing.Optional[str],
58
+ typing.Optional[str],
59
+ ],
60
+ ]
61
+
43
62
  IS_WINDOWS = sys.platform == "win32"
44
63
 
45
64
  # for replacing unprintable bytes with '?'
@@ -1089,7 +1108,7 @@ class BaseScreen(abc.ABC, metaclass=signals.MetaSignals):
1089
1108
 
1090
1109
  self.logger = logging.getLogger(f"{self.__class__.__module__}.{self.__class__.__name__}")
1091
1110
 
1092
- self._palette: dict[str | None, tuple[AttrSpec, AttrSpec, AttrSpec, AttrSpec, AttrSpec]] = {}
1111
+ self._palette: dict[Hashable, tuple[AttrSpec, AttrSpec, AttrSpec, AttrSpec, AttrSpec]] = {}
1093
1112
  self._started: bool = False
1094
1113
 
1095
1114
  @property
@@ -1170,12 +1189,7 @@ class BaseScreen(abc.ABC, metaclass=signals.MetaSignals):
1170
1189
  """
1171
1190
  return 80, 24
1172
1191
 
1173
- def register_palette(
1174
- self,
1175
- palette: Iterable[
1176
- tuple[str, str] | tuple[str, str, str] | tuple[str, str, str, str] | tuple[str, str, str, str, str, str]
1177
- ],
1178
- ) -> None:
1192
+ def register_palette(self, palette: Iterable[PaletteEntry]) -> None:
1179
1193
  """Register a set of palette entries.
1180
1194
 
1181
1195
  :param palette: a list of (name, like_other_name) or
@@ -1204,7 +1218,7 @@ class BaseScreen(abc.ABC, metaclass=signals.MetaSignals):
1204
1218
 
1205
1219
  def register_palette_entry(
1206
1220
  self,
1207
- name: str | None,
1221
+ name: Hashable,
1208
1222
  foreground: str,
1209
1223
  background: str,
1210
1224
  mono: str | tuple[str, ...] | None = None,
@@ -1213,7 +1227,7 @@ class BaseScreen(abc.ABC, metaclass=signals.MetaSignals):
1213
1227
  ) -> None:
1214
1228
  """Register a single palette entry.
1215
1229
 
1216
- :param name: new entry/attribute name
1230
+ :param name: new entry/attribute name, any hashable value; ``None`` names the default attribute
1217
1231
 
1218
1232
  :param foreground: a string containing a comma-separated foreground
1219
1233
  color and settings
@@ -117,6 +117,7 @@ class Screen(BaseScreen, RealTerminal):
117
117
  super().__init__()
118
118
  self.curses_pairs = [(None, None)] # Can't be sure what pair 0 will default to
119
119
  self.has_color = False
120
+ self.has_default_colors = False
120
121
  self.s: curses.window | None = None
121
122
  self.cursor_state: typing.Literal["fixed"] | int | None = None
122
123
  self.prev_input_resize = 0
@@ -124,6 +125,8 @@ class Screen(BaseScreen, RealTerminal):
124
125
  self.last_bstate = 0
125
126
  self._mouse_tracking_enabled = False
126
127
  self._curses_attr_cache: dict[AttrSpec, int] = {}
128
+ # Holds the last drawn canvas so the canvas cache keeps its entries alive.
129
+ self.keep_cache_alive_link: Canvas | None = None
127
130
 
128
131
  self.register_palette_entry(None, "default", "default")
129
132
 
@@ -41,7 +41,7 @@ if typing.TYPE_CHECKING:
41
41
 
42
42
  # NOTE: because of circular imports (urwid.util -> urwid.escape -> urwid.util)
43
43
  # from urwid.util import is_mouse_event -- will not work here
44
- import urwid.util # isort: skip # pylint: disable=wrong-import-position
44
+ import urwid.util # isort: skip
45
45
 
46
46
  IS_WINDOWS = sys.platform == "win32"
47
47
 
@@ -130,6 +130,15 @@ class Screen(BaseScreen):
130
130
  self.bright_is_bold = False # ignored: the browser renders bold as requested
131
131
  self.has_underline = True # ignored: the browser renders underline as requested
132
132
  self._colour_index = _COLOUR_INDEX[self.colors]
133
+ self.update_method = ""
134
+ self.last_screen: dict[tuple[tuple[AttrSpec | str | None, str] | int | None, ...], list[int]] = {}
135
+ self.last_screen_width = 0
136
+ self.pipe_name = ""
137
+ self.input_fd: int | None = None
138
+ self.input_tail = ""
139
+ self.content_head = ""
140
+ self.screen_size: tuple[int, int] | None = None
141
+ self.server_socket: socket.socket | None = None
133
142
  self.register_palette_entry(None, _default_foreground, _default_background)
134
143
 
135
144
  @property
@@ -216,7 +225,7 @@ class Screen(BaseScreen):
216
225
  sys.stdout.write("Status: 400 Bad Request\r\n\r\n")
217
226
  sys.exit(0)
218
227
 
219
- self.last_screen: dict[tuple[tuple[AttrSpec | str | None, str] | int | None, ...], list[int]] = {}
228
+ self.last_screen = {}
220
229
  self.last_screen_width = 0
221
230
 
222
231
  clients = glob.glob(os.path.join(_prefs.pipe_dir, "urwid*.in"))
@@ -252,8 +261,9 @@ class Screen(BaseScreen):
252
261
  with suppress(Exception):
253
262
  self._close_connection()
254
263
  signal.signal(signal.SIGTERM, signal.SIG_DFL)
255
- with suppress(Exception):
256
- os.close(self.input_fd)
264
+ if self.input_fd is not None:
265
+ with suppress(Exception):
266
+ os.close(self.input_fd)
257
267
  self._cleanup_pipe()
258
268
  self._started = False
259
269
 
@@ -261,7 +271,7 @@ class Screen(BaseScreen):
261
271
  """Not supported for web display."""
262
272
 
263
273
  def _close_connection(self) -> None:
264
- if self.update_method == "polling child":
274
+ if self.update_method == "polling child" and self.server_socket is not None:
265
275
  self.server_socket.settimeout(0)
266
276
  sock, _addr = self.server_socket.accept()
267
277
  sock.sendall(b"Z")
@@ -303,7 +313,7 @@ class Screen(BaseScreen):
303
313
  elif self.update_method == "polling child":
304
314
  signal.alarm(0)
305
315
  try:
306
- s, _addr = self.server_socket.accept()
316
+ s, _addr = typing.cast("socket.socket", self.server_socket).accept()
307
317
  except socket.timeout:
308
318
  sys.exit(0)
309
319
  send = s.sendall # type: ignore[assignment] # use default flags
@@ -409,7 +419,7 @@ class Screen(BaseScreen):
409
419
  if self.update_method == "polling child":
410
420
  # send empty update
411
421
  try:
412
- s, _addr = self.server_socket.accept()
422
+ s, _addr = typing.cast("socket.socket", self.server_socket).accept()
413
423
  s.close()
414
424
  except socket.timeout:
415
425
  sys.exit(0)
@@ -421,7 +431,7 @@ class Screen(BaseScreen):
421
431
 
422
432
  def get_cols_rows(self) -> tuple[int, int]:
423
433
  """Return the screen size."""
424
- return self.screen_size
434
+ return typing.cast("tuple[int, int]", self.screen_size)
425
435
 
426
436
  @typing.overload
427
437
  def get_input(self, raw_keys: Literal[False]) -> list[str]: ...
@@ -433,8 +443,9 @@ class Screen(BaseScreen):
433
443
  """Return pending input as a list."""
434
444
  pending_input = []
435
445
  resized = False
446
+ input_fd = typing.cast("int", self.input_fd)
436
447
  with selectors.DefaultSelector() as selector:
437
- selector.register(self.input_fd, selectors.EVENT_READ)
448
+ selector.register(input_fd, selectors.EVENT_READ)
438
449
 
439
450
  iready = [event.fd for event, _ in selector.select(0.5)]
440
451
 
@@ -443,8 +454,8 @@ class Screen(BaseScreen):
443
454
  return [], []
444
455
  return []
445
456
 
446
- keydata = os.read(self.input_fd, MAX_READ).decode(get_encoding())
447
- os.close(self.input_fd)
457
+ keydata = os.read(input_fd, MAX_READ).decode(get_encoding())
458
+ os.close(input_fd)
448
459
  self.input_fd = os.open(f"{self.pipe_name}.in", os.O_NONBLOCK | os.O_RDONLY)
449
460
  # sys.stderr.write( repr((keydata,self.input_tail))+"\n" )
450
461
  keys = keydata.split("\n")
@@ -48,6 +48,7 @@ if typing.TYPE_CHECKING:
48
48
  from typing_extensions import Literal, Self
49
49
 
50
50
  from urwid.display import BaseScreen
51
+ from urwid.display.common import PaletteEntry
51
52
  from urwid.widget import AbstractWidget
52
53
 
53
54
  from .abstract_loop import EventLoop
@@ -143,12 +144,27 @@ class MainLoop:
143
144
  :attr:`event_loop`; whether it actually does anything still depends on :attr:`event_loop`.
144
145
  """
145
146
 
147
+ __slots__ = (
148
+ "__dict__",
149
+ "__weakref__",
150
+ "_input_filter",
151
+ "_pop_ups",
152
+ "_topmost_widget",
153
+ "_unhandled_input",
154
+ "_watch_pipes",
155
+ "_widget",
156
+ "event_loop",
157
+ "handle_mouse",
158
+ "idle_handle",
159
+ "logger",
160
+ "screen",
161
+ "screen_size",
162
+ )
163
+
146
164
  def __init__(
147
165
  self,
148
166
  widget: AbstractWidget,
149
- palette: Iterable[
150
- tuple[str, str] | tuple[str, str, str] | tuple[str, str, str, str] | tuple[str, str, str, str, str, str]
151
- ] = (),
167
+ palette: Iterable[PaletteEntry] = (),
152
168
  screen: BaseScreen | None = None,
153
169
  handle_mouse: bool = True,
154
170
  input_filter: (
@@ -187,6 +203,7 @@ class MainLoop:
187
203
  if event_loop is None:
188
204
  event_loop = SelectEventLoop()
189
205
  self.event_loop: EventLoop = event_loop
206
+ self.idle_handle: typing.Any = None
190
207
 
191
208
  if hasattr(self.screen, "signal_handler_setter"):
192
209
  # Tell the screen what function it must use to set
@@ -477,7 +494,7 @@ class MainLoop:
477
494
  Only call this if you're managing the event loop yourself, after the loop stops.
478
495
  """
479
496
  self.event_loop.remove_enter_idle(self.idle_handle)
480
- del self.idle_handle
497
+ self.idle_handle = None
481
498
  signals.disconnect_signal(self.screen, INPUT_DESCRIPTORS_CHANGED, self._reset_input_descriptors)
482
499
  typing.cast("_ExternalLoopScreen", self.screen).unhook_event_loop(self.event_loop)
483
500
 
@@ -39,7 +39,9 @@ if sys.version_info < (3, 11):
39
39
  from exceptiongroup import BaseExceptionGroup # pylint: disable=redefined-builtin # backport
40
40
 
41
41
  if typing.TYPE_CHECKING:
42
+ import asyncio
42
43
  from collections.abc import Awaitable, Callable
44
+ from concurrent.futures import Executor, Future
43
45
 
44
46
  from typing_extensions import ParamSpec
45
47
 
@@ -96,6 +98,22 @@ class TrioEventLoop(EventLoop):
96
98
  self._sleep = trio.sleep
97
99
  self._wait_readable = trio.lowlevel.wait_readable
98
100
 
101
+ def run_in_executor(
102
+ self,
103
+ executor: Executor,
104
+ func: Callable[_Spec, _T],
105
+ *args: _Spec.args,
106
+ **kwargs: _Spec.kwargs,
107
+ ) -> Future[_T] | asyncio.Future[_T]:
108
+ """Raise :exc:`NotImplementedError`: use Trio's own thread API.
109
+
110
+ :raises NotImplementedError: Trio runs blocking calls in threads itself; use ``trio.to_thread.run_sync``.
111
+ """
112
+ raise NotImplementedError(
113
+ "Trio implements its own worker threads. Please use native API for call:\n"
114
+ "'await trio.to_thread.run_sync(Callable[..., T], *args)'"
115
+ )
116
+
99
117
  def alarm(
100
118
  self,
101
119
  seconds: float,
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '4.2.1'
22
- __version_tuple__ = version_tuple = (4, 2, 1)
21
+ __version__ = version = '4.2.3'
22
+ __version_tuple__ = version_tuple = (4, 2, 3)
23
23
 
24
- __commit_id__ = commit_id = 'gf0dabd13b'
24
+ __commit_id__ = commit_id = 'gbb4bfdce9'
@@ -1377,6 +1377,7 @@ class Terminal(Widget):
1377
1377
  self.height: int | None = None
1378
1378
  self.term: TermCanvas | None = None
1379
1379
  self.has_focus = False
1380
+ self.old_tios: tuple[int, int, int, int, int] | None = None
1380
1381
  self.terminated = False
1381
1382
 
1382
1383
  def get_cursor_coords(self, size: tuple[int, int]) -> tuple[int, int] | None:
@@ -1519,8 +1520,8 @@ class Terminal(Widget):
1519
1520
  if has_focus:
1520
1521
  self.old_tios = RealTerminal().tty_signal_keys()
1521
1522
  RealTerminal().tty_signal_keys(*(["undefined"] * 5)) # type: ignore[arg-type]
1522
- elif old_tios := getattr(self, "old_tios", ()):
1523
- RealTerminal().tty_signal_keys(*old_tios) # pylint: disable=not-an-iterable
1523
+ elif self.old_tios:
1524
+ RealTerminal().tty_signal_keys(*self.old_tios)
1524
1525
 
1525
1526
  def render(self, size: tuple[int, int], focus: bool = False) -> TermCanvas: # type: ignore[override]
1526
1527
  """Resize the terminal to *size*, read any pending child output, and return the terminal canvas."""
@@ -22,7 +22,7 @@ from __future__ import annotations
22
22
 
23
23
  import typing
24
24
 
25
- from urwid.ansi_parser import AnsiParser
25
+ from urwid.ansi_parser import AnsiParser, ParsedLine
26
26
 
27
27
  from .constants import Align, WrapMode
28
28
  from .text import Text
@@ -33,7 +33,6 @@ if typing.TYPE_CHECKING:
33
33
 
34
34
  from typing_extensions import Literal
35
35
 
36
- from urwid.ansi_parser import ParsedLine
37
36
  from urwid.display import AttrSpec
38
37
 
39
38
  __all__ = ("ANSIText",)
@@ -93,6 +92,7 @@ class ANSIText(WidgetWrap[Text]):
93
92
  """
94
93
  self._one_line = one_line
95
94
  super().__init__(Text("", align=align, wrap=wrap))
95
+ self._parsed = ParsedLine("", [], previous_attr)
96
96
  self.set_ansi_text(ansi_text, previous_attr)
97
97
 
98
98
  def _repr_words(self) -> list[str]:
@@ -67,18 +67,10 @@ class AttrMap(
67
67
  [('greeting', None, ...'hi'), ('bg', None, ...' ')]
68
68
  """
69
69
  super().__init__(w)
70
-
71
- if isinstance(attr_map, Mapping):
72
- self.attr_map = dict(attr_map)
73
- else:
74
- self.attr_map = {None: attr_map}
75
-
76
- if isinstance(focus_map, Mapping):
77
- self.focus_map = dict(focus_map)
78
- elif focus_map is None:
79
- self.focus_map = focus_map
80
- else:
81
- self.focus_map = {None: focus_map}
70
+ self._attr_map: dict[Hashable, Hashable] = {}
71
+ self._focus_map: dict[Hashable, Hashable] | None = None
72
+ self.set_attr_map(attr_map)
73
+ self.set_focus_map(focus_map)
82
74
 
83
75
  def _repr_attrs(self) -> dict[str, typing.Any]:
84
76
  # only include the focus_attr when it takes effect (not None)
@@ -93,28 +85,31 @@ class AttrMap(
93
85
  # FIXME: a dictionary that detects modifications would be better
94
86
  return dict(self._attr_map)
95
87
 
96
- def set_attr_map(self, attr_map: dict[Hashable, Hashable]) -> None:
88
+ def set_attr_map(self, attr_map: Hashable | Mapping[Hashable, Hashable]) -> None:
97
89
  """
98
90
  Set the attribute mapping dictionary {from_attr: to_attr, ...}.
99
91
 
100
- Note this function does not accept a single attribute the way the
101
- constructor does. You must specify {None: attribute} instead.
92
+ A single attribute is stored as ``{None: attribute}``, the way the constructor does.
102
93
 
103
94
  >>> from urwid import Text
104
95
  >>> w = AttrMap(Text("hi"), None)
105
96
  >>> w.set_attr_map({"a": "b"})
106
97
  >>> w
107
98
  <AttrMap fixed/flow widget <Text fixed/flow widget 'hi'> attr_map={'a': 'b'}>
99
+ >>> w.set_attr_map("c")
100
+ >>> w
101
+ <AttrMap fixed/flow widget <Text fixed/flow widget 'hi'> attr_map={None: 'c'}>
108
102
 
109
103
  :raises AttrMapError: a key or value of the mapping is not hashable.
110
104
  """
111
- for from_attr, to_attr in attr_map.items():
105
+ mapping: dict[Hashable, Hashable] = dict(attr_map) if isinstance(attr_map, Mapping) else {None: attr_map}
106
+ for from_attr, to_attr in mapping.items():
112
107
  if not isinstance(from_attr, Hashable) or not isinstance(to_attr, Hashable):
113
108
  raise AttrMapError(
114
109
  f"{from_attr!r}:{to_attr!r} attribute mapping is invalid. Attributes must be hashable"
115
110
  )
116
111
 
117
- self._attr_map = attr_map
112
+ self._attr_map = mapping
118
113
  self._invalidate()
119
114
 
120
115
  attr_map = property(get_attr_map, set_attr_map)
@@ -127,33 +122,39 @@ class AttrMap(
127
122
  return dict(self._focus_map)
128
123
  return None
129
124
 
130
- def set_focus_map(self, focus_map: dict[Hashable, Hashable] | None) -> None:
125
+ def set_focus_map(self, focus_map: Hashable | Mapping[Hashable, Hashable] | None) -> None:
131
126
  """Set the focus attribute mapping dictionary {from_attr: to_attr, ...}.
132
127
 
133
128
  If None this widget will use the attr mapping instead (no change
134
129
  when in focus).
135
130
 
136
- Note this function does not accept a single attribute the way the
137
- constructor does. You must specify {None: attribute} instead.
131
+ A single attribute is stored as ``{None: attribute}``, the way the constructor does.
138
132
 
139
133
  >>> from urwid import Text
140
134
  >>> w = AttrMap(Text("hi"), {})
141
135
  >>> w.set_focus_map({"a": "b"})
142
136
  >>> w
143
137
  <AttrMap fixed/flow widget <Text fixed/flow widget 'hi'> attr_map={} focus_map={'a': 'b'}>
138
+ >>> w.set_focus_map("c")
139
+ >>> w
140
+ <AttrMap fixed/flow widget <Text fixed/flow widget 'hi'> attr_map={} focus_map={None: 'c'}>
144
141
  >>> w.set_focus_map(None)
145
142
  >>> w
146
143
  <AttrMap fixed/flow widget <Text fixed/flow widget 'hi'> attr_map={}>
147
144
 
148
145
  :raises AttrMapError: a key or value of the mapping is not hashable.
149
146
  """
150
- if focus_map is not None:
151
- for from_attr, to_attr in focus_map.items():
147
+ mapping: dict[Hashable, Hashable] | None
148
+ if focus_map is None:
149
+ mapping = None
150
+ else:
151
+ mapping = dict(focus_map) if isinstance(focus_map, Mapping) else {None: focus_map}
152
+ for from_attr, to_attr in mapping.items():
152
153
  if not isinstance(from_attr, Hashable) or not isinstance(to_attr, Hashable):
153
154
  raise AttrMapError(
154
155
  f"{from_attr!r}:{to_attr!r} attribute mapping is invalid. Attributes must be hashable"
155
156
  )
156
- self._focus_map = focus_map
157
+ self._focus_map = mapping
157
158
  self._invalidate()
158
159
 
159
160
  focus_map = property(get_focus_map, set_focus_map)
@@ -114,7 +114,7 @@ class AttrWrap(AttrMap[WrappedWidget]):
114
114
  >> w
115
115
  <AttrWrap flow widget <Divider flow widget '-'> attr='new_attr'>
116
116
  """
117
- self.set_attr_map({None: attr})
117
+ self.set_attr_map(attr)
118
118
 
119
119
  attr = property(get_attr, set_attr)
120
120
 
@@ -138,7 +138,7 @@ class AttrWrap(AttrMap[WrappedWidget]):
138
138
  >> w
139
139
  <AttrWrap flow widget <Divider flow widget '-'> attr='old'>
140
140
  """
141
- self.set_focus_map({None: focus_attr})
141
+ self.set_focus_map(focus_attr)
142
142
 
143
143
  focus_attr = property(get_focus_attr, set_focus_attr)
144
144
 
@@ -275,7 +275,6 @@ class Columns(
275
275
  *box_columns* will be displayed with this calculated number of rows,
276
276
  filling the full height.
277
277
  """
278
- self._selectable = False
279
278
  self._cache_column_widths: list[int] = []
280
279
  self._cache_pack_widths: dict[int, int] = {}
281
280
  super().__init__()
@@ -401,10 +400,17 @@ class Columns(
401
400
  return len(self._contents)
402
401
 
403
402
  def _contents_modified(self) -> None:
404
- """Recalculate whether this widget should be selectable whenever the contents has been changed."""
405
- self._selectable = any(w.selectable() for w, o in self.contents)
403
+ """Invalidate this widget whenever the contents has been changed."""
406
404
  self._invalidate()
407
405
 
406
+ def selectable(self) -> bool:
407
+ """Return whether any child widget is selectable.
408
+
409
+ Asked on every call rather than cached,
410
+ since a child such as :class:`~urwid.WidgetPlaceholder` may change its selectability after it was added.
411
+ """
412
+ return any(w.selectable() for w, _ in self.contents)
413
+
408
414
  def _validate_contents_modified(
409
415
  self,
410
416
  slc: tuple[int, int, int],
@@ -676,6 +676,11 @@ class Edit(WidgetWrap[Text]):
676
676
  if not self._shift_view_to_cursor:
677
677
  return trans
678
678
 
679
+ if self._w.wrap in (WrapMode.SPACE, WrapMode.ANY):
680
+ # Wrapping modes grow the number of rows instead of clipping a row's width,
681
+ # so there is never a partially hidden row to scroll horizontally into view.
682
+ return trans
683
+
679
684
  text, _ignore = self.get_text()
680
685
  x, y = text_layout.calc_coords(text, trans, self.edit_pos + len(self.caption))
681
686
  if x < 0:
@@ -704,7 +709,13 @@ class Edit(WidgetWrap[Text]):
704
709
  (maxcol,) = size
705
710
 
706
711
  self._shift_view_to_cursor = True
707
- return self.position_coords(maxcol, self.edit_pos)
712
+ x, y = self.position_coords(maxcol, self.edit_pos)
713
+ if self._w.wrap in (WrapMode.SPACE, WrapMode.ANY) and x >= maxcol:
714
+ # The cursor sits right after a row that is already full and entirely visible.
715
+ # Clamp onto its last column instead of scrolling that row out of view for a wrap
716
+ # that has not happened yet.
717
+ x = maxcol - 1
718
+ return x, y
708
719
 
709
720
  def position_coords(self, maxcol: int, pos: int) -> tuple[int, int]:
710
721
  """Return (*x*, *y*) coordinates for an offset into self.edit_text."""
@@ -475,9 +475,6 @@ class GridFlow(
475
475
  if self.v_sep:
476
476
  # remove first divider
477
477
  del p.contents[:1]
478
- else:
479
- # Ensure p __selectable is updated
480
- p._contents_modified() # pylint: disable=protected-access
481
478
 
482
479
  return p
483
480
 
@@ -192,7 +192,7 @@ class LineBox(
192
192
  middle: Columns = typing.cast("Columns", self._wrapped_widget[v_index])
193
193
  _old_widget, options = middle.contents[h_index]
194
194
  middle.contents[h_index] = (original_widget, options) # type: ignore[assignment]
195
- WidgetDecoration.original_widget.fset(self, original_widget) # type: ignore[attr-defined] # pylint: disable=no-member
195
+ WidgetDecoration.original_widget.fset(self, original_widget) # type: ignore[attr-defined]
196
196
 
197
197
  @property
198
198
  def _w(self) -> Pile:
@@ -183,6 +183,12 @@ class Overlay(
183
183
 
184
184
  self.top_w = top_w
185
185
  self.bottom_w = bottom_w
186
+ self.min_width: int | None = None
187
+ self.min_height: int | None = None
188
+ self.left = 0
189
+ self.right = 0
190
+ self.top = 0
191
+ self.bottom = 0
186
192
 
187
193
  self.set_overlay_parameters(align, width, valign, height, min_width, min_height, left, right, top, bottom)
188
194
 
@@ -223,7 +223,6 @@ class Pile(
223
223
  .. note:: If the Pile is treated as a box widget there must be at least
224
224
  one ``'weight'`` tuple in :attr:`widget_list`.
225
225
  """
226
- self._selectable = False
227
226
  super().__init__()
228
227
  self._contents: MonitoredFocusList[
229
228
  tuple[
@@ -344,10 +343,17 @@ class Pile(
344
343
  return len(self._contents)
345
344
 
346
345
  def _contents_modified(self) -> None:
347
- """Recalculate whether this widget should be selectable whenever the contents has been changed."""
348
- self._selectable = any(w.selectable() for w, o in self.contents)
346
+ """Invalidate this widget whenever the contents has been changed."""
349
347
  self._invalidate()
350
348
 
349
+ def selectable(self) -> bool:
350
+ """Return whether any child widget is selectable.
351
+
352
+ Asked on every call rather than cached,
353
+ since a child such as :class:`~urwid.WidgetPlaceholder` may change its selectability after it was added.
354
+ """
355
+ return any(w.selectable() for w, _ in self.contents)
356
+
351
357
  def _validate_contents_modified(
352
358
  self,
353
359
  slc: tuple[int, int, int],
@@ -129,6 +129,7 @@ class PopUpTarget(WidgetDecoration[WrappedWidget]):
129
129
  # One (widget, Overlay) entry per currently open pop-up, outermost first.
130
130
  self._pop_up_levels: list[tuple[AbstractWidget, Overlay[AbstractWidget, AbstractWidget]]] = []
131
131
  self._current_widget: AbstractWidget = self._original_widget
132
+ self._cache_original_canvas: Canvas | None = None
132
133
 
133
134
  def _update_overlay(self, size: tuple[int, int], focus: bool) -> None:
134
135
  """Rebuild :attr:`_current_widget` as a chain of Overlay widgets, one per open pop-up.
@@ -75,6 +75,8 @@ class Text(Widget):
75
75
  self._layout: text_layout.TextLayout = layout or text_layout.default_layout
76
76
  self._text: str | bytes
77
77
  self._attrib: list[tuple[Hashable, int]]
78
+ self._align_mode: Literal["left", "center", "right"] | Align = Align.LEFT
79
+ self._wrap_mode: Literal["space", "any", "clip", "ellipsis"] | WrapMode = WrapMode.SPACE
78
80
  self.set_text(markup)
79
81
  self.set_layout(align, wrap, layout)
80
82
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: urwid
3
- Version: 4.2.1
3
+ Version: 4.2.3
4
4
  Summary: A full-featured console (xterm et al.) user interface library
5
5
  Author-email: Ian Ward <ian@excess.org>
6
6
  License-Expression: LGPL-2.1-only
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "4.2.3",
3
+ "distance": 0,
4
+ "node": "gbb4bfdce968f57f77bdbd69b5ad865dc4ea956b8",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-09-30"
8
+ }
@@ -1 +0,0 @@
1
- pylint==4.0.9
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "4.2.1",
3
- "distance": 0,
4
- "node": "gf0dabd13b6489029422d8d7883f60e98215d811f",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-09-29"
8
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes