urwid 4.0.4__tar.gz → 4.0.6__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 (316) hide show
  1. {urwid-4.0.4/urwid.egg-info → urwid-4.0.6}/PKG-INFO +1 -1
  2. {urwid-4.0.4 → urwid-4.0.6}/docs/changelog.rst +51 -0
  3. urwid-4.0.6/ruff-requirements.txt +1 -0
  4. {urwid-4.0.4 → urwid-4.0.6}/tests/test_columns.py +109 -23
  5. {urwid-4.0.4 → urwid-4.0.6}/tests/test_graphics.py +1 -1
  6. {urwid-4.0.4 → urwid-4.0.6}/tests/test_listbox.py +1 -1
  7. {urwid-4.0.4 → urwid-4.0.6}/tests/test_main_loop.py +1 -1
  8. {urwid-4.0.4 → urwid-4.0.6}/tests/test_overlay.py +2 -2
  9. {urwid-4.0.4 → urwid-4.0.6}/tests/test_pile.py +114 -28
  10. {urwid-4.0.4 → urwid-4.0.6}/tests/test_scrollable.py +66 -0
  11. {urwid-4.0.4 → urwid-4.0.6}/tests/test_signals.py +24 -0
  12. {urwid-4.0.4 → urwid-4.0.6}/tests/test_widget.py +52 -0
  13. {urwid-4.0.4 → urwid-4.0.6}/urwid/__init__.py +2 -0
  14. {urwid-4.0.4 → urwid-4.0.6}/urwid/canvas.py +24 -17
  15. {urwid-4.0.4 → urwid-4.0.6}/urwid/display/_posix_raw_display.py +1 -1
  16. {urwid-4.0.4 → urwid-4.0.6}/urwid/display/_raw_display_base.py +2 -4
  17. {urwid-4.0.4 → urwid-4.0.6}/urwid/display/web.py +14 -9
  18. {urwid-4.0.4 → urwid-4.0.6}/urwid/event_loop/main_loop.py +4 -4
  19. {urwid-4.0.4 → urwid-4.0.6}/urwid/event_loop/tornado_loop.py +1 -1
  20. {urwid-4.0.4 → urwid-4.0.6}/urwid/numedit.py +1 -1
  21. {urwid-4.0.4 → urwid-4.0.6}/urwid/signals.py +1 -1
  22. {urwid-4.0.4 → urwid-4.0.6}/urwid/split_repr.py +1 -1
  23. {urwid-4.0.4 → urwid-4.0.6}/urwid/util.py +12 -7
  24. {urwid-4.0.4 → urwid-4.0.6}/urwid/version.py +3 -3
  25. {urwid-4.0.4 → urwid-4.0.6}/urwid/widget/__init__.py +8 -0
  26. {urwid-4.0.4 → urwid-4.0.6}/urwid/widget/attr_map.py +8 -4
  27. {urwid-4.0.4 → urwid-4.0.6}/urwid/widget/attr_wrap.py +4 -4
  28. {urwid-4.0.4 → urwid-4.0.6}/urwid/widget/box_adapter.py +3 -2
  29. {urwid-4.0.4 → urwid-4.0.6}/urwid/widget/columns.py +50 -33
  30. {urwid-4.0.4 → urwid-4.0.6}/urwid/widget/container.py +15 -20
  31. {urwid-4.0.4 → urwid-4.0.6}/urwid/widget/edit.py +115 -53
  32. {urwid-4.0.4 → urwid-4.0.6}/urwid/widget/filler.py +28 -9
  33. {urwid-4.0.4 → urwid-4.0.6}/urwid/widget/frame.py +46 -24
  34. {urwid-4.0.4 → urwid-4.0.6}/urwid/widget/grid_flow.py +31 -27
  35. {urwid-4.0.4 → urwid-4.0.6}/urwid/widget/line_box.py +4 -4
  36. {urwid-4.0.4 → urwid-4.0.6}/urwid/widget/listbox.py +15 -13
  37. {urwid-4.0.4 → urwid-4.0.6}/urwid/widget/overlay.py +6 -6
  38. {urwid-4.0.4 → urwid-4.0.6}/urwid/widget/padding.py +8 -5
  39. {urwid-4.0.4 → urwid-4.0.6}/urwid/widget/pile.py +41 -33
  40. {urwid-4.0.4 → urwid-4.0.6}/urwid/widget/popup.py +25 -13
  41. {urwid-4.0.4 → urwid-4.0.6}/urwid/widget/scrollable.py +119 -72
  42. {urwid-4.0.4 → urwid-4.0.6}/urwid/widget/widget.py +235 -34
  43. {urwid-4.0.4 → urwid-4.0.6}/urwid/widget/widget_decoration.py +16 -11
  44. {urwid-4.0.4 → urwid-4.0.6}/urwid/widget/wimp.py +4 -4
  45. {urwid-4.0.4 → urwid-4.0.6/urwid.egg-info}/PKG-INFO +1 -1
  46. {urwid-4.0.4 → urwid-4.0.6}/urwid.egg-info/scm_file_list.json +265 -265
  47. urwid-4.0.6/urwid.egg-info/scm_version.json +8 -0
  48. urwid-4.0.4/ruff-requirements.txt +0 -1
  49. urwid-4.0.4/urwid.egg-info/scm_version.json +0 -8
  50. {urwid-4.0.4 → urwid-4.0.6}/.coveralls.yml +0 -0
  51. {urwid-4.0.4 → urwid-4.0.6}/.devcontainer/dev.dockerfile +0 -0
  52. {urwid-4.0.4 → urwid-4.0.6}/.devcontainer/devcontainer.json +0 -0
  53. {urwid-4.0.4 → urwid-4.0.6}/.dockerignore +0 -0
  54. {urwid-4.0.4 → urwid-4.0.6}/.editorconfig +0 -0
  55. {urwid-4.0.4 → urwid-4.0.6}/.github/CODE_OF_CONDUCT.md +0 -0
  56. {urwid-4.0.4 → urwid-4.0.6}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  57. {urwid-4.0.4 → urwid-4.0.6}/.github/ISSUE_TEMPLATE/docs.md +0 -0
  58. {urwid-4.0.4 → urwid-4.0.6}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  59. {urwid-4.0.4 → urwid-4.0.6}/.github/ISSUE_TEMPLATE/question.md +0 -0
  60. {urwid-4.0.4 → urwid-4.0.6}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  61. {urwid-4.0.4 → urwid-4.0.6}/.github/dependabot.yml +0 -0
  62. {urwid-4.0.4 → urwid-4.0.6}/.github/labeler.yml +0 -0
  63. {urwid-4.0.4 → urwid-4.0.6}/.github/release.yml +0 -0
  64. {urwid-4.0.4 → urwid-4.0.6}/.github/workflows/documentation.yml +0 -0
  65. {urwid-4.0.4 → urwid-4.0.6}/.github/workflows/isolated_static_check.yml +0 -0
  66. {urwid-4.0.4 → urwid-4.0.6}/.github/workflows/labels.yml +0 -0
  67. {urwid-4.0.4 → urwid-4.0.6}/.github/workflows/pythonpackage.yml +0 -0
  68. {urwid-4.0.4 → urwid-4.0.6}/.github/workflows/yamllint.yml +0 -0
  69. {urwid-4.0.4 → urwid-4.0.6}/.gitignore +0 -0
  70. {urwid-4.0.4 → urwid-4.0.6}/.pre-commit-config.yaml +0 -0
  71. {urwid-4.0.4 → urwid-4.0.6}/.yamllint.yml +0 -0
  72. {urwid-4.0.4 → urwid-4.0.6}/COPYING +0 -0
  73. {urwid-4.0.4 → urwid-4.0.6}/MANIFEST.in +0 -0
  74. {urwid-4.0.4 → urwid-4.0.6}/README.rst +0 -0
  75. {urwid-4.0.4 → urwid-4.0.6}/SECURITY.md +0 -0
  76. {urwid-4.0.4 → urwid-4.0.6}/classifiers.txt +0 -0
  77. {urwid-4.0.4 → urwid-4.0.6}/docs/Makefile +0 -0
  78. {urwid-4.0.4 → urwid-4.0.6}/docs/conf.py +0 -0
  79. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/bigtext.py +0 -0
  80. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/bigtext.py.xdotool +0 -0
  81. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/bigtext1.png +0 -0
  82. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/bigtext2.png +0 -0
  83. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/bigtext3.png +0 -0
  84. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/browse.py +0 -0
  85. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/browse.py.xdotool +0 -0
  86. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/browse1.png +0 -0
  87. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/browse2.png +0 -0
  88. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/edit.py +0 -0
  89. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/edit.py.xdotool +0 -0
  90. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/edit1.png +0 -0
  91. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/edit2.png +0 -0
  92. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/edit_text.txt +0 -0
  93. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/graph.py +0 -0
  94. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/graph.py.xdotool +0 -0
  95. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/graph1.png +0 -0
  96. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/graph2.png +0 -0
  97. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/index.rst +0 -0
  98. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/palette_test.py +0 -0
  99. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/palette_test.py.xdotool +0 -0
  100. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/palette_test1.png +0 -0
  101. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/palette_test2.png +0 -0
  102. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/pop_up.py +0 -0
  103. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/pop_up.py.xdotool +0 -0
  104. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/pop_up1.png +0 -0
  105. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/pop_up2.png +0 -0
  106. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/real_browse.py +0 -0
  107. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/real_edit.py +0 -0
  108. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/subproc.py +0 -0
  109. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/subproc.py.xdotool +0 -0
  110. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/subproc1.png +0 -0
  111. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/subproc2.png +0 -0
  112. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/subproc2.py +0 -0
  113. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/tour.py +0 -0
  114. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/tour.py.xdotool +0 -0
  115. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/tour1.png +0 -0
  116. {urwid-4.0.4 → urwid-4.0.6}/docs/examples/tour2.png +0 -0
  117. {urwid-4.0.4 → urwid-4.0.6}/docs/index.rst +0 -0
  118. {urwid-4.0.4 → urwid-4.0.6}/docs/manual/bright_combinations.py +0 -0
  119. {urwid-4.0.4 → urwid-4.0.6}/docs/manual/bright_combinations.py.xdotool +0 -0
  120. {urwid-4.0.4 → urwid-4.0.6}/docs/manual/bright_combinations1.png +0 -0
  121. {urwid-4.0.4 → urwid-4.0.6}/docs/manual/canvascache.rst +0 -0
  122. {urwid-4.0.4 → urwid-4.0.6}/docs/manual/displayattributes.rst +0 -0
  123. {urwid-4.0.4 → urwid-4.0.6}/docs/manual/displaymodules.rst +0 -0
  124. {urwid-4.0.4 → urwid-4.0.6}/docs/manual/encodings.rst +0 -0
  125. {urwid-4.0.4 → urwid-4.0.6}/docs/manual/images/display_modules.png +0 -0
  126. {urwid-4.0.4 → urwid-4.0.6}/docs/manual/images/introduction.png +0 -0
  127. {urwid-4.0.4 → urwid-4.0.6}/docs/manual/images/urwid_widgets.svgz +0 -0
  128. {urwid-4.0.4 → urwid-4.0.6}/docs/manual/images/urwid_widgets_1.png +0 -0
  129. {urwid-4.0.4 → urwid-4.0.6}/docs/manual/images/urwid_widgets_1.xcf +0 -0
  130. {urwid-4.0.4 → urwid-4.0.6}/docs/manual/images/urwid_widgets_2.png +0 -0
  131. {urwid-4.0.4 → urwid-4.0.6}/docs/manual/images/urwid_widgets_2.xcf +0 -0
  132. {urwid-4.0.4 → urwid-4.0.6}/docs/manual/images/widget_layout.png +0 -0
  133. {urwid-4.0.4 → urwid-4.0.6}/docs/manual/index.rst +0 -0
  134. {urwid-4.0.4 → urwid-4.0.6}/docs/manual/mainloop.rst +0 -0
  135. {urwid-4.0.4 → urwid-4.0.6}/docs/manual/overview.rst +0 -0
  136. {urwid-4.0.4 → urwid-4.0.6}/docs/manual/safe_combinations.py +0 -0
  137. {urwid-4.0.4 → urwid-4.0.6}/docs/manual/safe_combinations.py.xdotool +0 -0
  138. {urwid-4.0.4 → urwid-4.0.6}/docs/manual/safe_combinations1.png +0 -0
  139. {urwid-4.0.4 → urwid-4.0.6}/docs/manual/textlayout.rst +0 -0
  140. {urwid-4.0.4 → urwid-4.0.6}/docs/manual/userinput.rst +0 -0
  141. {urwid-4.0.4 → urwid-4.0.6}/docs/manual/wanat.py +0 -0
  142. {urwid-4.0.4 → urwid-4.0.6}/docs/manual/wanat_multi.py +0 -0
  143. {urwid-4.0.4 → urwid-4.0.6}/docs/manual/wcur1.py +0 -0
  144. {urwid-4.0.4 → urwid-4.0.6}/docs/manual/wcur2.py +0 -0
  145. {urwid-4.0.4 → urwid-4.0.6}/docs/manual/widgets.rst +0 -0
  146. {urwid-4.0.4 → urwid-4.0.6}/docs/manual/wmod.py +0 -0
  147. {urwid-4.0.4 → urwid-4.0.6}/docs/manual/wsel.py +0 -0
  148. {urwid-4.0.4 → urwid-4.0.6}/docs/reference/attrspec.rst +0 -0
  149. {urwid-4.0.4 → urwid-4.0.6}/docs/reference/canvas.rst +0 -0
  150. {urwid-4.0.4 → urwid-4.0.6}/docs/reference/command_map.rst +0 -0
  151. {urwid-4.0.4 → urwid-4.0.6}/docs/reference/constants.rst +0 -0
  152. {urwid-4.0.4 → urwid-4.0.6}/docs/reference/deprecated.rst +0 -0
  153. {urwid-4.0.4 → urwid-4.0.6}/docs/reference/display_modules.rst +0 -0
  154. {urwid-4.0.4 → urwid-4.0.6}/docs/reference/exceptions.rst +0 -0
  155. {urwid-4.0.4 → urwid-4.0.6}/docs/reference/global_settings.rst +0 -0
  156. {urwid-4.0.4 → urwid-4.0.6}/docs/reference/index.rst +0 -0
  157. {urwid-4.0.4 → urwid-4.0.6}/docs/reference/list_walkers.rst +0 -0
  158. {urwid-4.0.4 → urwid-4.0.6}/docs/reference/main_loop.rst +0 -0
  159. {urwid-4.0.4 → urwid-4.0.6}/docs/reference/meta.rst +0 -0
  160. {urwid-4.0.4 → urwid-4.0.6}/docs/reference/signals.rst +0 -0
  161. {urwid-4.0.4 → urwid-4.0.6}/docs/reference/text_layout.rst +0 -0
  162. {urwid-4.0.4 → urwid-4.0.6}/docs/reference/widget.rst +0 -0
  163. {urwid-4.0.4 → urwid-4.0.6}/docs/tools/compile_pngs.sh +0 -0
  164. {urwid-4.0.4 → urwid-4.0.6}/docs/tools/screenshots.sh +0 -0
  165. {urwid-4.0.4 → urwid-4.0.6}/docs/tools/static/.placeholder +0 -0
  166. {urwid-4.0.4 → urwid-4.0.6}/docs/tools/templates/indexcontent.html +0 -0
  167. {urwid-4.0.4 → urwid-4.0.6}/docs/tools/templates/indexsidebar.html +0 -0
  168. {urwid-4.0.4 → urwid-4.0.6}/docs/tools/templates/localtoc.html +0 -0
  169. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/adventure.py +0 -0
  170. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/adventure.py.xdotool +0 -0
  171. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/adventure1.png +0 -0
  172. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/adventure2.png +0 -0
  173. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/adventure3.png +0 -0
  174. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/adventure4.png +0 -0
  175. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/cmenu.py +0 -0
  176. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/cmenu.py.xdotool +0 -0
  177. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/cmenu1.png +0 -0
  178. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/cmenu2.png +0 -0
  179. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/cmenu3.png +0 -0
  180. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/cmenu4.png +0 -0
  181. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/highcolors.py +0 -0
  182. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/highcolors.py.xdotool +0 -0
  183. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/highcolors1.png +0 -0
  184. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/hmenu.py +0 -0
  185. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/hmenu.py.xdotool +0 -0
  186. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/hmenu1.png +0 -0
  187. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/hmenu2.png +0 -0
  188. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/hmenu3.png +0 -0
  189. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/hmenu4.png +0 -0
  190. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/index.rst +0 -0
  191. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/input.py +0 -0
  192. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/input.py.xdotool +0 -0
  193. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/input1.png +0 -0
  194. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/input2.png +0 -0
  195. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/input3.png +0 -0
  196. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/input4.png +0 -0
  197. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/input5.png +0 -0
  198. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/menu25.png +0 -0
  199. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/minimal.py +0 -0
  200. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/minimal.py.xdotool +0 -0
  201. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/minimal1.png +0 -0
  202. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/multiple.py +0 -0
  203. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/multiple.py.xdotool +0 -0
  204. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/multiple1.png +0 -0
  205. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/multiple2.png +0 -0
  206. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/multiple3.png +0 -0
  207. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/multiple4.png +0 -0
  208. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/qa.py +0 -0
  209. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/qa.py.xdotool +0 -0
  210. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/qa1.png +0 -0
  211. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/qa2.png +0 -0
  212. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/qa3.png +0 -0
  213. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/sig.py +0 -0
  214. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/sig.py.xdotool +0 -0
  215. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/sig1.png +0 -0
  216. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/sig2.png +0 -0
  217. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/sig3.png +0 -0
  218. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/sig4.png +0 -0
  219. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/smenu.py +0 -0
  220. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/smenu.py.xdotool +0 -0
  221. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/smenu1.png +0 -0
  222. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/smenu2.png +0 -0
  223. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/smenu3.png +0 -0
  224. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/urwid_attr.py +0 -0
  225. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/urwid_attr.py.xdotool +0 -0
  226. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/urwid_attr1.png +0 -0
  227. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/urwid_attr2.png +0 -0
  228. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/urwid_attr3.png +0 -0
  229. {urwid-4.0.4 → urwid-4.0.6}/docs/tutorial/urwid_attr4.png +0 -0
  230. {urwid-4.0.4 → urwid-4.0.6}/docs/urwid-logo.png +0 -0
  231. {urwid-4.0.4 → urwid-4.0.6}/examples/asyncio_socket_server.py +0 -0
  232. {urwid-4.0.4 → urwid-4.0.6}/examples/bigtext.py +0 -0
  233. {urwid-4.0.4 → urwid-4.0.6}/examples/browse.py +0 -0
  234. {urwid-4.0.4 → urwid-4.0.6}/examples/calc.py +0 -0
  235. {urwid-4.0.4 → urwid-4.0.6}/examples/dialog.py +0 -0
  236. {urwid-4.0.4 → urwid-4.0.6}/examples/edit.py +0 -0
  237. {urwid-4.0.4 → urwid-4.0.6}/examples/fib.py +0 -0
  238. {urwid-4.0.4 → urwid-4.0.6}/examples/graph.py +0 -0
  239. {urwid-4.0.4 → urwid-4.0.6}/examples/input_test.py +0 -0
  240. {urwid-4.0.4 → urwid-4.0.6}/examples/lcd_cf635.py +0 -0
  241. {urwid-4.0.4 → urwid-4.0.6}/examples/palette_test.py +0 -0
  242. {urwid-4.0.4 → urwid-4.0.6}/examples/pop_up.py +0 -0
  243. {urwid-4.0.4 → urwid-4.0.6}/examples/subproc.py +0 -0
  244. {urwid-4.0.4 → urwid-4.0.6}/examples/subproc2.py +0 -0
  245. {urwid-4.0.4 → urwid-4.0.6}/examples/terminal.py +0 -0
  246. {urwid-4.0.4 → urwid-4.0.6}/examples/tour.py +0 -0
  247. {urwid-4.0.4 → urwid-4.0.6}/examples/treesample.py +0 -0
  248. {urwid-4.0.4 → urwid-4.0.6}/examples/twisted_serve_ssh.py +0 -0
  249. {urwid-4.0.4 → urwid-4.0.6}/examples/twisted_serve_ssh.tac +0 -0
  250. {urwid-4.0.4 → urwid-4.0.6}/pylint-requirements.txt +0 -0
  251. {urwid-4.0.4 → urwid-4.0.6}/pyproject.toml +0 -0
  252. {urwid-4.0.4 → urwid-4.0.6}/requirements.txt +0 -0
  253. {urwid-4.0.4 → urwid-4.0.6}/setup.cfg +0 -0
  254. {urwid-4.0.4 → urwid-4.0.6}/test_requirements.txt +0 -0
  255. {urwid-4.0.4 → urwid-4.0.6}/tests/__init__.py +0 -0
  256. {urwid-4.0.4 → urwid-4.0.6}/tests/test_canvas.py +0 -0
  257. {urwid-4.0.4 → urwid-4.0.6}/tests/test_container.py +0 -0
  258. {urwid-4.0.4 → urwid-4.0.6}/tests/test_doctests.py +0 -0
  259. {urwid-4.0.4 → urwid-4.0.6}/tests/test_escapes.py +0 -0
  260. {urwid-4.0.4 → urwid-4.0.6}/tests/test_event_loops.py +0 -0
  261. {urwid-4.0.4 → urwid-4.0.6}/tests/test_filler.py +0 -0
  262. {urwid-4.0.4 → urwid-4.0.6}/tests/test_floatedit.py +0 -0
  263. {urwid-4.0.4 → urwid-4.0.6}/tests/test_font.py +0 -0
  264. {urwid-4.0.4 → urwid-4.0.6}/tests/test_frame.py +0 -0
  265. {urwid-4.0.4 → urwid-4.0.6}/tests/test_grapheme_clusters.py +0 -0
  266. {urwid-4.0.4 → urwid-4.0.6}/tests/test_grid_flow.py +0 -0
  267. {urwid-4.0.4 → urwid-4.0.6}/tests/test_line_box.py +0 -0
  268. {urwid-4.0.4 → urwid-4.0.6}/tests/test_padding.py +0 -0
  269. {urwid-4.0.4 → urwid-4.0.6}/tests/test_raw_display.py +0 -0
  270. {urwid-4.0.4 → urwid-4.0.6}/tests/test_str_util.py +0 -0
  271. {urwid-4.0.4 → urwid-4.0.6}/tests/test_text_layout.py +0 -0
  272. {urwid-4.0.4 → urwid-4.0.6}/tests/test_tree.py +0 -0
  273. {urwid-4.0.4 → urwid-4.0.6}/tests/test_util.py +0 -0
  274. {urwid-4.0.4 → urwid-4.0.6}/tests/test_vterm.py +0 -0
  275. {urwid-4.0.4 → urwid-4.0.6}/tests/util.py +0 -0
  276. {urwid-4.0.4 → urwid-4.0.6}/tox.ini +0 -0
  277. {urwid-4.0.4 → urwid-4.0.6}/urwid/command_map.py +0 -0
  278. {urwid-4.0.4 → urwid-4.0.6}/urwid/display/__init__.py +0 -0
  279. {urwid-4.0.4 → urwid-4.0.6}/urwid/display/_web.css +0 -0
  280. {urwid-4.0.4 → urwid-4.0.6}/urwid/display/_web.js +0 -0
  281. {urwid-4.0.4 → urwid-4.0.6}/urwid/display/_win32.py +0 -0
  282. {urwid-4.0.4 → urwid-4.0.6}/urwid/display/_win32_raw_display.py +0 -0
  283. {urwid-4.0.4 → urwid-4.0.6}/urwid/display/common.py +0 -0
  284. {urwid-4.0.4 → urwid-4.0.6}/urwid/display/curses.py +0 -0
  285. {urwid-4.0.4 → urwid-4.0.6}/urwid/display/escape.py +0 -0
  286. {urwid-4.0.4 → urwid-4.0.6}/urwid/display/html_fragment.py +0 -0
  287. {urwid-4.0.4 → urwid-4.0.6}/urwid/display/lcd.py +0 -0
  288. {urwid-4.0.4 → urwid-4.0.6}/urwid/display/raw.py +0 -0
  289. {urwid-4.0.4 → urwid-4.0.6}/urwid/event_loop/__init__.py +0 -0
  290. {urwid-4.0.4 → urwid-4.0.6}/urwid/event_loop/abstract_loop.py +0 -0
  291. {urwid-4.0.4 → urwid-4.0.6}/urwid/event_loop/asyncio_loop.py +0 -0
  292. {urwid-4.0.4 → urwid-4.0.6}/urwid/event_loop/glib_loop.py +0 -0
  293. {urwid-4.0.4 → urwid-4.0.6}/urwid/event_loop/select_loop.py +0 -0
  294. {urwid-4.0.4 → urwid-4.0.6}/urwid/event_loop/trio_loop.py +0 -0
  295. {urwid-4.0.4 → urwid-4.0.6}/urwid/event_loop/twisted_loop.py +0 -0
  296. {urwid-4.0.4 → urwid-4.0.6}/urwid/event_loop/zmq_loop.py +0 -0
  297. {urwid-4.0.4 → urwid-4.0.6}/urwid/font.py +0 -0
  298. {urwid-4.0.4 → urwid-4.0.6}/urwid/graphics.py +0 -0
  299. {urwid-4.0.4 → urwid-4.0.6}/urwid/py.typed +0 -0
  300. {urwid-4.0.4 → urwid-4.0.6}/urwid/str_util.py +0 -0
  301. {urwid-4.0.4 → urwid-4.0.6}/urwid/text_layout.py +0 -0
  302. {urwid-4.0.4 → urwid-4.0.6}/urwid/vterm.py +0 -0
  303. {urwid-4.0.4 → urwid-4.0.6}/urwid/widget/bar_graph.py +0 -0
  304. {urwid-4.0.4 → urwid-4.0.6}/urwid/widget/big_text.py +0 -0
  305. {urwid-4.0.4 → urwid-4.0.6}/urwid/widget/constants.py +0 -0
  306. {urwid-4.0.4 → urwid-4.0.6}/urwid/widget/divider.py +0 -0
  307. {urwid-4.0.4 → urwid-4.0.6}/urwid/widget/monitored_list.py +0 -0
  308. {urwid-4.0.4 → urwid-4.0.6}/urwid/widget/progress_bar.py +0 -0
  309. {urwid-4.0.4 → urwid-4.0.6}/urwid/widget/solid_fill.py +0 -0
  310. {urwid-4.0.4 → urwid-4.0.6}/urwid/widget/text.py +0 -0
  311. {urwid-4.0.4 → urwid-4.0.6}/urwid/widget/treetools.py +0 -0
  312. {urwid-4.0.4 → urwid-4.0.6}/urwid.egg-info/SOURCES.txt +0 -0
  313. {urwid-4.0.4 → urwid-4.0.6}/urwid.egg-info/dependency_links.txt +0 -0
  314. {urwid-4.0.4 → urwid-4.0.6}/urwid.egg-info/not-zip-safe +0 -0
  315. {urwid-4.0.4 → urwid-4.0.6}/urwid.egg-info/requires.txt +0 -0
  316. {urwid-4.0.4 → urwid-4.0.6}/urwid.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: urwid
