urwid 2.6.0.post0__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.

Potentially problematic release.


This version of urwid might be problematic. Click here for more details.

Files changed (75) hide show
  1. urwid/__init__.py +333 -0
  2. urwid/canvas.py +1413 -0
  3. urwid/command_map.py +137 -0
  4. urwid/container.py +59 -0
  5. urwid/decoration.py +65 -0
  6. urwid/display/__init__.py +97 -0
  7. urwid/display/_posix_raw_display.py +413 -0
  8. urwid/display/_raw_display_base.py +914 -0
  9. urwid/display/_web.css +12 -0
  10. urwid/display/_web.js +462 -0
  11. urwid/display/_win32.py +171 -0
  12. urwid/display/_win32_raw_display.py +269 -0
  13. urwid/display/common.py +1219 -0
  14. urwid/display/curses.py +690 -0
  15. urwid/display/escape.py +624 -0
  16. urwid/display/html_fragment.py +251 -0
  17. urwid/display/lcd.py +518 -0
  18. urwid/display/raw.py +37 -0
  19. urwid/display/web.py +636 -0
  20. urwid/event_loop/__init__.py +55 -0
  21. urwid/event_loop/abstract_loop.py +175 -0
  22. urwid/event_loop/asyncio_loop.py +231 -0
  23. urwid/event_loop/glib_loop.py +294 -0
  24. urwid/event_loop/main_loop.py +721 -0
  25. urwid/event_loop/select_loop.py +230 -0
  26. urwid/event_loop/tornado_loop.py +206 -0
  27. urwid/event_loop/trio_loop.py +302 -0
  28. urwid/event_loop/twisted_loop.py +269 -0
  29. urwid/event_loop/zmq_loop.py +275 -0
  30. urwid/font.py +695 -0
  31. urwid/graphics.py +96 -0
  32. urwid/highlight.css +19 -0
  33. urwid/listbox.py +1899 -0
  34. urwid/monitored_list.py +522 -0
  35. urwid/numedit.py +376 -0
  36. urwid/signals.py +330 -0
  37. urwid/split_repr.py +130 -0
  38. urwid/str_util.py +358 -0
  39. urwid/text_layout.py +632 -0
  40. urwid/treetools.py +515 -0
  41. urwid/util.py +557 -0
  42. urwid/version.py +16 -0
  43. urwid/vterm.py +1806 -0
  44. urwid/widget/__init__.py +181 -0
  45. urwid/widget/attr_map.py +161 -0
  46. urwid/widget/attr_wrap.py +140 -0
  47. urwid/widget/bar_graph.py +649 -0
  48. urwid/widget/big_text.py +77 -0
  49. urwid/widget/box_adapter.py +126 -0
  50. urwid/widget/columns.py +1145 -0
  51. urwid/widget/constants.py +574 -0
  52. urwid/widget/container.py +227 -0
  53. urwid/widget/divider.py +110 -0
  54. urwid/widget/edit.py +718 -0
  55. urwid/widget/filler.py +403 -0
  56. urwid/widget/frame.py +539 -0
  57. urwid/widget/grid_flow.py +539 -0
  58. urwid/widget/line_box.py +194 -0
  59. urwid/widget/overlay.py +829 -0
  60. urwid/widget/padding.py +597 -0
  61. urwid/widget/pile.py +971 -0
  62. urwid/widget/popup.py +170 -0
  63. urwid/widget/progress_bar.py +141 -0
  64. urwid/widget/scrollable.py +597 -0
  65. urwid/widget/solid_fill.py +44 -0
  66. urwid/widget/text.py +354 -0
  67. urwid/widget/widget.py +852 -0
  68. urwid/widget/widget_decoration.py +166 -0
  69. urwid/widget/wimp.py +792 -0
  70. urwid/wimp.py +23 -0
  71. urwid-2.6.0.post0.dist-info/COPYING +504 -0
  72. urwid-2.6.0.post0.dist-info/METADATA +332 -0
  73. urwid-2.6.0.post0.dist-info/RECORD +75 -0
  74. urwid-2.6.0.post0.dist-info/WHEEL +5 -0
  75. urwid-2.6.0.post0.dist-info/top_level.txt +1 -0
