urwid 4.2.0__tar.gz → 4.2.2__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.0 → urwid-4.2.2}/.pre-commit-config.yaml +1 -1
  2. {urwid-4.2.0/urwid.egg-info → urwid-4.2.2}/PKG-INFO +1 -1
  3. urwid-4.2.2/pylint-requirements.txt +1 -0
  4. {urwid-4.2.0 → urwid-4.2.2}/pyproject.toml +0 -1
  5. {urwid-4.2.0 → urwid-4.2.2}/urwid/canvas.py +9 -1
  6. {urwid-4.2.0 → urwid-4.2.2}/urwid/display/_posix_raw_display.py +2 -1
  7. {urwid-4.2.0 → urwid-4.2.2}/urwid/display/_raw_display_base.py +6 -6
  8. {urwid-4.2.0 → urwid-4.2.2}/urwid/display/common.py +24 -10
  9. {urwid-4.2.0 → urwid-4.2.2}/urwid/display/curses.py +3 -0
  10. {urwid-4.2.0 → urwid-4.2.2}/urwid/display/escape.py +1 -1
  11. {urwid-4.2.0 → urwid-4.2.2}/urwid/display/web.py +21 -10
  12. {urwid-4.2.0 → urwid-4.2.2}/urwid/event_loop/main_loop.py +21 -4
  13. {urwid-4.2.0 → urwid-4.2.2}/urwid/event_loop/trio_loop.py +18 -0
  14. {urwid-4.2.0 → urwid-4.2.2}/urwid/text_layout.py +18 -5
  15. {urwid-4.2.0 → urwid-4.2.2}/urwid/version.py +3 -3
  16. {urwid-4.2.0 → urwid-4.2.2}/urwid/vterm.py +3 -2
  17. {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/ansi_text.py +2 -2
  18. {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/attr_map.py +24 -23
  19. {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/attr_wrap.py +2 -2
  20. {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/edit.py +14 -2
  21. {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/line_box.py +1 -1
  22. {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/overlay.py +6 -0
  23. {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/popup.py +1 -0
  24. {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/text.py +2 -0
  25. {urwid-4.2.0 → urwid-4.2.2/urwid.egg-info}/PKG-INFO +1 -1
  26. urwid-4.2.2/urwid.egg-info/scm_version.json +8 -0
  27. urwid-4.2.0/pylint-requirements.txt +0 -1
  28. urwid-4.2.0/urwid.egg-info/scm_version.json +0 -8
  29. {urwid-4.2.0 → urwid-4.2.2}/.devcontainer/dev.dockerfile +0 -0
  30. {urwid-4.2.0 → urwid-4.2.2}/.devcontainer/devcontainer.json +0 -0
  31. {urwid-4.2.0 → urwid-4.2.2}/.dockerignore +0 -0
  32. {urwid-4.2.0 → urwid-4.2.2}/.editorconfig +0 -0
  33. {urwid-4.2.0 → urwid-4.2.2}/.github/CODE_OF_CONDUCT.md +0 -0
  34. {urwid-4.2.0 → urwid-4.2.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  35. {urwid-4.2.0 → urwid-4.2.2}/.github/ISSUE_TEMPLATE/docs.md +0 -0
  36. {urwid-4.2.0 → urwid-4.2.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  37. {urwid-4.2.0 → urwid-4.2.2}/.github/ISSUE_TEMPLATE/question.md +0 -0
  38. {urwid-4.2.0 → urwid-4.2.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  39. {urwid-4.2.0 → urwid-4.2.2}/.github/dependabot.yml +0 -0
  40. {urwid-4.2.0 → urwid-4.2.2}/.github/labeler.yml +0 -0
  41. {urwid-4.2.0 → urwid-4.2.2}/.github/release.yml +0 -0
  42. {urwid-4.2.0 → urwid-4.2.2}/.github/workflows/documentation.yml +0 -0
  43. {urwid-4.2.0 → urwid-4.2.2}/.github/workflows/isolated_mypy.yml +0 -0
  44. {urwid-4.2.0 → urwid-4.2.2}/.github/workflows/isolated_pylint.yml +0 -0
  45. {urwid-4.2.0 → urwid-4.2.2}/.github/workflows/labels.yml +0 -0
  46. {urwid-4.2.0 → urwid-4.2.2}/.github/workflows/pythonpackage.yml +0 -0
  47. {urwid-4.2.0 → urwid-4.2.2}/.github/workflows/yamllint.yml +0 -0
  48. {urwid-4.2.0 → urwid-4.2.2}/.gitignore +0 -0
  49. {urwid-4.2.0 → urwid-4.2.2}/.yamllint.yml +0 -0
  50. {urwid-4.2.0 → urwid-4.2.2}/COPYING +0 -0
  51. {urwid-4.2.0 → urwid-4.2.2}/MANIFEST.in +0 -0
  52. {urwid-4.2.0 → urwid-4.2.2}/README.rst +0 -0
  53. {urwid-4.2.0 → urwid-4.2.2}/SECURITY.md +0 -0
  54. {urwid-4.2.0 → urwid-4.2.2}/classifiers.txt +0 -0
  55. {urwid-4.2.0 → urwid-4.2.2}/conftest.py +0 -0
  56. {urwid-4.2.0 → urwid-4.2.2}/mypy-requirements.txt +0 -0
  57. {urwid-4.2.0 → urwid-4.2.2}/pytest_requirements.txt +0 -0
  58. {urwid-4.2.0 → urwid-4.2.2}/requirements.txt +0 -0
  59. {urwid-4.2.0 → urwid-4.2.2}/setup.cfg +0 -0
  60. {urwid-4.2.0 → urwid-4.2.2}/tox.ini +0 -0
  61. {urwid-4.2.0 → urwid-4.2.2}/urwid/__init__.py +0 -0
  62. {urwid-4.2.0 → urwid-4.2.2}/urwid/ansi_parser.py +0 -0
  63. {urwid-4.2.0 → urwid-4.2.2}/urwid/command_map.py +0 -0
  64. {urwid-4.2.0 → urwid-4.2.2}/urwid/display/__init__.py +0 -0
  65. {urwid-4.2.0 → urwid-4.2.2}/urwid/display/_web.css +0 -0
  66. {urwid-4.2.0 → urwid-4.2.2}/urwid/display/_web.js +0 -0
  67. {urwid-4.2.0 → urwid-4.2.2}/urwid/display/_win32.py +0 -0
  68. {urwid-4.2.0 → urwid-4.2.2}/urwid/display/_win32_raw_display.py +0 -0
  69. {urwid-4.2.0 → urwid-4.2.2}/urwid/display/html_fragment.py +0 -0
  70. {urwid-4.2.0 → urwid-4.2.2}/urwid/display/lcd.py +0 -0
  71. {urwid-4.2.0 → urwid-4.2.2}/urwid/display/raw.py +0 -0
  72. {urwid-4.2.0 → urwid-4.2.2}/urwid/event_loop/__init__.py +0 -0
  73. {urwid-4.2.0 → urwid-4.2.2}/urwid/event_loop/abstract_loop.py +0 -0
  74. {urwid-4.2.0 → urwid-4.2.2}/urwid/event_loop/asyncio_loop.py +0 -0
  75. {urwid-4.2.0 → urwid-4.2.2}/urwid/event_loop/glib_loop.py +0 -0
  76. {urwid-4.2.0 → urwid-4.2.2}/urwid/event_loop/select_loop.py +0 -0
  77. {urwid-4.2.0 → urwid-4.2.2}/urwid/event_loop/tornado_loop.py +0 -0
  78. {urwid-4.2.0 → urwid-4.2.2}/urwid/event_loop/twisted_loop.py +0 -0
  79. {urwid-4.2.0 → urwid-4.2.2}/urwid/event_loop/zmq_loop.py +0 -0
  80. {urwid-4.2.0 → urwid-4.2.2}/urwid/font.py +0 -0
  81. {urwid-4.2.0 → urwid-4.2.2}/urwid/graphics.py +0 -0
  82. {urwid-4.2.0 → urwid-4.2.2}/urwid/numedit.py +0 -0
  83. {urwid-4.2.0 → urwid-4.2.2}/urwid/py.typed +0 -0
  84. {urwid-4.2.0 → urwid-4.2.2}/urwid/signals.py +0 -0
  85. {urwid-4.2.0 → urwid-4.2.2}/urwid/split_repr.py +0 -0
  86. {urwid-4.2.0 → urwid-4.2.2}/urwid/str_util.py +0 -0
  87. {urwid-4.2.0 → urwid-4.2.2}/urwid/util.py +0 -0
  88. {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/__init__.py +0 -0
  89. {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/bar_graph.py +0 -0
  90. {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/big_text.py +0 -0
  91. {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/box_adapter.py +0 -0
  92. {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/columns.py +0 -0
  93. {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/constants.py +0 -0
  94. {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/container.py +0 -0
  95. {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/deque_walker.py +0 -0
  96. {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/divider.py +0 -0
  97. {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/filler.py +0 -0
  98. {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/frame.py +0 -0
  99. {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/grid_flow.py +0 -0
  100. {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/list_walker.py +0 -0
  101. {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/listbox.py +0 -0
  102. {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/monitored_deque.py +0 -0
  103. {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/monitored_list.py +0 -0
  104. {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/padding.py +0 -0
  105. {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/pile.py +0 -0
  106. {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/progress_bar.py +0 -0
  107. {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/scrollable.py +0 -0
  108. {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/solid_fill.py +0 -0
  109. {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/treetools.py +0 -0
  110. {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/widget.py +0 -0
  111. {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/widget_decoration.py +0 -0
  112. {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/wimp.py +0 -0
  113. {urwid-4.2.0 → urwid-4.2.2}/urwid.egg-info/SOURCES.txt +0 -0
  114. {urwid-4.2.0 → urwid-4.2.2}/urwid.egg-info/dependency_links.txt +0 -0
  115. {urwid-4.2.0 → urwid-4.2.2}/urwid.egg-info/not-zip-safe +0 -0
  116. {urwid-4.2.0 → urwid-4.2.2}/urwid.egg-info/requires.txt +0 -0
  117. {urwid-4.2.0 → urwid-4.2.2}/urwid.egg-info/scm_file_list.json +0 -0
  118. {urwid-4.2.0 → urwid-4.2.2}/urwid.egg-info/top_level.txt +0 -0
  119. {urwid-4.2.0 → urwid-4.2.2}/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.0
3
+ Version: 4.2.2
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(
@@ -535,7 +537,10 @@ class TextCanvas(Canvas):
535
537
  if not rows:
536
538
  rows = maxrow - trim_top
537
539
 
538
- if not ((0 <= trim_left < maxcol) and (cols > 0 and trim_left + cols <= maxcol)):
540
+ # a canvas without columns (empty text) has one region only: all of it, zero columns wide
541
+ if not (
542
+ (0 <= trim_left < maxcol and cols > 0 and trim_left + cols <= maxcol) or maxcol == trim_left == cols == 0
543
+ ):
539
544
  raise ValueError(trim_left)
540
545
  if not ((0 <= trim_top < maxrow) and (rows > 0 and trim_top + rows <= maxrow)):
541
546
  raise ValueError(trim_top)
@@ -598,6 +603,8 @@ class BlankCanvas(Canvas):
598
603
  Only works as part of a composite canvas since it doesn't know its own size.
599
604
  """
600
605
 
606
+ __slots__ = ()
607
+
601
608
  def content(
602
609
  self,
603
610
  trim_left: int = 0,
@@ -730,6 +737,7 @@ class CompositeCanvas(Canvas):
730
737
 
731
738
  # tuples that define the unfinished cviews that are part of shards following the first shard.
732
739
  super().__init__()
740
+ self.depends_on: Sequence[AbstractWidget] | None = None
733
741
 
734
742
  if canv is None:
735
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,
@@ -121,26 +121,33 @@ class StandardTextLayout(TextLayout):
121
121
  A tab character advances to the next tab stop, measured in rendered screen columns
122
122
  from the start of the displayed line: explicit *tab_stops* are used first,
123
123
  then stops repeat every *tab_stop_every* columns counted from column 0.
124
+ With *tab_stop_every* ``0`` a tab past the last explicit stop takes no columns.
124
125
 
125
126
  :param tab_stops: screen columns of explicit tab stops.
126
- :param tab_stop_every: interval of the default tab stops.
127
- :raises ValueError: *tab_stop_every* or one of *tab_stops* is not a positive column.
127
+ :param tab_stop_every: interval of the default tab stops, ``0`` for none.
128
+ :raises ValueError: *tab_stop_every* is negative, or one of *tab_stops* is not a positive column.
128
129
  """
129
130
  self.tab_stops: tuple[int, ...] = tuple(sorted(set(tab_stops)))
130
- if tab_stop_every < 1 or (self.tab_stops and self.tab_stops[0] < 1):
131
+ if tab_stop_every < 0 or (self.tab_stops and self.tab_stops[0] < 1):
131
132
  raise ValueError(f"Tab stops must be positive: {tab_stops=!r}, {tab_stop_every=!r}")
132
133
  self.tab_stop_every = tab_stop_every
133
134
 
134
135
  def next_tab_stop(self, column: int) -> int:
135
136
  """Return the screen column of the first tab stop after *column*.
136
137
 
138
+ When there is no stop after *column*, return *column* itself: the tab takes no columns.
139
+
137
140
  >>> StandardTextLayout(tab_stops=(4, 10)).next_tab_stop(5)
138
141
  10
139
142
  >>> StandardTextLayout(tab_stops=(4, 10)).next_tab_stop(10)
140
143
  16
144
+ >>> StandardTextLayout(tab_stops=(4, 10), tab_stop_every=0).next_tab_stop(10)
145
+ 10
141
146
  """
142
147
  if (idx := bisect.bisect_right(self.tab_stops, column)) < len(self.tab_stops):
143
148
  return self.tab_stops[idx]
149
+ if not self.tab_stop_every:
150
+ return column
144
151
  return (column // self.tab_stop_every + 1) * self.tab_stop_every
145
152
 
146
153
  def supports_align_mode(self, align: Literal["left", "center", "right"] | Align) -> bool:
@@ -305,7 +312,10 @@ class StandardTextLayout(TextLayout):
305
312
  pos = start
306
313
  while pos < end:
307
314
  if text[pos : pos + 1] == tab:
308
- tab_width = self.next_tab_stop(column) - column
315
+ if not (tab_width := self.next_tab_stop(column) - column):
316
+ line.append((0, pos))
317
+ pos += 1
318
+ continue
309
319
  if limit is not None and column + tab_width > limit:
310
320
  if limit > column:
311
321
  line.append((limit - column, pos, b" " * (limit - column)))
@@ -355,7 +365,10 @@ class StandardTextLayout(TextLayout):
355
365
  pos = start
356
366
  while pos < end:
357
367
  if text[pos : pos + 1] == tab:
358
- if column < width:
368
+ if self.next_tab_stop(column) == column:
369
+ line.append((0, pos))
370
+ pos += 1
371
+ elif column < width:
359
372
  tab_width = min(self.next_tab_stop(column), width) - column
360
373
  line.append((tab_width, pos, b" " * tab_width))
361
374
  column += tab_width
@@ -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.0'
22
- __version_tuple__ = version_tuple = (4, 2, 0)
21
+ __version__ = version = '4.2.2'
22
+ __version_tuple__ = version_tuple = (4, 2, 2)
23
23
 
24
- __commit_id__ = commit_id = 'g15c2c59b0'
24
+ __commit_id__ = commit_id = 'gd131ddace'
@@ -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
 
@@ -487,7 +487,8 @@ class Edit(WidgetWrap[Text]):
487
487
  if idx:
488
488
  column = layout.next_tab_stop(column)
489
489
  column += calc_width(chunk, 0, len(chunk))
490
- self.insert_text(" " * (layout.next_tab_stop(column) - column))
490
+ if spaces := layout.next_tab_stop(column) - column:
491
+ self.insert_text(" " * spaces)
491
492
  return None
492
493
 
493
494
  if key == "enter" and self.multiline:
@@ -675,6 +676,11 @@ class Edit(WidgetWrap[Text]):
675
676
  if not self._shift_view_to_cursor:
676
677
  return trans
677
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
+
678
684
  text, _ignore = self.get_text()
679
685
  x, y = text_layout.calc_coords(text, trans, self.edit_pos + len(self.caption))
680
686
  if x < 0:
@@ -703,7 +709,13 @@ class Edit(WidgetWrap[Text]):
703
709
  (maxcol,) = size
704
710
 
705
711
  self._shift_view_to_cursor = True
706
- 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
707
719
 
708
720
  def position_coords(self, maxcol: int, pos: int) -> tuple[int, int]:
709
721
  """Return (*x*, *y*) coordinates for an offset into self.edit_text."""
@@ -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
 
@@ -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.0
3
+ Version: 4.2.2
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.2",
3
+ "distance": 0,
4
+ "node": "gd131ddacee0bce743eefcb49d6d0ee2873ae3f6e",
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.0",
3
- "distance": 0,
4
- "node": "g15c2c59b010d1ffe65453e7ded5dd05fae5ae6ab",
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
File without changes
File without changes