urwid 2.6.15__py3-none-any.whl → 3.0.5__py3-none-any.whl

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 (62) hide show
  1. urwid/__init__.py +30 -20
  2. urwid/canvas.py +34 -53
  3. urwid/command_map.py +6 -4
  4. urwid/container.py +1 -1
  5. urwid/decoration.py +1 -1
  6. urwid/display/__init__.py +53 -48
  7. urwid/display/_posix_raw_display.py +20 -8
  8. urwid/display/_raw_display_base.py +21 -16
  9. urwid/display/_win32_raw_display.py +16 -17
  10. urwid/display/common.py +45 -74
  11. urwid/display/curses.py +3 -5
  12. urwid/display/escape.py +28 -13
  13. urwid/display/lcd.py +8 -10
  14. urwid/display/web.py +11 -16
  15. urwid/event_loop/asyncio_loop.py +35 -15
  16. urwid/event_loop/main_loop.py +18 -23
  17. urwid/event_loop/tornado_loop.py +4 -5
  18. urwid/event_loop/trio_loop.py +1 -1
  19. urwid/font.py +19 -22
  20. urwid/numedit.py +65 -65
  21. urwid/signals.py +19 -27
  22. urwid/split_repr.py +9 -3
  23. urwid/str_util.py +105 -60
  24. urwid/text_layout.py +14 -13
  25. urwid/util.py +8 -19
  26. urwid/version.py +22 -4
  27. urwid/vterm.py +20 -47
  28. urwid/widget/__init__.py +0 -6
  29. urwid/widget/attr_map.py +10 -10
  30. urwid/widget/attr_wrap.py +11 -13
  31. urwid/widget/bar_graph.py +3 -8
  32. urwid/widget/big_text.py +8 -9
  33. urwid/widget/box_adapter.py +6 -6
  34. urwid/widget/columns.py +52 -83
  35. urwid/widget/container.py +29 -75
  36. urwid/widget/divider.py +6 -6
  37. urwid/widget/edit.py +50 -50
  38. urwid/widget/filler.py +14 -14
  39. urwid/widget/frame.py +31 -40
  40. urwid/widget/grid_flow.py +25 -110
  41. urwid/widget/line_box.py +31 -18
  42. urwid/widget/listbox.py +16 -51
  43. urwid/widget/monitored_list.py +75 -49
  44. urwid/widget/overlay.py +4 -37
  45. urwid/widget/padding.py +31 -68
  46. urwid/widget/pile.py +179 -158
  47. urwid/widget/popup.py +2 -2
  48. urwid/widget/progress_bar.py +17 -18
  49. urwid/widget/scrollable.py +26 -34
  50. urwid/widget/solid_fill.py +3 -3
  51. urwid/widget/text.py +44 -30
  52. urwid/widget/treetools.py +27 -48
  53. urwid/widget/widget.py +13 -130
  54. urwid/widget/widget_decoration.py +6 -35
  55. urwid/widget/wimp.py +61 -61
  56. urwid/wimp.py +1 -1
  57. {urwid-2.6.15.dist-info → urwid-3.0.5.dist-info}/METADATA +24 -24
  58. urwid-3.0.5.dist-info/RECORD +74 -0
  59. {urwid-2.6.15.dist-info → urwid-3.0.5.dist-info}/WHEEL +1 -1
  60. urwid-2.6.15.dist-info/RECORD +0 -74
  61. {urwid-2.6.15.dist-info → urwid-3.0.5.dist-info/licenses}/COPYING +0 -0
  62. {urwid-2.6.15.dist-info → urwid-3.0.5.dist-info}/top_level.txt +0 -0
urwid/signals.py CHANGED
@@ -20,16 +20,16 @@
20
20
 
21
21
  from __future__ import annotations
22
22
 
23
+ import abc
23
24
  import itertools
24
25
  import typing
25
- import warnings
26
26
  import weakref
27
27
 
28
28
  if typing.TYPE_CHECKING:
29
29
  from collections.abc import Callable, Collection, Container, Hashable, Iterable
30
30
 
31
31
 