3
- Version: 4.0.4
3
+ Version: 4.0.6
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
@@ -2,6 +2,57 @@
2
2
  Changelog
3
3
  ---------
4
4
 
5
+ Urwid 4.0.6
6
+ ===========
7
+
8
+ 2026-07-23
9
+
10
+ Implemented protocol base classes `AbstractWidget` and internal `AbstractBoxWidget`, `AbstractFlowWidget` and `AbstractFixedWidget` for protocol based typing.
11
+ All classes implementing `AbstractWidget` are considered valid widget (as all required APIs available).
12
+ Container widgets as `Columns`, `Pile`, `GridFlow`, `Frame`, `Overlay` and `Padding` now declare expected APIs from widgets depends on sizing parameter in `__init__`.
13
+
14
+ Bug fixes 🕷
15
+ ++++++++++++
16
+ * Fix web display broken by token_urlsafe session ids by @2018302345 in https://github.com/urwid/urwid/pull/1195
17
+ Documentation 🕮
18
+ ++++++++++++++++
19
+ * Typing: fix scrollable module annotations by @penguinolog in https://github.com/urwid/urwid/pull/1194
20
+ * Typing: introduce `AbstractWidget` abstract base class/protocol by @penguinolog in https://github.com/urwid/urwid/pull/1196
21
+
22
+
23
+ New Contributors
24
+ ++++++++++++++++
25
+ * @2018302345 made their first contribution in https://github.com/urwid/urwid/pull/1195
26
+
27
+ **Full Changelog**: https://github.com/urwid/urwid/compare/4.0.5...4.0.6
28
+
29
+ Urwid 4.0.5
30
+ ===========
31
+
32
+ 2026-07-20
33
+
34
+ New features 🗹
35
+ +++++++++++++++
36
+ * Copy signal handlers before emission by @Sanjays2402 in https://github.com/urwid/urwid/pull/1187
37
+ * Implement `_scrollbar_layout` and left click handling for `Scrollable` by @penguinolog in https://github.com/urwid/urwid/pull/1192
38
+ Bug fixes 🕷
39
+ ++++++++++++
40
+ * Fix rle_product for zero-len elements by @penguinolog in https://github.com/urwid/urwid/pull/1191
41
+ Documentation 🕮
42
+ ++++++++++++++++
43
+ * typing: fix `Filler` typing annotations by @penguinolog in https://github.com/urwid/urwid/pull/1183
44
+ * typing: extend widget_decoration module typing coverage by @penguinolog in https://github.com/urwid/urwid/pull/1184
45
+ * Typing: annotate and fix attr_map by @penguinolog in https://github.com/urwid/urwid/pull/1186
46
+ Refactoring 🛠
47
+ ++++++++++++++
48
+ * Migrate `Edit` widget to use `WidgetWrap` as base class and update methods by @penguinolog in https://github.com/urwid/urwid/pull/1180
49
+
50
+ New Contributors
51
+ ++++++++++++++++
52
+ * @Sanjays2402 made their first contribution in https://github.com/urwid/urwid/pull/1187
53
+
54
+ **Full Changelog**: https://github.com/urwid/urwid/compare/4.0.4...4.0.5
55
+
5
56
  Urwid 4.0.4