urwid/util.py ADDED
@@ -0,0 +1,557 @@
1
+ # Urwid utility functions
2
+ # Copyright (C) 2004-2011 Ian Ward
3
+ #
4
+ # This library is free software; you can redistribute it and/or
5
+ # modify it under the terms of the GNU Lesser General Public
6
+ # License as published by the Free Software Foundation; either
7
+ # version 2.1 of the License, or (at your option) any later version.
8
+ #
9
+ # This library is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ # Lesser General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU Lesser General Public
15
+ # License along with this library; if not, write to the Free Software
16
+ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
+ #
18
+ # Urwid web site: https://urwid.org/
19
+
20
+
21
+ from __future__ import annotations
22
+
23
+ import codecs
24
+ import contextlib
25
+ import sys
26
+ import typing
27
+ import warnings
28
+ from contextlib import suppress
29
+
30
+ from urwid import str_util
31
+
32
+ if typing.TYPE_CHECKING:
33
+ from collections.abc import Generator, Iterable
34
+ from types import TracebackType
35
+
36
+ from typing_extensions import Literal, Protocol, Self
37
+
38
+ class CanBeStopped(Protocol):
39
+ def stop(self) -> None: ...
40
+
41
+
42
+ def __getattr__(name: str) -> typing.Any:
43
+ if hasattr(str_util, name):
44
+ warnings.warn(
45
+ f"Do not import {name!r} from {__package__}.{__name__}, import it from 'urwid'.",
46
+ DeprecationWarning,
47
+ stacklevel=3,
48
+ )
49
+ return getattr(str_util, name)
50
+ raise AttributeError(f"{name} is not defined in {__package__}.{__name__}")
51
+
52
+
53
+ def detect_encoding() -> str:
54
+ # Windows is a special case:
55
+ # CMD is Unicode and non-unicode at the same time:
56
+ # Unicode display support depends on C API usage and partially limited by font settings.
57
+ # By default, python is distributed in "Unicode" version, and since Python version 3.8 only Unicode.
58
+ # In case of curses, "windows-curses" is distributed in unicode-only.
59
+ # Since Windows 10 default console font is already unicode,
60
+ # in older versions maybe need to set TTF font manually to support more symbols
61
+ # (this does not affect unicode IO).
62
+ if sys.platform == "win32" and sys.getdefaultencoding() == "utf-8":
63
+ return "utf-8"
64
+ # Try to determine if using a supported double-byte encoding
65
+ import locale
66
+
67
+ no_set_locale = locale.getpreferredencoding(False)
68
+
69
+ if no_set_locale != "ascii":
70
+ # ascii is fallback locale in case of detect failed
71
+
72
+ return no_set_locale
73
+
74
+ # Use actual `getpreferredencoding` with public API only
75
+ old_loc = locale.setlocale(locale.LC_CTYPE) # == getlocale, but not mangle data
76
+ try:
77
+ with suppress(locale.Error):
78
+ locale.setlocale(locale.LC_CTYPE, "")
79
+ # internally call private `_get_locale_encoding`
80
+ return locale.getpreferredencoding(False)
81
+ finally:
82
+ with suppress(locale.Error):
83
+ locale.setlocale(locale.LC_CTYPE, old_loc)
84
+
85
+
86
+ if "detected_encoding" not in locals():
87
+ detected_encoding = detect_encoding()
88
+ else:
89
+ raise RuntimeError("Encoding detection broken")
90
+
91
+ _target_encoding = "ascii"
92
+ _use_dec_special = True
93
+
94
+
95
+ def set_encoding(encoding: str) -> None:
96
+ """
97
+ Set the byte encoding to assume when processing strings and the
98
+ encoding to use when converting unicode strings.
99
+ """
100
+ encoding = encoding.lower()
101
+
102
+ global _target_encoding, _use_dec_special # noqa: PLW0603 # noqa: PLW0603 # pylint: disable=global-statement
103
+
104
+ if encoding in {"utf-8", "utf8", "utf"}:
105
+ str_util.set_byte_encoding("utf8")
106
+
107
+ _use_dec_special = False
108
+ elif encoding in {
109
+ "euc-jp", # JISX 0208 only
110
+ "euc-kr",
111
+ "euc-cn",
112
+ "euc-tw", # CNS 11643 plain 1 only
113
+ "gb2312",
114
+ "gbk",
115
+ "big5",
116
+ "cn-gb",
117
+ "uhc",
118
+ # these shouldn't happen, should they?
119
+ "eucjp",
120
+ "euckr",
121
+ "euccn",
122
+ "euctw",
123
+ "cncb",
124
+ }:
125
+ str_util.set_byte_encoding("wide")
126
+
127
+ _use_dec_special = True
128
+ else:
129
+ str_util.set_byte_encoding("narrow")
130
+ _use_dec_special = True
131
+
132
+ # if encoding is valid for conversion from unicode, remember it
133
+ _target_encoding = "ascii"
134
+ with contextlib.suppress(LookupError):
135
+ if encoding:
136
+ "".encode(encoding)
137
+ _target_encoding = encoding
138
+
139
+
140
+ def get_encoding() -> str:
141
+ """Get target encoding."""
142
+ return _target_encoding
143
+
144
+
145
+ @contextlib.contextmanager
146
+ def set_temporary_encoding(encoding_name: str) -> Generator[None, None, None]:
147
+ """Internal helper for encoding specific validation in unittests/doctests.
148
+
149
+ Not exported globally.
150
+ """
151
+ old_encoding = _target_encoding
152
+ try:
153
+ set_encoding(encoding_name)
154
+ yield
155
+ finally:
156
+ set_encoding(old_encoding)
157
+
158
+
159
+ def get_encoding_mode() -> Literal["wide", "narrow", "utf8"]:
160
+ """
161
+ Get the mode Urwid is using when processing text strings.
162
+ Returns 'narrow' for 8-bit encodings, 'wide' for CJK encodings
163
+ or 'utf8' for UTF-8 encodings.
164
+ """
165
+ return str_util.get_byte_encoding()
166
+
167
+
168
+ def apply_target_encoding(s: str | bytes) -> tuple[bytes, list[tuple[Literal["U", "0"] | None, int]]]:
169
+ """
170
+ Return (encoded byte string, character set rle).
171
+ """
172
+ # Import locally to warranty no circular imports
173
+ from urwid.display import escape
174
+
175
+ if _use_dec_special and isinstance(s, str):
176
+ # first convert drawing characters
177
+ s = s.translate(escape.DEC_SPECIAL_CHARMAP)
178
+
179
+ if isinstance(s, str):
180
+ s = s.replace(escape.SI + escape.SO, "") # remove redundant shifts
181
+ s = codecs.encode(s, _target_encoding, "replace")
182
+
183
+ if not isinstance(s, bytes):
184
+ raise TypeError(s)
185
+ SO = escape.SO.encode("ascii")
186
+ SI = escape.SI.encode("ascii")
187
+
188
+ sis = s.split(SO)
189
+
190
+ sis0 = sis[0].replace(SI, b"")
191
+ sout = []
192
+ cout = []
193
+ if sis0:
194
+ sout.append(sis0)
195
+ cout.append((None, len(sis0)))
196
+
197
+ if len(sis) == 1:
198
+ return sis0, cout
199
+
200
+ for sn in sis[1:]:
201
+ sl = sn.split(SI, 1)
202
+ if len(sl) == 1:
203
+ sin = sl[0]
204
+ sout.append(sin)
205
+ rle_append_modify(cout, (escape.DEC_TAG, len(sin)))
206
+ continue
207
+
208
+ sin, son = sl
209
+ son = son.replace(SI, b"")
210
+ if sin:
211
+ sout.append(sin)
212
+ rle_append_modify(cout, (escape.DEC_TAG, len(sin)))
213
+
214
+ if son:
215
+ sout.append(son)
216
+ rle_append_modify(cout, (None, len(son)))
217
+
218
+ outstr = b"".join(sout)
219
+ return outstr, cout
220
+
221
+
222
+ ######################################################################
223
+ # Try to set the encoding using the one detected by the locale module
224
+ set_encoding(detected_encoding)
225
+ ######################################################################
226
+
227
+
228
+ def supports_unicode() -> bool:
229
+ """
230
+ Return True if python is able to convert non-ascii unicode strings
231
+ to the current encoding.
232
+ """
233
+ return _target_encoding and _target_encoding != "ascii"
234
+
235
+
236
+ def calc_trim_text(
237
+ text: str | bytes,
238
+ start_offs: int,
239
+ end_offs: int,
240
+ start_col: int,
241
+ end_col: int,
242
+ ) -> tuple[int, int, int, int]:
243
+ """
244
+ Calculate the result of trimming text.
245
+ start_offs -- offset into text to treat as screen column 0
246
+ end_offs -- offset into text to treat as the end of the line
247
+ start_col -- screen column to trim at the left
248
+ end_col -- screen column to trim at the right
249
+
250
+ Returns (start, end, pad_left, pad_right), where:
251
+ start -- resulting start offset
252
+ end -- resulting end offset
253
+ pad_left -- 0 for no pad or 1 for one space to be added
254
+ pad_right -- 0 for no pad or 1 for one space to be added
255
+ """
256
+ spos = start_offs
257
+ pad_left = pad_right = 0
258
+ if start_col > 0:
259
+ spos, sc = str_util.calc_text_pos(text, spos, end_offs, start_col)
260
+ if sc < start_col:
261
+ pad_left = 1
262
+ spos, sc = str_util.calc_text_pos(text, start_offs, end_offs, start_col + 1)
263
+ run = end_col - start_col - pad_left
264
+ pos, sc = str_util.calc_text_pos(text, spos, end_offs, run)
265
+ if sc < run:
266
+ pad_right = 1
267
+ return (spos, pos, pad_left, pad_right)
268
+
269
+
270
+ def trim_text_attr_cs(text: bytes, attr, cs, start_col: int, end_col: int):
271
+ """
272
+ Return ( trimmed text, trimmed attr, trimmed cs ).
273
+ """
274
+ spos, epos, pad_left, pad_right = calc_trim_text(text, 0, len(text), start_col, end_col)
275
+ attrtr = rle_subseg(attr, spos, epos)
276
+ cstr = rle_subseg(cs, spos, epos)
277
+ if pad_left:
278
+ al = rle_get_at(attr, spos - 1)
279
+ rle_prepend_modify(attrtr, (al, 1))
280
+ rle_prepend_modify(cstr, (None, 1))
281
+ if pad_right:
282
+ al = rle_get_at(attr, epos)
283
+ rle_append_modify(attrtr, (al, 1))
284
+ rle_append_modify(cstr, (None, 1))
285
+
286
+ return (b"".rjust(pad_left) + text[spos:epos] + b"".rjust(pad_right), attrtr, cstr)
287
+
288
+
289
+ def rle_get_at(rle, pos: int):
290
+ """
291
+ Return the attribute at offset pos.
292
+ """
293
+ x = 0
294
+ if pos < 0:
295
+ return None
296
+ for a, run in rle:
297
+ if x + run > pos:
298
+ return a
299
+ x += run
300
+ return None
301
+
302
+
303
+ def rle_subseg(rle, start: int, end: int):
304
+ """Return a sub segment of a rle list."""
305
+ sub_segment = []
306
+ x = 0
307
+ for a, run in rle:
308
+ if start:
309
+ if start >= run:
310
+ start -= run
311
+ x += run
312
+ continue
313
+ x += start
314
+ run -= start # noqa: PLW2901
315
+ start = 0
316
+ if x >= end:
317
+ break
318
+ if x + run > end:
319
+ run = end - x # noqa: PLW2901
320
+ x += run
321
+ sub_segment.append((a, run))
322
+ return sub_segment
323
+
324
+
325
+ def rle_len(rle: Iterable[tuple[typing.Any, int]]) -> int:
326
+ """
327
+ Return the number of characters covered by a run length
328
+ encoded attribute list.
329
+ """
330
+
331
+ run = 0
332
+ for v in rle:
333
+ if not isinstance(v, tuple):
334
+ raise TypeError(rle)
335
+ _a, r = v
336
+ run += r
337
+ return run
338
+
339
+
340
+ def rle_prepend_modify(rle, a_r) -> None:
341
+ """
342
+ Append (a, r) (unpacked from *a_r*) to BEGINNING of rle.
343
+ Merge with first run when possible
344
+
345
+ MODIFIES rle parameter contents. Returns None.
346
+ """
347
+ a, r = a_r
348
+ if not rle:
349
+ rle[:] = [(a, r)]
350
+ else:
351
+ al, run = rle[0]
352
+ if a == al:
353
+ rle[0] = (a, run + r)
354
+ else:
355
+ rle[0:0] = [(a, r)]
356
+
357
+
358
+ def rle_append_modify(rle, a_r) -> None:
359
+ """
360
+ Append (a, r) (unpacked from *a_r*) to the rle list rle.
361
+ Merge with last run when possible.
362
+
363
+ MODIFIES rle parameter contents. Returns None.
364
+ """
365
+ a, r = a_r
366
+ if not rle or rle[-1][0] != a:
367
+ rle.append((a, r))
368
+ return
369
+ _la, lr = rle[-1]
370
+ rle[-1] = (a, lr + r)
371
+
372
+
373
+ def rle_join_modify(rle, rle2) -> None:
374
+ """
375
+ Append attribute list rle2 to rle.
376
+ Merge last run of rle with first run of rle2 when possible.
377
+
378
+ MODIFIES attr parameter contents. Returns None.
379
+ """
380
+ if not rle2:
381
+ return
382
+ rle_append_modify(rle, rle2[0])
383
+ rle += rle2[1:]
384
+
385
+
386
+ def rle_product(rle1, rle2):
387
+ """
388
+ Merge the runs of rle1 and rle2 like this:
389
+ eg.
390
+ rle1 = [ ("a", 10), ("b", 5) ]
391
+ rle2 = [ ("Q", 5), ("P", 10) ]
392
+ rle_product: [ (("a","Q"), 5), (("a","P"), 5), (("b","P"), 5) ]
393
+
394
+ rle1 and rle2 are assumed to cover the same total run.
395
+ """
396
+ i1 = i2 = 1 # rle1, rle2 indexes
397
+ if not rle1 or not rle2:
398
+ return []
399
+ a1, r1 = rle1[0]
400
+ a2, r2 = rle2[0]
401
+
402
+ result = []
403
+ while r1 and r2:
404
+ r = min(r1, r2)
405
+ rle_append_modify(result, ((a1, a2), r))
406
+ r1 -= r
407
+ if r1 == 0 and i1 < len(rle1):
408
+ a1, r1 = rle1[i1]
409
+ i1 += 1
410
+ r2 -= r
411
+ if r2 == 0 and i2 < len(rle2):
412
+ a2, r2 = rle2[i2]
413
+ i2 += 1
414
+ return result
415
+
416
+
417
+ def rle_factor(rle):
418
+ """
419
+ Inverse of rle_product.
420
+ """
421
+ rle1 = []
422
+ rle2 = []
423
+ for (a1, a2), r in rle:
424
+ rle_append_modify(rle1, (a1, r))
425
+ rle_append_modify(rle2, (a2, r))
426
+ return rle1, rle2
427
+
428
+
429
+ class TagMarkupException(Exception):
430
+ pass
431
+
432
+
433
+ def decompose_tagmarkup(tm):
434
+ """Return (text string, attribute list) for tagmarkup passed."""
435
+
436
+ tl, al = _tagmarkup_recurse(tm, None)
437
+ # join as unicode or bytes based on type of first element
438
+ if tl:
439
+ text = tl[0][:0].join(tl)
440
+ else:
441
+ text = ""
442
+
443
+ if al and al[-1][0] is None:
444
+ del al[-1]
445
+
446
+ return text, al
447
+
448
+
449
+ def _tagmarkup_recurse(tm, attr):
450
+ """Return (text list, attribute list) for tagmarkup passed.
451
+
452
+ tm -- tagmarkup
453
+ attr -- current attribute or None"""
454
+
455
+ if isinstance(tm, list):
456
+ # for lists recurse to process each subelement
457
+ rtl = []
458
+ ral = []
459
+ for element in tm:
460
+ tl, al = _tagmarkup_recurse(element, attr)
461
+ if ral:
462
+ # merge attributes when possible
463
+ last_attr, last_run = ral[-1]
464
+ top_attr, top_run = al[0]
465
+ if last_attr == top_attr:
466
+ ral[-1] = (top_attr, last_run + top_run)
467
+ del al[0]
468
+ rtl += tl
469
+ ral += al
470
+ return rtl, ral
471
+
472
+ if isinstance(tm, tuple):
473
+ # tuples mark a new attribute boundary
474
+ if len(tm) != 2:
475
+ raise TagMarkupException(f"Tuples must be in the form (attribute, tagmarkup): {tm!r}")
476
+
477
+ attr, element = tm
478
+ return _tagmarkup_recurse(element, attr)
479
+
480
+ if not isinstance(tm, (str, bytes)):
481
+ raise TagMarkupException(f"Invalid markup element: {tm!r}")
482
+
483
+ # text
484
+ return [tm], [(attr, len(tm))]
485
+
486
+
487
+ def is_mouse_event(ev: tuple[str, int, int, int] | typing.Any) -> bool:
488
+ return isinstance(ev, tuple) and len(ev) == 4 and "mouse" in ev[0]
489
+
490
+
491
+ def is_mouse_press(ev: str) -> bool:
492
+ return "press" in ev
493
+
494
+
495
+ class MetaSuper(type):
496
+ """adding .__super"""
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)
512
+
513
+ setattr(cls, f"_{name}__super", _super)
514
+
515
+
516
+ def int_scale(val: int, val_range: int, out_range: int) -> int:
517
+ """
518
+ Scale val in the range [0, val_range-1] to an integer in the range
519
+ [0, out_range-1]. This implementation uses the "round-half-up" rounding
520
+ method.
521
+
522
+ >>> "%x" % int_scale(0x7, 0x10, 0x10000)
523
+ '7777'
524
+ >>> "%x" % int_scale(0x5f, 0x100, 0x10)
525
+ '6'
526
+ >>> int_scale(2, 6, 101)
527
+ 40
528
+ >>> int_scale(1, 3, 4)
529
+ 2
530
+ """
531
+ num = int(val * (out_range - 1) * 2 + (val_range - 1))
532
+ dem = (val_range - 1) * 2
533
+ # if num % dem == 0 then we are exactly half-way and have rounded up.
534
+ return num // dem
535
+
536
+
537
+ class StoppingContext(typing.ContextManager["StoppingContext"]):
538
+ """Context manager that calls ``stop`` on a given object on exit. Used to
539
+ make the ``start`` method on `MainLoop` and `BaseScreen` optionally act as
540
+ context managers.
541
+ """
542
+
543
+ __slots__ = ("_wrapped",)
544
+
545
+ def __init__(self, wrapped: CanBeStopped) -> None:
546
+ self._wrapped = wrapped
547
+
548
+ def __enter__(self) -> Self:
549
+ return self
550
+
551
+ def __exit__(
552
+ self,
553
+ exc_type: type[BaseException] | None,
554
+ exc_val: BaseException | None,
555
+ exc_tb: TracebackType | None,
556
+ ) -> None:
557
+ self._wrapped.stop()
urwid/version.py ADDED
@@ -0,0 +1,16 @@
1
+ # file generated by setuptools_scm
2
+ # don't change, don't track in version control
3
+ TYPE_CHECKING = False
4
+ if TYPE_CHECKING:
5
+ from typing import Tuple, Union
6
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
7
+ else:
8
+ VERSION_TUPLE = object
9
+
10
+ version: str
11
+ __version__: str
12
+ __version_tuple__: VERSION_TUPLE
13
+ version_tuple: VERSION_TUPLE
14
+
15
+ __version__ = version = '2.6.0.post0'
16
+ __version_tuple__ = version_tuple = (2, 6, 0)