32
- class MetaSignals(type):
32
+ class MetaSignals(abc.ABCMeta):
33
33
  """
34
34
  register the list of signals in the class variable signals,
35
35
  including signals in superclasses.
@@ -98,9 +98,9 @@ class Signals:
98
98
  :type name: signal name
99
99
  :param callback: the function to call when that signal is sent
100
100
  :type callback: function
101
- :param user_arg: deprecated additional argument to callback (appended
102
- after the arguments passed when the signal is
103
- emitted). If None no arguments will be added.
101
+ :param user_arg: additional argument to callback
102
+ (appended after the arguments passed when the signal is emitted).
103
+ If None no arguments will be added.
104
104
  Don't use this argument, use user_args instead.
105
105
  :param weak_args: additional arguments passed to the callback
106
106
  (before any arguments passed when the signal
@@ -132,11 +132,11 @@ class Signals:
132
132
  As an example of using weak_args, consider the following snippet:
133
133
 
134
134
  >>> import urwid
135
- >>> debug = urwid.Text('')
135
+ >>> debug = urwid.Text("")
136
136
  >>> def handler(widget, newtext):
137
- ... debug.set_text("Edit widget changed to %s" % newtext)
138
- >>> edit = urwid.Edit('')
139
- >>> key = urwid.connect_signal(edit, 'change', handler)
137
+ ... debug.set_text("Edit widget changed to %s" % newtext)
138
+ >>> edit = urwid.Edit("")
139
+ >>> key = urwid.connect_signal(edit, "change", handler)
140
140
 
141
141
  If you now build some interface using "edit" and "debug", the
142
142
  "debug" widget will show whatever you type in the "edit" widget.
@@ -148,11 +148,11 @@ class Signals:
148
148
  (it's not really a closure, since it doesn't reference any
149
149
  outside variables, so it's just a dynamic function):
150
150
 
151
- >>> debug = urwid.Text('')
151
+ >>> debug = urwid.Text("")
152
152
  >>> def handler(weak_debug, widget, newtext):
153
- ... weak_debug.set_text("Edit widget changed to %s" % newtext)
154
- >>> edit = urwid.Edit('')
155
- >>> key = urwid.connect_signal(edit, 'change', handler, weak_args=[debug])
153
+ ... weak_debug.set_text("Edit widget changed to %s" % newtext)
154
+ >>> edit = urwid.Edit("")
155
+ >>> key = urwid.connect_signal(edit, "change", handler, weak_args=[debug])
156
156
 
157
157
  Here the weak_debug parameter in print_debug is the value passed
158
158
  in the weak_args list to connect_signal. Note that the
@@ -166,12 +166,6 @@ class Signals:
166
166
  handler can also be disconnected by calling
167
167
  urwid.disconnect_signal, which doesn't need this key.
168
168
  """
169
- if user_arg is not None:
170
- warnings.warn(
171
- "Don't use user_arg argument, use user_args instead.",
172
- DeprecationWarning,
173
- stacklevel=2,
174
- )
175
169
 
176
170
  sig_cls = obj.__class__
177
171
  if name not in self._supported.get(sig_cls, ()):
@@ -194,9 +188,8 @@ class Signals:
194
188
  # their callbacks) from existing.
195
189
  obj_weak = weakref.ref(obj)
196
190
 
197
- def weakref_callback(weakref): # pylint: disable=redefined-outer-name # bad, but not changing API
198
- o = obj_weak()
199
- if o:
191
+ def weakref_callback(_ref: weakref.ReferenceType[typing.Any]) -> None:
192
+ if o := obj_weak():
200
193
  self.disconnect_by_key(o, name, key)
201
194
 
202
195
  user_args = self._prepare_user_args(weak_args, user_args, weakref_callback)
@@ -208,7 +201,7 @@ class Signals:
208
201
  self,
209
202
  weak_args: Iterable[typing.Any] = (),
210
203
  user_args: Iterable[typing.Any] = (),
211
- callback: Callable[..., typing.Any] | None = None,
204
+ callback: Callable[[weakref.ReferenceType[typing.Any]], typing.Any] | None = None,
212
205
  ) -> tuple[Collection[weakref.ReferenceType], Collection[typing.Any]]:
213
206
  # Turn weak_args into weakrefs and prepend them to user_args
214
207
  w_args = tuple(weakref.ref(w_arg, callback) for w_arg in weak_args)
@@ -302,14 +295,13 @@ class Signals:
302
295
  self,
303
296
  callback,
304
297
  user_arg: typing.Any,
305
- weak_args: Collection[weakref.ReferenceType],
306
- user_args: Collection[typing.Any],
298
+ weak_args: Iterable[weakref.ReferenceType],
299
+ user_args: Iterable[typing.Any],
307
300
  emit_args: Iterable[typing.Any],
308
301
  ) -> bool:
309
302
  args_to_pass = []
310
303
  for w_arg in weak_args:
311
- real_arg = w_arg()
312
- if real_arg is not None:
304
+ if (real_arg := w_arg()) is not None:
313
305
  args_to_pass.append(real_arg)
314
306
  else:
315
307
  # de-referenced
urwid/split_repr.py CHANGED
@@ -32,15 +32,18 @@ def split_repr(self):
32
32
 
33
33
  >>> class Foo(object):
34
34
  ... __repr__ = split_repr
35
+ ...
35
36
  ... def _repr_words(self):
36
37
  ... return ["words", "here"]
38
+ ...
37
39
  ... def _repr_attrs(self):
38
- ... return {'attrs': "appear too"}
40
+ ... return {"attrs": "appear too"}
39
41
  >>> Foo()
40
42
  <Foo words here attrs='appear too'>
41
43
  >>> class Bar(Foo):
42
44
  ... def _repr_words(self):
43
45
  ... return Foo._repr_words(self) + ["too"]
46
+ ...
44
47
  ... def _repr_attrs(self):
45
48
  ... return dict(Foo._repr_attrs(self), barttr=42)
46
49
  >>> Bar()
@@ -62,9 +65,9 @@ def normalize_repr(v):
62
65
  """
63
66
  Return dictionary repr sorted by keys, leave others unchanged
64
67
 
65
- >>> normalize_repr({1:2,3:4,5:6,7:8})
68
+ >>> normalize_repr({1: 2, 3: 4, 5: 6, 7: 8})
66
69
  '{1: 2, 3: 4, 5: 6, 7: 8}'
67
- >>> normalize_repr('foo')
70
+ >>> normalize_repr("foo")
68
71
  "'foo'"
69
72
  """
70
73
  if isinstance(v, dict):
@@ -88,9 +91,12 @@ def remove_defaults(d, fn):
88
91
  >>> class Foo(object):
89
92
  ... def __init__(self, a=1, b=2):
90
93
  ... self.values = a, b
94
+ ...
91
95
  ... __repr__ = split_repr
96
+ ...
92
97
  ... def _repr_words(self):
93
98
  ... return ["object"]
99
+ ...
94
100
  ... def _repr_attrs(self):
95
101
  ... d = dict(a=self.values[0], b=self.values[1])
96
102
  ... return remove_defaults(d, Foo.__init__)
urwid/str_util.py CHANGED
@@ -29,22 +29,61 @@ import wcwidth
29
29
  if typing.TYPE_CHECKING:
30
30
  from typing_extensions import Literal
31
31
 
32
- SAFE_ASCII_RE = re.compile("^[ -~]*$")
33
- SAFE_ASCII_BYTES_RE = re.compile(b"^[ -~]*$")
32
+ SAFE_ASCII_RE = re.compile(r"^[ -~]*$")
33
+ SAFE_ASCII_BYTES_RE = re.compile(rb"^[ -~]*$")
34
34
 
35
35
  _byte_encoding: Literal["utf8", "narrow", "wide"] = "narrow"
36
36
 
37
37
 
38
38
  def get_char_width(char: str) -> Literal[0, 1, 2]:
39
- width = wcwidth.wcwidth(char)
40
- if width < 0:
41
- return 0
42
- return width
39
+ """
40
+ Return the screen column width for a single character.
41
+
42
+ .. deprecated:: 3.0.4
43
+ """
44
+ warnings.warn(
45
+ "get_char_width is deprecated in favor of wcwidth.width",
46
+ DeprecationWarning,
47
+ stacklevel=2,
48
+ )
49
+ if (width := wcwidth.wcwidth(char)) >= 0:
50
+ return width
51
+
52
+ return 0
43
53
 