6
57
  ===========
7
58
 
@@ -0,0 +1 @@
1
+ ruff==0.15.22
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import typing
4
4
  import unittest
5
+ import warnings
5
6
 
6
7
  import urwid
7
8
  from tests.util import SelectableText
@@ -12,6 +13,85 @@ if typing.TYPE_CHECKING:
12
13
  from typing_extensions import Literal
13
14
 
14
15
 
16
+ class NotAWidget:
17
+ __slots__ = ("name", "symbol")
18
+
19
+ def __init__(self, name: str, symbol: bytes) -> None:
20
+ self.name = name
21
+ self.symbol = symbol
22
+
23
+ def __repr__(self) -> str:
24
+ return f"{self.__class__.__name__}(name={self.name!r}, symbol={self.symbol!r})"
25
+
26
+ def selectable(self) -> bool:
27
+ return False
28
+
29
+ def rows(self, max_col_row: tuple[int], focus: bool = False) -> int:
30
+ return 1
31
+
32
+ def render(self, max_col_row: tuple[int, int] | tuple[int], focus: bool = False) -> urwid.Canvas:
33
+ maxcol = max_col_row[0]
34
+ line = self.symbol * maxcol
35
+ if len(max_col_row) == 1:
36
+ return urwid.TextCanvas((line,), maxcol=maxcol)
37
+ return urwid.TextCanvas((line,) * max_col_row[1], maxcol=maxcol)
38
+
39
+
40
+ class ImplementWidget:
41
+ __slots__ = ("name", "symbol")
42
+
43
+ def sizing(self) -> frozenset[urwid.Sizing]:
44
+ return frozenset((urwid.BOX, urwid.FLOW))
45
+
46
+ @property
47
+ def base_widget(self) -> urwid.AbstractWidget:
48
+ return self
49
+
50
+ def __init__(self, name: str, symbol: bytes) -> None:
51
+ self.name = name
52
+ self.symbol = symbol
53
+
54
+ def __repr__(self) -> str:
55
+ return f"{self.__class__.__name__}(name={self.name!r}, symbol={self.symbol!r})"
56
+
57
+ def selectable(self) -> bool:
58
+ return False
59
+
60
+ @property
61
+ def focus(self) -> None:
62
+ return None
63
+
64
+ def _invalidate(self) -> None:
65
+ pass
66
+
67
+ def rows(self, max_col_row: tuple[int], focus: bool = False) -> int:
68
+ return 1
69
+
70
+ def pack(self, size: typing.Any, focus: bool = False) -> tuple[int, int]:
71
+ return self.rows(size, focus)
72
+
73
+ def render(self, max_col_row: tuple[int, int] | tuple[int], focus: bool = False) -> urwid.Canvas:
74
+ maxcol = max_col_row[0]
75
+ line = self.symbol * maxcol
76
+ if len(max_col_row) == 1:
77
+ return urwid.TextCanvas((line,), maxcol=maxcol)
78
+ return urwid.TextCanvas((line,) * max_col_row[1], maxcol=maxcol)
79
+
80
+ def keypress(self, size: tuple[int, int] | tuple[int], key: str) -> str | None:
81
+ return key
82
+
83
+ def mouse_event(
84
+ self,
85
+ size: tuple[int, int] | tuple[int],
86
+ event: str,
87
+ button: int,
88
+ col: int,
89
+ row: int,
90
+ focus: bool,
91
+ ) -> bool | None:
92
+ return False
93
+
94
+
15
95
  class ColumnsTest(unittest.TestCase):
16
96
  def test_basic_sizing(self) -> None:
17
97
  box_only = urwid.SolidFill("#")
@@ -320,29 +400,6 @@ class ColumnsTest(unittest.TestCase):
320
400
  self.assertEqual((" ",), canvas.decoded_text)
321
401
 
322
402
  def test_not_a_widget(self):
323
- class NotAWidget:
324
- __slots__ = ("name", "symbol")
325
-
326
- def __init__(self, name: str, symbol: bytes) -> None:
327
- self.name = name
328
- self.symbol = symbol
329
-
330
- def __repr__(self) -> str:
331
- return f"{self.__class__.__name__}(name={self.name!r}, symbol={self.symbol!r})"
332
-
333
- def selectable(self) -> bool:
334
- return False
335
-
336
- def rows(self, max_col_row: tuple[int], focus: bool = False) -> int:
337
- return 1
338
-
339
- def render(self, max_col_row: tuple[int, int] | tuple[int], focus: bool = False) -> urwid.Canvas:
340
- maxcol = max_col_row[0]
341
- line = self.symbol * maxcol
342
- if len(max_col_row) == 1:
343
- return urwid.TextCanvas((line,), maxcol=maxcol)
344
- return urwid.TextCanvas((line,) * max_col_row[1], maxcol=maxcol)
345
-
346
403
  with self.subTest("Box"), self.assertWarns(urwid.widget.ColumnsWarning) as ctx:
347
404
  items = (NotAWidget("First", b"*"), NotAWidget("Second", b"^"))
348
405
  widget = urwid.Columns(items)
@@ -359,6 +416,35 @@ class ColumnsTest(unittest.TestCase):
359
416
  self.assertEqual(f"{items[0]!r} is not a Widget", str(ctx.warnings[0].message))
360
417
  self.assertEqual(f"{items[1]!r} is not a Widget", str(ctx.warnings[1].message))
361
418
 
419
+ def test_implement_widget_interface(self):
420
+ with (
421
+ warnings.catch_warnings(record=True) as collected_w,
422
+ self.subTest("Box"),
423
+ ):
424
+ items = (ImplementWidget("First", b"*"), ImplementWidget("Second", b"^"))
425
+ widget = urwid.Columns(items)
426
+
427
+ self.assertEqual(("**^^", "**^^"), widget.render((4, 2)).decoded_text)
428
+
429
+ columns_warnings = tuple(
430
+ warning.message for warning in collected_w if warning.category == urwid.widget.ColumnsWarning
431
+ )
432
+ self.assertTrue(len(columns_warnings) == 0, "No warnings expected")
433
+
434
+ with (
435
+ warnings.catch_warnings(record=True) as collected_w,
436
+ self.subTest("Flow"),
437
+ ):
438
+ items = (ImplementWidget("First", b"*"), ImplementWidget("Second", b"^"))
439
+ widget = urwid.Columns(items)
440
+
441
+ self.assertEqual(("***^^^",), widget.render((6,)).decoded_text)
442
+
443
+ columns_warnings = tuple(
444
+ warning.message for warning in collected_w if warning.category == urwid.widget.ColumnsWarning
445
+ )
446
+ self.assertTrue(len(columns_warnings) == 0, "No warnings expected")
447
+
362
448
  def test_zero_width_column(self):