44
54
 
45
55
  def get_width(o: int) -> Literal[0, 1, 2]:
46
- """Return the screen column width for unicode ordinal o."""
47
- return get_char_width(chr(o))
56
+ """
57
+ Return the screen column width for unicode ordinal o.
58
+
59
+ .. deprecated:: 3.0.4
60
+ """
61
+ warnings.warn(
62
+ "get_width is deprecated in favor of wcwidth.width",
63
+ DeprecationWarning,
64
+ stacklevel=2,
65
+ )
66
+ if (width := wcwidth.wcwidth(chr(o))) >= 0:
67
+ return width
68
+
69
+ return 0
70
+
71
+
72
+ def _decode_grapheme_at(text: bytes, start: int, end: int) -> tuple[str, int]:
73
+ """
74
+ Decode bytes starting at `start` to get the first grapheme cluster.
75
+
76
+ :param text: UTF-8 encoded bytes
77
+ :param start: starting byte position
78
+ :param end: ending byte position
79
+ :returns: (grapheme_string, next_byte_position)
80
+
81
+ Assumes caller provides valid UTF-8 byte boundaries.
82
+ """
83
+ decoded = text[start:end].decode("utf-8")
84
+ grapheme = next(wcwidth.iter_graphemes(decoded), "")
85
+ grapheme_bytes = grapheme.encode("utf-8")
86
+ return grapheme, start + len(grapheme_bytes)
48
87
 
49
88
 
50
89
  def decode_one(text: bytes | str, pos: int) -> tuple[int, int]:
@@ -86,10 +125,9 @@ def decode_one(text: bytes | str, pos: int) -> tuple[int, int]:
86
125
  if b1 & 0xE0 == 0xC0:
87
126
  if b2 & 0xC0 != 0x80:
88
127
  return error
89
- o = ((b1 & 0x1F) << 6) | (b2 & 0x3F)
90
- if o < 0x80:
91
- return error
92
- return o, pos + 2
128
+ if (o := ((b1 & 0x1F) << 6) | (b2 & 0x3F)) >= 0x80:
129
+ return o, pos + 2
130
+ return error
93
131
  if lt < 3:
94
132
  return error
95
133
  if b1 & 0xF0 == 0xE0:
@@ -97,10 +135,9 @@ def decode_one(text: bytes | str, pos: int) -> tuple[int, int]:
97
135
  return error
98
136
  if b3 & 0xC0 != 0x80:
99
137
  return error
100
- o = ((b1 & 0x0F) << 12) | ((b2 & 0x3F) << 6) | (b3 & 0x3F)
101
- if o < 0x800:
102
- return error
103
- return o, pos + 3
138
+ if (o := ((b1 & 0x0F) << 12) | ((b2 & 0x3F) << 6) | (b3 & 0x3F)) >= 0x800:
139
+ return o, pos + 3
140
+ return error
104
141
  if lt < 4:
105
142
  return error
106
143
  if b1 & 0xF8 == 0xF0:
@@ -110,10 +147,9 @@ def decode_one(text: bytes | str, pos: int) -> tuple[int, int]:
110
147
  return error
111
148
  if b4 & 0xC0 != 0x80:
112
149
  return error
113
- o = ((b1 & 0x07) << 18) | ((b2 & 0x3F) << 12) | ((b3 & 0x3F) << 6) | (b4 & 0x3F)
114
- if o < 0x10000:
115
- return error
116
- return o, pos + 4
150
+ if (o := ((b1 & 0x07) << 18) | ((b2 & 0x3F) << 12) | ((b3 & 0x3F) << 6) | (b4 & 0x3F)) >= 0x10000:
151
+ return o, pos + 4
152
+ return error
117
153
  return error
118
154
 
119
155
 
@@ -160,23 +196,26 @@ def calc_string_text_pos(text: str, start_offs: int, end_offs: int, pref_col: in
160
196
  where start_offs is the offset into text assumed to be screen column 0
161
197
  and end_offs is the end of the range to search.
162
198
 
199
+ Iterates by grapheme clusters for emoji ZWJ sequences, flags,
200
+ combining characters, and other multi-codepoint unicode sequences.
201
+
163
202
  :param text: string
164
203
  :param start_offs: starting text position
165
204
  :param end_offs: ending text position
166
205
  :param pref_col: target column
167
206
  :returns: (position, actual_col)
168
-
169
- ..note:: this method is a simplified version of `wcwidth.wcswidth` and ideally should be in wcwidth package.
170
207
  """
171
208
  if start_offs > end_offs:
172
209
  raise ValueError((start_offs, end_offs))
173
210
 
174
211
  cols = 0
175
- for idx in range(start_offs, end_offs):
176
- width = get_char_width(text[idx])
177
- if width + cols > pref_col:
178
- return idx, cols
179
- cols += width
212
+ pos = start_offs
213
+ for grapheme in wcwidth.iter_graphemes(text[start_offs:end_offs]):
214
+ grapheme_width = wcwidth.width(grapheme, control_codes="ignore")
215
+ if grapheme_width + cols > pref_col:
216
+ return pos, cols
217
+ cols += grapheme_width
218
+ pos += len(grapheme)
180
219
 
181
220
  return end_offs, cols
182
221
 
@@ -201,16 +240,10 @@ def calc_text_pos(text: str | bytes, start_offs: int, end_offs: int, pref_col: i
201
240
  raise TypeError(text)
202
241
 
203
242
  if _byte_encoding == "utf8":
204
- i = start_offs
205
- sc = 0
206
- while i < end_offs:
207
- o, n = decode_one(text, i)
208
- w = get_width(o)
209
- if w + sc > pref_col:
210
- return i, sc
211
- i = n
212
- sc += w
213
- return i, sc
243
+ decoded = text[start_offs:end_offs].decode("utf-8")
244
+ str_pos, cols = calc_string_text_pos(decoded, 0, len(decoded), pref_col)
245
+ byte_offset = len(decoded[:str_pos].encode("utf-8"))
246
+ return start_offs + byte_offset, cols
214
247
 
215
248
  # "wide" and "narrow"
216
249
  i = start_offs + pref_col
@@ -228,19 +261,20 @@ def calc_width(text: str | bytes, start_offs: int, end_offs: int) -> int:
228
261
  text may be unicode or a byte string in the target _byte_encoding
229
262
 
230
263
  Some characters are wide (take two columns) and others affect the
231
- previous character (take zero columns). Use the widths table above
232
- to calculate the screen column width of text[start_offs:end_offs]
264
+ previous character (take zero columns), while others are grouped
265
+ in sequence by "grapheme boundaries" (Emoji, Skin tones, flags, etc).
233
266
  """
234
267
 
235
268
  if start_offs > end_offs:
236
- raise ValueError((start_offs, end_offs))
269
+ msg = f"{start_offs=} > {end_offs=}"
270
+ raise ValueError(msg)
237
271
 
238
272
  if isinstance(text, str):
239
- return sum(get_char_width(char) for char in text[start_offs:end_offs])
273
+ return wcwidth.width(text[start_offs:end_offs], control_codes="ignore")
240
274
 
241
275
  if _byte_encoding == "utf8":
242
276
  try:
243
- return sum(get_char_width(char) for char in text[start_offs:end_offs].decode("utf-8"))
277
+ return wcwidth.width(text[start_offs:end_offs].decode("utf-8"), control_codes="ignore")
244
278
  except UnicodeDecodeError as exc:
245
279
  warnings.warn(
246
280
  "`calc_width` with text encoded to bytes can produce incorrect results"
@@ -253,8 +287,8 @@ def calc_width(text: str | bytes, start_offs: int, end_offs: int) -> int:
253
287
  sc = 0
254
288
  while i < end_offs:
255
289
  o, i = decode_one(text, i)
256
- w = get_width(o)
257
- sc += w
290
+ if (w := wcwidth.wcwidth(chr(o))) > 0:
291
+ sc += w
258
292
  return sc
259
293
  # "wide", "narrow" or all printable ASCII, just return the character count
260
294
  return end_offs - start_offs
@@ -262,17 +296,22 @@ def calc_width(text: str | bytes, start_offs: int, end_offs: int) -> int:
262
296
 
263
297
  def is_wide_char(text: str | bytes, offs: int) -> bool:
264
298
  """
265
- Test if the character at offs within text is wide.
299
+ Test if the grapheme cluster at offs within text is wide (2 columns).
300
+
301
+ For Unicode strings, extracts the full grapheme cluster starting at offs
302
+ and checks if it renders as wide. This correctly handles multi-codepoint
303
+ graphemes like emoji ZWJ sequences and flags.
266
304
 
267
305
  text may be unicode or a byte string in the target _byte_encoding
268
306
  """
269
307
  if isinstance(text, str):
270
- return get_char_width(text[offs]) == 2
308
+ grapheme = next(wcwidth.iter_graphemes(text[offs:]))
309
+ return wcwidth.width(grapheme, control_codes="ignore") == 2
271
310
  if not isinstance(text, bytes):
272
311
  raise TypeError(text)
273
312
  if _byte_encoding == "utf8":
274
- o, _n = decode_one(text, offs)
275
- return get_width(o) == 2
313
+ grapheme, _ = _decode_grapheme_at(text, offs, len(text))
314
+ return wcwidth.width(grapheme, control_codes="ignore") == 2
276
315
  if _byte_encoding == "wide":
277
316
  return within_double_byte(text, offs, offs) == 1
278
317
  return False
@@ -280,19 +319,23 @@ def is_wide_char(text: str | bytes, offs: int) -> bool:
280
319
 
281
320
  def move_prev_char(text: str | bytes, start_offs: int, end_offs: int) -> int:
282
321
  """
283
- Return the position of the character before end_offs.
322
+ Return the position of the grapheme cluster before end_offs.
323
+
324
+ For Unicode strings, handle multi-codepoint, "grapheme clusters",
325
+ to better measure emoji ZWJ, flags, combining characters, skin tones.
284
326
  """
285
327
  if start_offs >= end_offs:
286
328
  raise ValueError((start_offs, end_offs))
287
329
  if isinstance(text, str):
288
- return end_offs - 1
330
+ return wcwidth.grapheme_boundary_before(text, end_offs)
289
331
  if not isinstance(text, bytes):
290
332
  raise TypeError(text)
291
333
  if _byte_encoding == "utf8":
292
- o = end_offs - 1
293
- while text[o] & 0xC0 == 0x80:
294
- o -= 1
295
- return o
334
+ decoded = text[start_offs:end_offs].decode("utf-8")
335
+ str_pos = len(decoded)
336
+ prev_str_pos = wcwidth.grapheme_boundary_before(decoded, str_pos)
337
+ prefix = decoded[:prev_str_pos]
338
+ return start_offs + len(prefix.encode("utf-8"))
296
339
  if _byte_encoding == "wide" and within_double_byte(text, start_offs, end_offs - 1) == 2:
297
340
  return end_offs - 2
298
341
  return end_offs - 1
@@ -300,19 +343,21 @@ def move_prev_char(text: str | bytes, start_offs: int, end_offs: int) -> int:
300
343
 
301
344
  def move_next_char(text: str | bytes, start_offs: int, end_offs: int) -> int:
302
345
  """
303
- Return the position of the character after start_offs.
346
+ Return the position of the next grapheme cluster after start_offs.
347
+
348
+ For Unicode strings, handle multi-codepoint, "grapheme clusters",
349
+ to better measure emoji ZWJ, flags, combining characters, skin tones.
304
350
  """
305
351
  if start_offs >= end_offs:
306
352
  raise ValueError((start_offs, end_offs))
307
353
  if isinstance(text, str):
308
- return start_offs + 1
354
+ grapheme = next(wcwidth.iter_graphemes(text[start_offs:end_offs]))
355
+ return start_offs + len(grapheme)
309
356
  if not isinstance(text, bytes):
310
357
  raise TypeError(text)
311
358
  if _byte_encoding == "utf8":
312
- o = start_offs + 1
313
- while o < end_offs and text[o] & 0xC0 == 0x80:
314
- o += 1
315
- return o
359
+ _, next_pos = _decode_grapheme_at(text, start_offs, end_offs)
360
+ return next_pos
316
361
  if _byte_encoding == "wide" and within_double_byte(text, start_offs, start_offs) == 1:
317
362
  return start_offs + 2
318
363
  return start_offs + 1
urwid/text_layout.py CHANGED
@@ -23,7 +23,9 @@ from __future__ import annotations
23
23
  import functools
24
24
  import typing
25
25
 
26
- from urwid.str_util import calc_text_pos, calc_width, get_char_width, is_wide_char, move_next_char, move_prev_char
26
+ import wcwidth
27
+
28
+ from urwid.str_util import calc_text_pos, calc_width, is_wide_char, move_next_char, move_prev_char
27
29
  from urwid.util import calc_trim_text, get_encoding
28
30
 
29
31
  if typing.TYPE_CHECKING:
@@ -44,7 +46,7 @@ def get_ellipsis_string(encoding: str) -> str:
44
46
  @functools.lru_cache(maxsize=4)
45
47
  def _get_width(string) -> int:
46
48
  """Get ellipsis character width for given encoding."""
47
- return sum(get_char_width(char) for char in string)
49
+ return wcwidth.width(string, control_codes="ignore")
48
50
 
49
51
 
50
52
  class TextLayout:
@@ -176,7 +178,7 @@ class StandardTextLayout(TextLayout):
176
178
  self,
177
179
  text: str | bytes,
178
180
  width: int,
179
- wrap: Literal["any", "space", "clip", "ellipsis"] | WrapMode,
181
+ wrap: Literal["clip", "ellipsis", WrapMode.CLIP, WrapMode.ELLIPSIS],
180
182
  ) -> list[list[tuple[int, int, int | bytes] | tuple[int, int | None]]]:
181
183
  """Calculate text segments for cases of a text trimmed (wrap is clip or ellipsis)."""
182
184
  segments = []
@@ -185,8 +187,8 @@ class StandardTextLayout(TextLayout):
185
187
  encoding = get_encoding()
186
188
  ellipsis_string = get_ellipsis_string(encoding)
187
189
  ellipsis_width = _get_width(ellipsis_string)
188
- while width - 1 < ellipsis_width and ellipsis_string:
189
- ellipsis_string = ellipsis_string[:-1]
190
+ if (extra := width - ellipsis_width - 1) < 0:
191
+ ellipsis_string = ellipsis_string[:extra]
190
192
  ellipsis_width = _get_width(ellipsis_string)
191
193
 
192
194
  ellipsis_char = ellipsis_string.encode(encoding)
@@ -236,7 +238,7 @@ class StandardTextLayout(TextLayout):
236
238
  """
237
239
  Calculate the segments of text to display given width screen columns to display them.
238
240
 
239
- text - unicode text or byte string to display
241
+ text - Unicode text or byte string to display
240
242
  width - number of available screen columns
241
243
  wrap - wrapping mode used
242
244
 
@@ -461,7 +463,7 @@ def shift_line(
461
463
  # existing shift
462
464
  amount += segs[0][0]
463
465
  if amount:
464
- return [(amount, None)] + segs[1:]
466
+ return [(amount, None), *segs[1:]]
465
467
  return segs[1:]
466
468
 
467
469
  if amount:
@@ -581,8 +583,7 @@ def calc_pos(
581
583
  if row < 0 or row >= len(layout):
582
584
  raise ValueError("calculate_pos: out of layout row range")
583
585
 
584
- pos = calc_line_pos(text, layout[row], pref_col)
585
- if pos is not None:
586
+ if (pos := calc_line_pos(text, layout[row], pref_col)) is not None:
586
587
  return pos
587
588
 
588
589
  rows_above = list(range(row - 1, -1, -1))
@@ -590,14 +591,14 @@ def calc_pos(
590
591
  while rows_above and rows_below:
591
592
  if rows_above:
592
593
  r = rows_above.pop(0)
593
- pos = calc_line_pos(text, layout[r], pref_col)
594
- if pos is not None:
594
+ if (pos := calc_line_pos(text, layout[r], pref_col)) is not None:
595
595
  return pos
596
+
596
597
  if rows_below:
597
598
  r = rows_below.pop(0)
598
- pos = calc_line_pos(text, layout[r], pref_col)
599
- if pos is not None:
599
+ if (pos := calc_line_pos(text, layout[r], pref_col)) is not None:
600
600
  return pos
601
+
601
602
  return 0
602
603
 
603
604
 
urwid/util.py CHANGED
@@ -143,7 +143,7 @@ def get_encoding() -> str:
143
143
 
144
144
 
145
145
  @contextlib.contextmanager
146
- def set_temporary_encoding(encoding_name: str) -> Generator[None, None, None]:
146
+ def set_temporary_encoding(encoding_name: str) -> Generator[None]:
147
147
  """Internal helper for encoding specific validation in unittests/doctests.
148
148
 
149
149
  Not exported globally.
@@ -493,24 +493,13 @@ def is_mouse_press(ev: str) -> bool:
493
493
 
494
494
 
495
495
  class MetaSuper(type):
496
- """adding .__super"""
496
+ """Deprecated metaclass.
497
497
 
498
- def __init__(cls, name: str, bases, d):
499
- super().__init__(name, bases, d)
500
- if hasattr(cls, f"_{name}__super"):
501
- raise AttributeError("Class has same name as one of its super classes")
502
-
503
- @property
504
- def _super(self):
505
- warnings.warn(
506
- f"`{name}.__super` was a deprecated feature for old python versions."
507
- f"Please use `super()` call instead.",
508
- DeprecationWarning,
509
- stacklevel=3,
510
- )
511
- return super(cls, self)
498
+ Present only for code compatibility, all logic has been removed.
499
+ Please move to the last position in the class bases to allow future changes.
500
+ """
512
501
 
513
- setattr(cls, f"_{name}__super", _super)
502
+ __slots__ = ()
514
503
 
515
504
 
516
505
  def int_scale(val: int, val_range: int, out_range: int) -> int:
@@ -521,7 +510,7 @@ def int_scale(val: int, val_range: int, out_range: int) -> int:
521
510
 
522
511
  >>> "%x" % int_scale(0x7, 0x10, 0x10000)
523
512
  '7777'
524
- >>> "%x" % int_scale(0x5f, 0x100, 0x10)
513
+ >>> "%x" % int_scale(0x5F, 0x100, 0x10)
525
514
  '6'
526
515
  >>> int_scale(2, 6, 101)
527
516
  40
@@ -534,7 +523,7 @@ def int_scale(val: int, val_range: int, out_range: int) -> int:
534
523
  return num // dem
535
524
 
536
525
 
537
- class StoppingContext(typing.ContextManager["StoppingContext"]):
526
+ class StoppingContext(contextlib.AbstractContextManager["StoppingContext"]):
538
527
  """Context manager that calls ``stop`` on a given object on exit. Used to
539
528
  make the ``start`` method on `MainLoop` and `BaseScreen` optionally act as
540
529
  context managers.
urwid/version.py CHANGED
@@ -1,16 +1,34 @@
1
- # file generated by setuptools_scm
1
+ # file generated by setuptools-scm
2
2
  # don't change, don't track in version control
3
+
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
12
+
3
13
  TYPE_CHECKING = False
4
14
  if TYPE_CHECKING:
5
- from typing import Tuple, Union
15
+ from typing import Tuple
16
+ from typing import Union
17
+
6
18
  VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
7
20
  else:
8
21
  VERSION_TUPLE = object
22
+ COMMIT_ID = object
9
23
 
10
24
  version: str
11
25
  __version__: str
12
26
  __version_tuple__: VERSION_TUPLE
13
27
  version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
30
+
31
+ __version__ = version = '3.0.5'
32
+ __version_tuple__ = version_tuple = (3, 0, 5)
14
33
 
15
- __version__ = version = '2.6.15'
16
- __version_tuple__ = version_tuple = (2, 6, 15)
34
+ __commit_id__ = commit_id = None