363
449
  elem_1 = urwid.BoxAdapter(urwid.SolidFill("#"), 2)
364
450
  elem_2 = urwid.BoxAdapter(urwid.SolidFill("*"), 4)
@@ -13,7 +13,7 @@ class BarGraphTest(unittest.TestCase):
13
13
  assert rval == exp, f"{desc} expected {exp!r}, got {rval!r}"
14
14
 
15
15
  def test1(self):
16
- self.bgtest("simplest", [[0]], 5, [1], 1, [(1, [(0, 1)])]),
16
+ self.bgtest("simplest", [[0]], 5, [1], 1, [(1, [(0, 1)])])
17
17
  self.bgtest(
18
18
  "simpler",
19
19
  [[0], [0]],
@@ -1965,7 +1965,7 @@ class PageDownAboveTopTest(unittest.TestCase):
1965
1965
  # Two-row rows so the four-item list does not fit the viewport at once.
1966
1966
  lb = urwid.ListBox(
1967
1967
  urwid.SimpleListWalker(
1968
- [
1968
+ [
1969
1969
  urwid.Text("a0\nb0"),
1970
1970
  SelectableText("a1\nb1"),
1971
1971
  urwid.Text("a2\nb2"),
@@ -22,7 +22,7 @@ class ClosingTemporaryFilesPair(typing.ContextManager[tuple[typing.TextIO, typin
22
22
  Since `sys.stdout` is TextIO, tests have to use compatible api for the proper behavior imitation.
23
23
  """
24
24
 
25
- __slots__ = ("rd_s", "wr_s", "rd_f", "wr_f")
25
+ __slots__ = ("rd_f", "rd_s", "wr_f", "wr_s")
26
26
 
27
27
  def __init__(self) -> None:
28
28
  self.rd_s: socket.socket | None = None
@@ -154,7 +154,7 @@ class OverlayTest(unittest.TestCase):
154
154
  self.assertEqual(rows, canvas.rows())
155
155
 
156
156
  self.assertEqual(
157
- "##########################\n" "#Flow and Fixed widget ##\n" "##########################",
157
+ "##########################\n#Flow and Fixed widget ##\n##########################",
158
158
  str(canvas),
159
159
  )
160
160
 
@@ -182,7 +182,7 @@ class OverlayTest(unittest.TestCase):
182
182
  self.assertEqual(rows, canvas.rows())
183
183
 
184
184
  self.assertEqual(
185
- "#########################\n" "#Flow and Fixed widget #\n" "#########################",
185
+ "#########################\n#Flow and Fixed widget #\n#########################",
186
186
  str(canvas),
187
187
  )
188
188
 
@@ -1,11 +1,96 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import unittest
4
+ import warnings
4
5
 
5
6
  import urwid
6
7
  from tests.util import SelectableText
7
8
 
8
9
 
10
+ class NotAWidget:
11
+ __slots__ = ("name", "symbol")
12
+
13
+ def __init__(self, name: str, symbol: bytes) -> None:
14
+ self.name = name
15
+ self.symbol = symbol
16
+
17
+ def __repr__(self) -> str:
18
+ return f"{self.__class__.__name__}(name={self.name!r}, symbol={self.symbol!r})"
19
+
20
+ def selectable(self) -> bool:
21
+ return False
22
+
23
+ def pack(self, max_col_row: tuple[int, int] | tuple[int], focus: bool = False) -> int:
24
+ if len(max_col_row) == 2:
25
+ return max_col_row
26
+ return max_col_row[0], self.rows(max_col_row)
27
+
28
+ def rows(self, max_col_row: tuple[int], focus=False) -> int:
29
+ return 1
30
+
31
+ def render(self, max_col_row: tuple[int, int] | tuple[int], focus: bool = False) -> urwid.Canvas:
32
+ maxcol = max_col_row[0]
33
+ line = self.symbol * maxcol
34
+ if len(max_col_row) == 1:
35
+ return urwid.TextCanvas((line,), maxcol=maxcol)
36
+ return urwid.TextCanvas((line,) * max_col_row[1], maxcol=maxcol)
37
+
38
+
39
+ class ImplementWidget:
40
+ __slots__ = ("name", "symbol")
41
+
42
+ def sizing(self) -> frozenset[urwid.Sizing]:
43
+ return frozenset((urwid.BOX, urwid.FLOW))
44
+
45
+ @property
46
+ def base_widget(self) -> urwid.AbstractWidget:
47
+ return self
48
+
49
+ def __init__(self, name: str, symbol: bytes) -> None:
50
+ self.name = name
51
+ self.symbol = symbol
52
+
53
+ def __repr__(self) -> str:
54
+ return f"{self.__class__.__name__}(name={self.name!r}, symbol={self.symbol!r})"
55
+
56
+ def selectable(self) -> bool:
57
+ return False
58
+
59
+ @property
60
+ def focus(self) -> None:
61
+ return None
62
+
63
+ def _invalidate(self) -> None:
64
+ pass
65
+
66
+ def rows(self, max_col_row: tuple[int], focus: bool = False) -> int:
67
+ return 1
68
+
69
+ def pack(self, size: typing.Any, focus: bool = False) -> tuple[int, int]:
70
+ return self.rows(size, focus)
71
+
72
+ def render(self, max_col_row: tuple[int, int] | tuple[int], focus: bool = False) -> urwid.Canvas:
73
+ maxcol = max_col_row[0]
74
+ line = self.symbol * maxcol
75
+ if len(max_col_row) == 1:
76
+ return urwid.TextCanvas((line,), maxcol=maxcol)
77
+ return urwid.TextCanvas((line,) * max_col_row[1], maxcol=maxcol)
78
+
79
+ def keypress(self, size: tuple[int, int] | tuple[int], key: str) -> str | None:
80
+ return key
81
+
82
+ def mouse_event(
83
+ self,
84
+ size: tuple[int, int] | tuple[int],
85
+ event: str,
86
+ button: int,
87
+ col: int,
88
+ row: int,
89
+ focus: bool,
90
+ ) -> bool | None:
91
+ return False
92
+
93
+
9
94
  class PileTest(unittest.TestCase):
10
95
  def test_basic_sizing(self) -> None:
11
96
  box_only = urwid.SolidFill("#")
@@ -247,34 +332,6 @@ class PileTest(unittest.TestCase):
247
332
  self.assertEqual("Cancel", widget.focus.focus.label)
248
333
 
249
334
  def test_not_a_widget(self):
250
- class NotAWidget:
251
- __slots__ = ("name", "symbol")
252
-
253
- def __init__(self, name: str, symbol: bytes) -> None:
254
- self.name = name
255
- self.symbol = symbol
256
-
257
- def __repr__(self) -> str:
258
- return f"{self.__class__.__name__}(name={self.name!r}, symbol={self.symbol!r})"
259
-
260
- def selectable(self) -> bool:
261
- return False
262
-
263
- def pack(self, max_col_row: tuple[int, int] | tuple[int], focus: bool = False) -> int:
264
- if len(max_col_row) == 2:
265
- return max_col_row
266
- return max_col_row[0], self.rows(max_col_row)
267
-
268
- def rows(self, max_col_row: tuple[int], focus=False) -> int:
269
- return 1
270
-
271
- def render(self, max_col_row: tuple[int, int] | tuple[int], focus: bool = False) -> urwid.Canvas:
272
- maxcol = max_col_row[0]
273
- line = self.symbol * maxcol
274
- if len(max_col_row) == 1:
275
- return urwid.TextCanvas((line,), maxcol=maxcol)
276
- return urwid.TextCanvas((line,) * max_col_row[1], maxcol=maxcol)
277
-
278
335
  with self.subTest("Box"), self.assertWarns(urwid.widget.PileWarning) as ctx:
279
336
  items = (NotAWidget("First", b"*"), NotAWidget("Second", b"^"))
280
337
  widget = urwid.Pile(items)
@@ -291,6 +348,35 @@ class PileTest(unittest.TestCase):
291
348
  self.assertEqual(f"{items[0]!r} is not a Widget", str(ctx.warnings[0].message))
292
349
  self.assertEqual(f"{items[1]!r} is not a Widget", str(ctx.warnings[1].message))
293
350
 
351
+ def test_implement_widget_interface(self):
352
+ with (
353
+ warnings.catch_warnings(record=True) as collected_w,
354
+ self.subTest("Box"),
355
+ ):
356
+ items = (ImplementWidget("First", b"*"), ImplementWidget("Second", b"^"))
357
+ widget = urwid.Pile(items)
358
+
359
+ self.assertEqual(("****", "^^^^"), widget.render((4, 2)).decoded_text)
360
+
361
+ pile_warnings = tuple(
362
+ warning.message for warning in collected_w if warning.category == urwid.widget.PileWarning
363
+ )
364
+ self.assertTrue(len(pile_warnings) == 0, "No warnings expected")
365
+
366
+ with (
367
+ warnings.catch_warnings(record=True) as collected_w,
368
+ self.subTest("Flow"),
369
+ ):
370
+ items = (ImplementWidget("First", b"*"), ImplementWidget("Second", b"^"))
371
+ widget = urwid.Pile(items)
372
+
373
+ self.assertEqual(("******", "^^^^^^"), widget.render((6,)).decoded_text)
374
+
375
+ pile_warnings = tuple(
376
+ warning.message for warning in collected_w if warning.category == urwid.widget.PileWarning
377
+ )
378
+ self.assertTrue(len(pile_warnings) == 0, "No warnings expected")
379
+
294
380
  def ktest(self, desc, contents, focus_item, key, rkey, rfocus, rpref_col):
295
381
  p = urwid.Pile(contents, focus_item)
296
382
  rval = p.keypress((20,), key)
@@ -142,6 +142,72 @@ class TestScrollBarScrollable(unittest.TestCase):
142
142
 
143
143
  self.assertEqual(top_position_rendered, widget.render(reduced_size).decoded_text)
144
144
 
145
+ def test_mouse_left_click_scrollbar(self):
146
+ """Left click on the rendered scrollbar jumps to the clicked position."""
147
+ content = urwid.Text("\n".join(string.ascii_letters)) # 52 single-char lines
148
+ reduced_size = (3, 5)
149
+ widget = urwid.ScrollBar(urwid.Scrollable(content))
150
+ scrollable = widget.original_widget
151
+ scrollbar_col = reduced_size[0] - 1 # right side scrollbar
152
+
153
+ self.assertEqual(0, scrollable.get_scrollpos())
154
+
155
+ # posmax == 52 - 5 == 47, thumb_height == 1, thumb travel == 5 - 1 == 4.
156
+ # Clicking the bottom row moves past the end and is clamped to posmax.
157
+ self.assertTrue(widget.mouse_event(reduced_size, "mouse press", 1, scrollbar_col, 4, False))
158
+ self.assertEqual(47, scrollable.get_scrollpos())
159
+
160
+ # Clicking the top row scrolls back to the top.
161
+ self.assertTrue(widget.mouse_event(reduced_size, "mouse press", 1, scrollbar_col, 0, False))
162
+ self.assertEqual(0, scrollable.get_scrollpos())
163
+
164
+ # Intermediate rows map proportionally: round(row * posmax / travel).
165
+ self.assertTrue(widget.mouse_event(reduced_size, "mouse press", 1, scrollbar_col, 1, False))
166
+ self.assertEqual(12, scrollable.get_scrollpos())
167
+
168
+ self.assertTrue(widget.mouse_event(reduced_size, "mouse press", 1, scrollbar_col, 3, False))
169
+ self.assertEqual(35, scrollable.get_scrollpos())
170
+
171
+ def test_mouse_left_click_content_ignored(self):
172
+ """Left click outside of the scrollbar columns does not scroll."""
173
+ content = urwid.Text("\n".join(string.ascii_letters))
174
+ reduced_size = (3, 5)
175
+ widget = urwid.ScrollBar(urwid.Scrollable(content))
176
+ scrollable = widget.original_widget
177
+
178
+ self.assertEqual(0, scrollable.get_scrollpos())
179
+
180
+ # Column 0 belongs to the wrapped content, not the scrollbar.
181
+ self.assertFalse(widget.mouse_event(reduced_size, "mouse press", 1, 0, 4, False))
182
+ self.assertEqual(0, scrollable.get_scrollpos())
183
+
184
+ def test_mouse_left_click_left_side_scrollbar(self):
185
+ """Left click detection honours a left-aligned scrollbar."""
186
+ content = urwid.Text("\n".join(string.ascii_letters))
187
+ reduced_size = (3, 5)
188
+ widget = urwid.ScrollBar(urwid.Scrollable(content), side="left")
189
+ scrollable = widget.original_widget
190
+
191
+ self.assertEqual(0, scrollable.get_scrollpos())
192
+
193
+ # The scrollbar occupies column 0 when aligned to the left.
194
+ self.assertTrue(widget.mouse_event(reduced_size, "mouse press", 1, 0, 4, False))
195
+ self.assertEqual(47, scrollable.get_scrollpos())
196
+
197
+ # A click on the content columns is ignored.
198
+ self.assertFalse(widget.mouse_event(reduced_size, "mouse press", 1, 1, 0, False))
199
+ self.assertEqual(47, scrollable.get_scrollpos())
200
+
201
+ def test_mouse_left_click_without_scrollbar(self):
202
+ """Without a rendered scrollbar a left click is not handled."""
203
+ content = urwid.Text("a\nb") # fits into the available height
204
+ reduced_size = (3, 5)
205
+ widget = urwid.ScrollBar(urwid.Scrollable(content))
206
+ scrollable = widget.original_widget
207
+
208
+ self.assertFalse(widget.mouse_event(reduced_size, "mouse press", 1, 2, 2, False))
209
+ self.assertEqual(0, scrollable.get_scrollpos())
210
+
145
211
  def test_alt_symbols(self):
146
212
  long_content = urwid.Text(LGPL_HEADER)
147
213
  reduced_size = (40, 5)
@@ -25,6 +25,30 @@ class SiglnalsTest(unittest.TestCase):
25
25
  edit.set_edit_text("another text")
26
26
  handler.assert_not_called()
27
27
 
28
+ def test_handler_mutation_during_emit(self):
29
+ emitter = self.EmClass()
30
+ calls = []
31
+
32
+ def third():
33
+ calls.append("third")
34
+
35
+ def first():
36
+ calls.append("first")
37
+ disconnect_signal(emitter, "test", first)
38
+ connect_signal(emitter, "test", third)
39
+
40
+ def second():
41
+ calls.append("second")
42
+
43
+ connect_signal(emitter, "test", first)
44
+ connect_signal(emitter, "test", second)
45
+
46
+ emit_signal(emitter, "test")
47
+ self.assertEqual(calls, ["first", "second"])
48
+
49
+ emit_signal(emitter, "test")
50
+ self.assertEqual(calls, ["first", "second", "second", "third"])
51
+
28
52
  @unittest.skipIf(sys.implementation.name == "pypy", "WeakRef works differently on PyPy")
29
53
  def test_weak_del(self):
30
54
  emitter = SiglnalsTest.EmClass()
@@ -39,6 +39,58 @@ class TextTest(unittest.TestCase):
39
39
  got = urwid.Text("û").render((3,))._text
40
40
  assert got == expected, f"got: {got!r} expected: {expected!r}"
41
41
 
42
+ def test_empty_markup_segment_middle(self) -> None:
43
+ """An empty markup segment in the middle must not truncate the rendered row."""
44
+ widget = urwid.Text([("a", "X" * 10), ("b", ""), ("c", " END")])
45
+
46
+ self.assertEqual(("XXXXXXXXXX END", [("a", 10), ("b", 0), ("c", 4)]), widget.get_text())
47
+
48
+ canvas = widget.render(())
49
+ self.assertEqual([b"XXXXXXXXXX END"], canvas.text)
50
+ self.assertEqual(("XXXXXXXXXX END",), canvas.decoded_text)
51
+
52
+ def test_empty_markup_segment_start(self) -> None:
53
+ """An empty markup segment at the start must keep the whole rendered row."""
54
+ widget = urwid.Text([("b", ""), ("a", "X" * 10), ("c", " END")])
55
+
56
+ self.assertEqual(("XXXXXXXXXX END", [("b", 0), ("a", 10), ("c", 4)]), widget.get_text())
57
+
58
+ canvas = widget.render(())
59
+ self.assertEqual([b"XXXXXXXXXX END"], canvas.text)
60
+ self.assertEqual(("XXXXXXXXXX END",), canvas.decoded_text)
61
+
62
+ def test_empty_markup_segment_end(self) -> None:
63
+ """An empty markup segment at the end must keep the whole rendered row."""
64
+ widget = urwid.Text([("a", "X" * 10), ("c", " END"), ("b", "")])
65
+
66
+ self.assertEqual(("XXXXXXXXXX END", [("a", 10), ("c", 4), ("b", 0)]), widget.get_text())
67
+
68
+ canvas = widget.render(())
69
+ self.assertEqual([b"XXXXXXXXXX END"], canvas.text)
70
+ self.assertEqual(("XXXXXXXXXX END",), canvas.decoded_text)
71
+
72
+ def test_multiple_empty_markup_segments(self) -> None:
73
+ """Several consecutive empty markup segments must not drop following content."""
74
+ widget = urwid.Text([("a", "AB"), ("b", ""), ("c", ""), ("d", "CD")])
75
+
76
+ self.assertEqual(("ABCD", [("a", 2), ("b", 0), ("c", 0), ("d", 2)]), widget.get_text())
77
+
78
+ canvas = widget.render(())
79
+ self.assertEqual([b"ABCD"], canvas.text)
80
+ self.assertEqual(("ABCD",), canvas.decoded_text)
81
+
82
+ def test_empty_markup_segment_render_content(self) -> None:
83
+ """The canvas content rows must expose the display attribute of each segment."""
84
+ widget = urwid.Text([("a", "X" * 10), ("b", ""), ("c", " END")])
85
+
86
+ content = list(widget.render(()).content())
87
+ attrs = [attr for row in content for attr, _cs, _text in row]
88
+ rendered = b"".join(text for row in content for _attr, _cs, text in row)
89
+
90
+ self.assertEqual(b"XXXXXXXXXX END", rendered)
91
+ self.assertIn("a", attrs)
92
+ self.assertIn("c", attrs)
93
+
42
94
 
43
95
  class EditTest(unittest.TestCase):
44
96
  def setUp(self):
@@ -140,6 +140,7 @@ from .widget import (
140
140
  SPACE,
141
141
  TOP,
142
142
  WEIGHT,
143
+ AbstractWidget,
143
144
  Align,
144
145
  AttrMap,
145
146
  AttrMapError,
@@ -262,6 +263,7 @@ __all__: tuple[str, ...] = (
262
263
  "WEIGHT",
263
264
  "WHITE",
264
265
  "YELLOW",
266
+ "AbstractWidget",
265
267
  "Align",
266
268
  "AsyncioEventLoop",
267
269
  "AttrMap",