visidata 3.1__tar.gz → 3.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 (311) hide show
  1. {visidata-3.1/visidata.egg-info → visidata-3.2}/PKG-INFO +5 -10
  2. {visidata-3.1 → visidata-3.2}/README.md +1 -8
  3. {visidata-3.1 → visidata-3.2}/setup.py +37 -10
  4. {visidata-3.1 → visidata-3.2}/visidata/__init__.py +2 -2
  5. {visidata-3.1 → visidata-3.2}/visidata/_input.py +70 -36
  6. {visidata-3.1 → visidata-3.2}/visidata/_open.py +9 -6
  7. {visidata-3.1 → visidata-3.2}/visidata/_types.py +2 -2
  8. {visidata-3.1 → visidata-3.2}/visidata/aggregators.py +125 -16
  9. {visidata-3.1 → visidata-3.2}/visidata/apps/vdsql/_ibis.py +8 -13
  10. {visidata-3.1 → visidata-3.2}/visidata/basesheet.py +4 -1
  11. {visidata-3.1 → visidata-3.2}/visidata/canvas.py +11 -7
  12. {visidata-3.1 → visidata-3.2}/visidata/clipboard.py +11 -2
  13. {visidata-3.1 → visidata-3.2}/visidata/cliptext.py +65 -23
  14. {visidata-3.1 → visidata-3.2}/visidata/cmdlog.py +5 -1
  15. {visidata-3.1 → visidata-3.2}/visidata/column.py +6 -2
  16. {visidata-3.1 → visidata-3.2}/visidata/ddwplay.py +2 -2
  17. {visidata-3.1 → visidata-3.2}/visidata/deprecated.py +91 -63
  18. visidata-3.2/visidata/errors.py +71 -0
  19. {visidata-3.1/visidata/features → visidata-3.2/visidata/experimental}/helloworld.py +1 -1
  20. {visidata-3.1 → visidata-3.2}/visidata/expr.py +1 -0
  21. {visidata-3.1 → visidata-3.2}/visidata/extensible.py +4 -0
  22. {visidata-3.1 → visidata-3.2}/visidata/features/cmdpalette.py +3 -3
  23. {visidata-3.1 → visidata-3.2}/visidata/features/describe.py +2 -2
  24. {visidata-3.1 → visidata-3.2}/visidata/features/expand_cols.py +8 -5
  25. {visidata-3.1 → visidata-3.2}/visidata/features/freeze.py +14 -2
  26. {visidata-3.1 → visidata-3.2}/visidata/features/go_col.py +2 -1
  27. visidata-3.2/visidata/features/graph_zoom_y.py +47 -0
  28. {visidata-3.1 → visidata-3.2}/visidata/features/incr.py +7 -3
  29. {visidata-3.1 → visidata-3.2}/visidata/features/join.py +23 -12
  30. {visidata-3.1 → visidata-3.2}/visidata/features/layout.py +8 -3
  31. {visidata-3.1 → visidata-3.2}/visidata/features/melt.py +1 -0
  32. visidata-3.2/visidata/features/rank.py +103 -0
  33. {visidata-3.1 → visidata-3.2}/visidata/features/reload_every.py +9 -6
  34. {visidata-3.1 → visidata-3.2}/visidata/features/sysedit.py +14 -4
  35. {visidata-3.1 → visidata-3.2}/visidata/features/transpose.py +1 -0
  36. {visidata-3.1 → visidata-3.2}/visidata/features/window.py +12 -0
  37. {visidata-3.1 → visidata-3.2}/visidata/form.py +4 -4
  38. {visidata-3.1 → visidata-3.2}/visidata/freqtbl.py +47 -3
  39. {visidata-3.1 → visidata-3.2}/visidata/fuzzymatch.py +8 -5
  40. {visidata-3.1 → visidata-3.2}/visidata/graph.py +5 -3
  41. visidata-3.2/visidata/guides/AggregatorsSheet.md +84 -0
  42. {visidata-3.1 → visidata-3.2}/visidata/guides/MacrosSheet.md +1 -1
  43. visidata-3.2/visidata/guides/RankGuide.md +51 -0
  44. {visidata-3.1 → visidata-3.2}/visidata/guides/TypesSheet.md +1 -1
  45. visidata-3.2/visidata/guides/WindowFunctionGuide.md +49 -0
  46. {visidata-3.1 → visidata-3.2}/visidata/help.py +3 -4
  47. {visidata-3.1 → visidata-3.2}/visidata/indexsheet.py +1 -1
  48. {visidata-3.1 → visidata-3.2}/visidata/loaders/_pandas.py +3 -1
  49. {visidata-3.1 → visidata-3.2}/visidata/loaders/archive.py +6 -3
  50. {visidata-3.1 → visidata-3.2}/visidata/loaders/csv.py +5 -1
  51. {visidata-3.1 → visidata-3.2}/visidata/loaders/eml.py +2 -0
  52. {visidata-3.1 → visidata-3.2}/visidata/loaders/f5log.py +2 -2
  53. {visidata-3.1 → visidata-3.2}/visidata/loaders/fec.py +6 -9
  54. {visidata-3.1 → visidata-3.2}/visidata/loaders/fixed_width.py +2 -0
  55. {visidata-3.1 → visidata-3.2}/visidata/loaders/hdf5.py +34 -10
  56. visidata-3.2/visidata/loaders/npy.py +128 -0
  57. {visidata-3.1 → visidata-3.2}/visidata/loaders/orgmode.py +3 -2
  58. {visidata-3.1 → visidata-3.2}/visidata/loaders/pandas_freqtbl.py +4 -0
  59. visidata-3.2/visidata/loaders/psv.py +13 -0
  60. {visidata-3.1 → visidata-3.2}/visidata/loaders/sqlite.py +1 -1
  61. {visidata-3.1 → visidata-3.2}/visidata/loaders/vds.py +3 -4
  62. {visidata-3.1 → visidata-3.2}/visidata/macros.py +4 -3
  63. {visidata-3.1 → visidata-3.2}/visidata/main.py +11 -5
  64. {visidata-3.1 → visidata-3.2}/visidata/mainloop.py +7 -4
  65. {visidata-3.1 → visidata-3.2}/visidata/man/parse_options.py +3 -2
  66. {visidata-3.1 → visidata-3.2}/visidata/man/vd.1 +26 -14
  67. {visidata-3.1 → visidata-3.2}/visidata/man/vd.txt +25 -14
  68. {visidata-3.1 → visidata-3.2}/visidata/man/visidata.1 +26 -14
  69. {visidata-3.1 → visidata-3.2}/visidata/menu.py +9 -9
  70. {visidata-3.1 → visidata-3.2}/visidata/metasheets.py +3 -3
  71. {visidata-3.1 → visidata-3.2}/visidata/mouse.py +1 -0
  72. {visidata-3.1 → visidata-3.2}/visidata/pyobj.py +17 -9
  73. {visidata-3.1 → visidata-3.2}/visidata/save.py +5 -1
  74. {visidata-3.1 → visidata-3.2}/visidata/selection.py +29 -18
  75. {visidata-3.1 → visidata-3.2}/visidata/settings.py +2 -2
  76. {visidata-3.1 → visidata-3.2}/visidata/sheets.py +52 -24
  77. {visidata-3.1 → visidata-3.2}/visidata/shell.py +2 -2
  78. {visidata-3.1 → visidata-3.2}/visidata/sidebar.py +4 -2
  79. visidata-3.2/visidata/sort.py +177 -0
  80. {visidata-3.1 → visidata-3.2}/visidata/statusbar.py +10 -9
  81. visidata-3.2/visidata/tests/test_cliptext.py +190 -0
  82. {visidata-3.1 → visidata-3.2}/visidata/tests/test_commands.py +5 -2
  83. {visidata-3.1 → visidata-3.2}/visidata/tests/test_menu.py +1 -1
  84. {visidata-3.1 → visidata-3.2}/visidata/textsheet.py +34 -8
  85. {visidata-3.1 → visidata-3.2}/visidata/themes/ascii8.py +2 -2
  86. {visidata-3.1 → visidata-3.2}/visidata/themes/light.py +5 -0
  87. {visidata-3.1 → visidata-3.2}/visidata/threads.py +16 -8
  88. {visidata-3.1 → visidata-3.2}/visidata/undo.py +1 -1
  89. visidata-3.2/visidata/vendor/__init__.py +0 -0
  90. {visidata-3.1 → visidata-3.2/visidata.egg-info}/PKG-INFO +5 -10
  91. {visidata-3.1 → visidata-3.2}/visidata.egg-info/SOURCES.txt +8 -1
  92. {visidata-3.1 → visidata-3.2}/visidata.egg-info/entry_points.txt +1 -0
  93. visidata-3.2/visidata.egg-info/requires.txt +100 -0
  94. visidata-3.1/visidata/errors.py +0 -35
  95. visidata-3.1/visidata/loaders/npy.py +0 -97
  96. visidata-3.1/visidata/sort.py +0 -99
  97. visidata-3.1/visidata/tests/test_cliptext.py +0 -39
  98. visidata-3.1/visidata.egg-info/requires.txt +0 -30
  99. {visidata-3.1 → visidata-3.2}/LICENSE.gpl3 +0 -0
  100. {visidata-3.1 → visidata-3.2}/MANIFEST.in +0 -0
  101. {visidata-3.1 → visidata-3.2}/bin/vd +0 -0
  102. {visidata-3.1 → visidata-3.2}/bin/vd2to3.vdx +0 -0
  103. {visidata-3.1 → visidata-3.2}/setup.cfg +0 -0
  104. {visidata-3.1 → visidata-3.2}/visidata/__main__.py +0 -0
  105. {visidata-3.1 → visidata-3.2}/visidata/_urlcache.py +0 -0
  106. {visidata-3.1 → visidata-3.2}/visidata/apps/__init__.py +0 -0
  107. {visidata-3.1 → visidata-3.2}/visidata/apps/vdsql/__about__.py +0 -0
  108. {visidata-3.1 → visidata-3.2}/visidata/apps/vdsql/__init__.py +0 -0
  109. {visidata-3.1 → visidata-3.2}/visidata/apps/vdsql/__main__.py +0 -0
  110. {visidata-3.1 → visidata-3.2}/visidata/apps/vdsql/bigquery.py +0 -0
  111. {visidata-3.1 → visidata-3.2}/visidata/apps/vdsql/clickhouse.py +0 -0
  112. {visidata-3.1 → visidata-3.2}/visidata/apps/vdsql/setup.py +0 -0
  113. {visidata-3.1 → visidata-3.2}/visidata/apps/vdsql/snowflake.py +0 -0
  114. {visidata-3.1 → visidata-3.2}/visidata/apps/vgit/__init__.py +0 -0
  115. {visidata-3.1 → visidata-3.2}/visidata/apps/vgit/__main__.py +0 -0
  116. {visidata-3.1 → visidata-3.2}/visidata/apps/vgit/abort.py +0 -0
  117. {visidata-3.1 → visidata-3.2}/visidata/apps/vgit/blame.py +0 -0
  118. {visidata-3.1 → visidata-3.2}/visidata/apps/vgit/branch.py +0 -0
  119. {visidata-3.1 → visidata-3.2}/visidata/apps/vgit/config.py +0 -0
  120. {visidata-3.1 → visidata-3.2}/visidata/apps/vgit/diff.py +0 -0
  121. {visidata-3.1 → visidata-3.2}/visidata/apps/vgit/gitsheet.py +0 -0
  122. {visidata-3.1 → visidata-3.2}/visidata/apps/vgit/grep.py +0 -0
  123. {visidata-3.1 → visidata-3.2}/visidata/apps/vgit/log.py +0 -0
  124. {visidata-3.1 → visidata-3.2}/visidata/apps/vgit/main.py +0 -0
  125. {visidata-3.1 → visidata-3.2}/visidata/apps/vgit/remote.py +0 -0
  126. {visidata-3.1 → visidata-3.2}/visidata/apps/vgit/repos.py +0 -0
  127. {visidata-3.1 → visidata-3.2}/visidata/apps/vgit/setup.py +0 -0
  128. {visidata-3.1 → visidata-3.2}/visidata/apps/vgit/stash.py +0 -0
  129. {visidata-3.1 → visidata-3.2}/visidata/apps/vgit/status.py +0 -0
  130. {visidata-3.1 → visidata-3.2}/visidata/apps/vgit/statusbar.py +0 -0
  131. {visidata-3.1 → visidata-3.2}/visidata/bezier.py +0 -0
  132. {visidata-3.1 → visidata-3.2}/visidata/canvas_text.py +0 -0
  133. {visidata-3.1 → visidata-3.2}/visidata/choose.py +0 -0
  134. {visidata-3.1 → visidata-3.2}/visidata/clean_names.py +0 -0
  135. {visidata-3.1 → visidata-3.2}/visidata/color.py +0 -0
  136. {visidata-3.1 → visidata-3.2}/visidata/ddw/input.ddw +0 -0
  137. {visidata-3.1 → visidata-3.2}/visidata/ddw/regex.ddw +0 -0
  138. {visidata-3.1 → visidata-3.2}/visidata/desktop/visidata.desktop +0 -0
  139. {visidata-3.1 → visidata-3.2}/visidata/editor.py +0 -0
  140. {visidata-3.1 → visidata-3.2}/visidata/experimental/__init__.py +0 -0
  141. {visidata-3.1 → visidata-3.2}/visidata/experimental/diff_sheet.py +0 -0
  142. {visidata-3.1 → visidata-3.2}/visidata/experimental/digit_autoedit.py +0 -0
  143. {visidata-3.1 → visidata-3.2}/visidata/experimental/gdrive.py +0 -0
  144. {visidata-3.1 → visidata-3.2}/visidata/experimental/google.py +0 -0
  145. {visidata-3.1 → visidata-3.2}/visidata/experimental/gsheets.py +0 -0
  146. {visidata-3.1 → visidata-3.2}/visidata/experimental/live_search.py +0 -0
  147. {visidata-3.1 → visidata-3.2}/visidata/experimental/liveupdate.py +0 -0
  148. {visidata-3.1 → visidata-3.2}/visidata/experimental/mark.py +0 -0
  149. {visidata-3.1 → visidata-3.2}/visidata/experimental/noahs_tapestry/__init__.py +0 -0
  150. {visidata-3.1 → visidata-3.2}/visidata/experimental/noahs_tapestry/clues.json +0 -0
  151. {visidata-3.1 → visidata-3.2}/visidata/experimental/noahs_tapestry/flame.ddw +0 -0
  152. {visidata-3.1 → visidata-3.2}/visidata/experimental/noahs_tapestry/menorah.ddw +0 -0
  153. {visidata-3.1 → visidata-3.2}/visidata/experimental/noahs_tapestry/noahs.sqlite +0 -0
  154. {visidata-3.1 → visidata-3.2}/visidata/experimental/noahs_tapestry/puzzle0.md +0 -0
  155. {visidata-3.1 → visidata-3.2}/visidata/experimental/noahs_tapestry/puzzle1.md +0 -0
  156. {visidata-3.1 → visidata-3.2}/visidata/experimental/noahs_tapestry/puzzle2.md +0 -0
  157. {visidata-3.1 → visidata-3.2}/visidata/experimental/noahs_tapestry/puzzle3.md +0 -0
  158. {visidata-3.1 → visidata-3.2}/visidata/experimental/noahs_tapestry/puzzle4.md +0 -0
  159. {visidata-3.1 → visidata-3.2}/visidata/experimental/noahs_tapestry/puzzle5.md +0 -0
  160. {visidata-3.1 → visidata-3.2}/visidata/experimental/noahs_tapestry/puzzle6.md +0 -0
  161. {visidata-3.1 → visidata-3.2}/visidata/experimental/noahs_tapestry/puzzle7.md +0 -0
  162. {visidata-3.1 → visidata-3.2}/visidata/experimental/noahs_tapestry/puzzle8.md +0 -0
  163. {visidata-3.1 → visidata-3.2}/visidata/experimental/noahs_tapestry/solutions.json +0 -0
  164. {visidata-3.1 → visidata-3.2}/visidata/experimental/noahs_tapestry/tapestry.ddw +0 -0
  165. {visidata-3.1 → visidata-3.2}/visidata/experimental/noahs_tapestry/tapestry.py +0 -0
  166. {visidata-3.1 → visidata-3.2}/visidata/experimental/rownum.py +0 -0
  167. {visidata-3.1 → visidata-3.2}/visidata/experimental/slide_cells.py +0 -0
  168. {visidata-3.1 → visidata-3.2}/visidata/experimental/sort_selected.py +0 -0
  169. {visidata-3.1 → visidata-3.2}/visidata/features/__init__.py +0 -0
  170. {visidata-3.1 → visidata-3.2}/visidata/features/addcol_audiometadata.py +0 -0
  171. {visidata-3.1 → visidata-3.2}/visidata/features/addcol_histogram.py +0 -0
  172. {visidata-3.1 → visidata-3.2}/visidata/features/canvas_save_svg.py +0 -0
  173. {visidata-3.1 → visidata-3.2}/visidata/features/change_precision.py +0 -0
  174. {visidata-3.1 → visidata-3.2}/visidata/features/colorbrewer.py +0 -0
  175. {visidata-3.1 → visidata-3.2}/visidata/features/colorsheet.py +0 -0
  176. {visidata-3.1 → visidata-3.2}/visidata/features/command_server.py +0 -0
  177. {visidata-3.1 → visidata-3.2}/visidata/features/currency_to_usd.py +0 -0
  178. {visidata-3.1 → visidata-3.2}/visidata/features/customdate.py +0 -0
  179. {visidata-3.1 → visidata-3.2}/visidata/features/dedupe.py +0 -0
  180. {visidata-3.1 → visidata-3.2}/visidata/features/fill.py +0 -0
  181. {visidata-3.1 → visidata-3.2}/visidata/features/graph_seaborn.py +0 -0
  182. {visidata-3.1 → visidata-3.2}/visidata/features/hint_types.py +0 -0
  183. {visidata-3.1 → visidata-3.2}/visidata/features/known_cols.py +0 -0
  184. {visidata-3.1 → visidata-3.2}/visidata/features/normcol.py +0 -0
  185. {visidata-3.1 → visidata-3.2}/visidata/features/open_config.py +0 -0
  186. {visidata-3.1 → visidata-3.2}/visidata/features/open_syspaste.py +0 -0
  187. {visidata-3.1 → visidata-3.2}/visidata/features/ping.py +0 -0
  188. {visidata-3.1 → visidata-3.2}/visidata/features/procmgr.py +0 -0
  189. {visidata-3.1 → visidata-3.2}/visidata/features/random_sample.py +0 -0
  190. {visidata-3.1 → visidata-3.2}/visidata/features/regex.py +0 -0
  191. {visidata-3.1 → visidata-3.2}/visidata/features/rename_col_cascade.py +0 -0
  192. {visidata-3.1 → visidata-3.2}/visidata/features/repeat.py +0 -0
  193. {visidata-3.1 → visidata-3.2}/visidata/features/scroll_context.py +0 -0
  194. {visidata-3.1 → visidata-3.2}/visidata/features/select_equal_selected.py +0 -0
  195. {visidata-3.1 → visidata-3.2}/visidata/features/setcol_fake.py +0 -0
  196. {visidata-3.1 → visidata-3.2}/visidata/features/slide.py +0 -0
  197. {visidata-3.1 → visidata-3.2}/visidata/features/sparkline.py +0 -0
  198. {visidata-3.1 → visidata-3.2}/visidata/features/status_source.py +0 -0
  199. {visidata-3.1 → visidata-3.2}/visidata/features/sysopen_mailcap.py +0 -0
  200. {visidata-3.1 → visidata-3.2}/visidata/features/term_extras.py +0 -0
  201. {visidata-3.1 → visidata-3.2}/visidata/features/type_ipaddr.py +0 -0
  202. {visidata-3.1 → visidata-3.2}/visidata/features/type_url.py +0 -0
  203. {visidata-3.1 → visidata-3.2}/visidata/features/unfurl.py +0 -0
  204. {visidata-3.1 → visidata-3.2}/visidata/guide.py +0 -0
  205. {visidata-3.1 → visidata-3.2}/visidata/guides/ClipboardGuide.md +0 -0
  206. {visidata-3.1 → visidata-3.2}/visidata/guides/ColumnsGuide.md +0 -0
  207. {visidata-3.1 → visidata-3.2}/visidata/guides/CommandsSheet.md +0 -0
  208. {visidata-3.1 → visidata-3.2}/visidata/guides/DirSheet.md +0 -0
  209. {visidata-3.1 → visidata-3.2}/visidata/guides/ErrorsSheet.md +0 -0
  210. {visidata-3.1 → visidata-3.2}/visidata/guides/FrequencyTable.md +0 -0
  211. {visidata-3.1 → visidata-3.2}/visidata/guides/GrepSheet.md +0 -0
  212. {visidata-3.1 → visidata-3.2}/visidata/guides/JsonSheet.md +0 -0
  213. {visidata-3.1 → visidata-3.2}/visidata/guides/MeltGuide.md +0 -0
  214. {visidata-3.1 → visidata-3.2}/visidata/guides/MemorySheet.md +0 -0
  215. {visidata-3.1 → visidata-3.2}/visidata/guides/MenuGuide.md +0 -0
  216. {visidata-3.1 → visidata-3.2}/visidata/guides/ModifyGuide.md +0 -0
  217. {visidata-3.1 → visidata-3.2}/visidata/guides/PivotGuide.md +0 -0
  218. {visidata-3.1 → visidata-3.2}/visidata/guides/RegexGuide.md +0 -0
  219. {visidata-3.1 → visidata-3.2}/visidata/guides/SelectionGuide.md +0 -0
  220. {visidata-3.1 → visidata-3.2}/visidata/guides/SlideGuide.md +0 -0
  221. {visidata-3.1 → visidata-3.2}/visidata/guides/SplitpaneGuide.md +0 -0
  222. {visidata-3.1 → visidata-3.2}/visidata/guides/XsvGuide.md +0 -0
  223. {visidata-3.1 → visidata-3.2}/visidata/hint.py +0 -0
  224. {visidata-3.1 → visidata-3.2}/visidata/input_history.py +0 -0
  225. {visidata-3.1 → visidata-3.2}/visidata/interface.py +0 -0
  226. {visidata-3.1 → visidata-3.2}/visidata/keys.py +0 -0
  227. {visidata-3.1 → visidata-3.2}/visidata/loaders/__init__.py +0 -0
  228. {visidata-3.1 → visidata-3.2}/visidata/loaders/api_airtable.py +0 -0
  229. {visidata-3.1 → visidata-3.2}/visidata/loaders/api_matrix.py +0 -0
  230. {visidata-3.1 → visidata-3.2}/visidata/loaders/api_reddit.py +0 -0
  231. {visidata-3.1 → visidata-3.2}/visidata/loaders/api_zulip.py +0 -0
  232. {visidata-3.1 → visidata-3.2}/visidata/loaders/arrow.py +0 -0
  233. {visidata-3.1 → visidata-3.2}/visidata/loaders/conll.py +0 -0
  234. {visidata-3.1 → visidata-3.2}/visidata/loaders/frictionless.py +0 -0
  235. {visidata-3.1 → visidata-3.2}/visidata/loaders/geojson.py +0 -0
  236. {visidata-3.1 → visidata-3.2}/visidata/loaders/google.py +0 -0
  237. {visidata-3.1 → visidata-3.2}/visidata/loaders/graphviz.py +0 -0
  238. {visidata-3.1 → visidata-3.2}/visidata/loaders/grep.py +0 -0
  239. {visidata-3.1 → visidata-3.2}/visidata/loaders/html.py +0 -0
  240. {visidata-3.1 → visidata-3.2}/visidata/loaders/http.py +0 -0
  241. {visidata-3.1 → visidata-3.2}/visidata/loaders/imap.py +0 -0
  242. {visidata-3.1 → visidata-3.2}/visidata/loaders/jrnl.py +0 -0
  243. {visidata-3.1 → visidata-3.2}/visidata/loaders/json.py +0 -0
  244. {visidata-3.1 → visidata-3.2}/visidata/loaders/jsonla.py +0 -0
  245. {visidata-3.1 → visidata-3.2}/visidata/loaders/lsv.py +0 -0
  246. {visidata-3.1 → visidata-3.2}/visidata/loaders/mailbox.py +0 -0
  247. {visidata-3.1 → visidata-3.2}/visidata/loaders/markdown.py +0 -0
  248. {visidata-3.1 → visidata-3.2}/visidata/loaders/mbtiles.py +0 -0
  249. {visidata-3.1 → visidata-3.2}/visidata/loaders/msgpack.py +0 -0
  250. {visidata-3.1 → visidata-3.2}/visidata/loaders/mysql.py +0 -0
  251. {visidata-3.1 → visidata-3.2}/visidata/loaders/odf.py +0 -0
  252. {visidata-3.1 → visidata-3.2}/visidata/loaders/parquet.py +0 -0
  253. {visidata-3.1 → visidata-3.2}/visidata/loaders/pcap.py +0 -0
  254. {visidata-3.1 → visidata-3.2}/visidata/loaders/pdf.py +0 -0
  255. {visidata-3.1 → visidata-3.2}/visidata/loaders/png.py +0 -0
  256. {visidata-3.1 → visidata-3.2}/visidata/loaders/postgres.py +0 -0
  257. {visidata-3.1 → visidata-3.2}/visidata/loaders/rec.py +0 -0
  258. {visidata-3.1 → visidata-3.2}/visidata/loaders/s3.py +0 -0
  259. {visidata-3.1 → visidata-3.2}/visidata/loaders/sas.py +0 -0
  260. {visidata-3.1 → visidata-3.2}/visidata/loaders/scrape.py +0 -0
  261. {visidata-3.1 → visidata-3.2}/visidata/loaders/shp.py +0 -0
  262. {visidata-3.1 → visidata-3.2}/visidata/loaders/spss.py +0 -0
  263. {visidata-3.1 → visidata-3.2}/visidata/loaders/texttables.py +0 -0
  264. {visidata-3.1 → visidata-3.2}/visidata/loaders/toml.py +0 -0
  265. {visidata-3.1 → visidata-3.2}/visidata/loaders/tsv.py +0 -0
  266. {visidata-3.1 → visidata-3.2}/visidata/loaders/ttf.py +0 -0
  267. {visidata-3.1 → visidata-3.2}/visidata/loaders/unzip_http.py +0 -0
  268. {visidata-3.1 → visidata-3.2}/visidata/loaders/usv.py +0 -0
  269. {visidata-3.1 → visidata-3.2}/visidata/loaders/vcf.py +0 -0
  270. {visidata-3.1 → visidata-3.2}/visidata/loaders/vdx.py +0 -0
  271. {visidata-3.1 → visidata-3.2}/visidata/loaders/xlsb.py +0 -0
  272. {visidata-3.1 → visidata-3.2}/visidata/loaders/xlsx.py +0 -0
  273. {visidata-3.1 → visidata-3.2}/visidata/loaders/xml.py +0 -0
  274. {visidata-3.1 → visidata-3.2}/visidata/loaders/xword.py +0 -0
  275. {visidata-3.1 → visidata-3.2}/visidata/loaders/yaml.py +0 -0
  276. {visidata-3.1 → visidata-3.2}/visidata/macos.py +0 -0
  277. {visidata-3.1 → visidata-3.2}/visidata/memory.py +0 -0
  278. {visidata-3.1 → visidata-3.2}/visidata/modify.py +0 -0
  279. {visidata-3.1 → visidata-3.2}/visidata/motd.py +0 -0
  280. {visidata-3.1 → visidata-3.2}/visidata/movement.py +0 -0
  281. {visidata-3.1 → visidata-3.2}/visidata/optionssheet.py +0 -0
  282. {visidata-3.1 → visidata-3.2}/visidata/path.py +0 -0
  283. {visidata-3.1 → visidata-3.2}/visidata/pivot.py +0 -0
  284. {visidata-3.1 → visidata-3.2}/visidata/plugins.py +0 -0
  285. {visidata-3.1 → visidata-3.2}/visidata/rename_col.py +0 -0
  286. {visidata-3.1 → visidata-3.2}/visidata/search.py +0 -0
  287. {visidata-3.1 → visidata-3.2}/visidata/stored_list.py +0 -0
  288. {visidata-3.1 → visidata-3.2}/visidata/tests/__init__.py +0 -0
  289. {visidata-3.1 → visidata-3.2}/visidata/tests/benchmark.csv +0 -0
  290. {visidata-3.1 → visidata-3.2}/visidata/tests/conftest.py +0 -0
  291. {visidata-3.1 → visidata-3.2}/visidata/tests/sample.tsv +0 -0
  292. {visidata-3.1 → visidata-3.2}/visidata/tests/test_completer.py +0 -0
  293. {visidata-3.1 → visidata-3.2}/visidata/tests/test_date.py +0 -0
  294. {visidata-3.1 → visidata-3.2}/visidata/tests/test_edittext.py +0 -0
  295. {visidata-3.1 → visidata-3.2}/visidata/tests/test_features.py +0 -0
  296. {visidata-3.1 → visidata-3.2}/visidata/tests/test_path.py +0 -0
  297. {visidata-3.1 → visidata-3.2}/visidata/text_source.py +0 -0
  298. {visidata-3.1 → visidata-3.2}/visidata/theme.py +0 -0
  299. {visidata-3.1 → visidata-3.2}/visidata/themes/__init__.py +0 -0
  300. {visidata-3.1 → visidata-3.2}/visidata/themes/asciimono.py +0 -0
  301. {visidata-3.1 → visidata-3.2}/visidata/tuiwin.py +0 -0
  302. {visidata-3.1 → visidata-3.2}/visidata/type_currency.py +0 -0
  303. {visidata-3.1 → visidata-3.2}/visidata/type_date.py +0 -0
  304. {visidata-3.1 → visidata-3.2}/visidata/type_floatsi.py +0 -0
  305. {visidata-3.1 → visidata-3.2}/visidata/utils.py +0 -0
  306. {visidata-3.1 → visidata-3.2}/visidata/vdobj.py +0 -0
  307. {visidata-3.1 → visidata-3.2}/visidata/vendor/appdirs.py +0 -0
  308. {visidata-3.1 → visidata-3.2}/visidata/windows.py +0 -0
  309. {visidata-3.1 → visidata-3.2}/visidata/wrappers.py +0 -0
  310. {visidata-3.1 → visidata-3.2}/visidata.egg-info/dependency_links.txt +0 -0
  311. {visidata-3.1 → visidata-3.2}/visidata.egg-info/top_level.txt +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: visidata
3
- Version: 3.1
3
+ Version: 3.2
4
4
  Summary: terminal interface for exploring and arranging tabular data
5
5
  Home-page: https://visidata.org
6
- Download-URL: https://github.com/saulpw/visidata/tarball/3.1
6
+ Download-URL: https://github.com/saulpw/visidata/tarball/3.2
7
7
  Author: Saul Pwanson
8
8
  Author-email: visidata@saul.pw
9
9
  License: GPLv3
@@ -25,9 +25,11 @@ Classifier: Topic :: Utilities
25
25
  Requires-Python: >=3.8
26
26
  Description-Content-Type: text/markdown
27
27
  Provides-Extra: test
28
+ Provides-Extra: windows-curses
29
+ Provides-Extra: all
28
30
  License-File: LICENSE.gpl3
29
31
 
30
- # VisiData v3.1
32
+ # VisiData v3.2
31
33
 
32
34
  [![Tests](https://github.com/saulpw/visidata/workflows/visidata-ci-build/badge.svg)](https://github.com/saulpw/visidata/actions/workflows/main.yml)
33
35
  [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/saulpw/visidata)
@@ -62,16 +64,9 @@ See [visidata.org/install](https://visidata.org/install) for detailed instructio
62
64
 
63
65
  ### Usage
64
66
 
65
- On Linux and OS/X
66
-
67
67
  $ vd <input>
68
68
  $ <command> | vd
69
69
 
70
- On Windows
71
-
72
- $ visidata <input>
73
- $ <command> | visidata
74
-
75
70
  Press `Ctrl+Q` to quit at any time.
76
71
 
77
72
  Hundreds of other commands and options are also available; see the documentation.
@@ -1,4 +1,4 @@
1
- # VisiData v3.1
1
+ # VisiData v3.2
2
2
 
3
3
  [![Tests](https://github.com/saulpw/visidata/workflows/visidata-ci-build/badge.svg)](https://github.com/saulpw/visidata/actions/workflows/main.yml)
4
4
  [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/saulpw/visidata)
@@ -33,16 +33,9 @@ See [visidata.org/install](https://visidata.org/install) for detailed instructio
33
33
 
34
34
  ### Usage
35
35
 
36
- On Linux and OS/X
37
-
38
36
  $ vd <input>
39
37
  $ <command> | vd
40
38
 
41
- On Windows
42
-
43
- $ visidata <input>
44
- $ <command> | visidata
45
-
46
39
  Press `Ctrl+Q` to quit at any time.
47
40
 
48
41
  Hundreds of other commands and options are also available; see the documentation.
@@ -1,10 +1,39 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
3
  from setuptools import setup
4
+ import platform
5
+ import sysconfig
6
+
7
+
8
+ def all_requirements():
9
+ requirements = []
10
+ with open('requirements.txt', 'r', encoding='utf-8') as f:
11
+ requirements = []
12
+ for line in f:
13
+ line = line.strip()
14
+ if (line and not line.startswith('#') and not line.startswith('-e git+https')):
15
+
16
+ # inline comments
17
+ if '#' in line:
18
+ line = line.split('#')[0].strip()
19
+
20
+ if line:
21
+ requirements.append(line)
22
+
23
+ return requirements
24
+
4
25
 
5
26
  # tox can't actually run python3 setup.py: https://github.com/tox-dev/tox/issues/96
6
27
  # from visidata import __version__
7
- __version__ = "3.1"
28
+ __version__ = "3.2"
29
+ install_requires = [
30
+ "python-dateutil",
31
+ 'importlib_resources; python_version<"3.9"',
32
+ 'standard-mailcap; python_version>="3.13"',
33
+ ]
34
+
35
+ if not sysconfig.get_platform().startswith("mingw"): # 2757
36
+ install_requires += ['windows-curses >= 2.4.1; platform_system == "Windows"'] # 2119
8
37
 
9
38
  setup(
10
39
  name="visidata",
@@ -17,17 +46,13 @@ setup(
17
46
  author_email="visidata@saul.pw",
18
47
  url="https://visidata.org",
19
48
  download_url="https://github.com/saulpw/visidata/tarball/" + __version__,
20
- scripts=["bin/vd", "bin/vd2to3.vdx"],
49
+ scripts=["bin/vd2to3.vdx"],
21
50
  entry_points={
22
- "console_scripts": ["visidata=visidata.main:vd_cli"],
51
+ "console_scripts": ["vd=visidata.main:vd_cli",
52
+ "visidata=visidata.main:vd_cli"],
23
53
  },
24
54
  py_modules=["visidata"],
25
- install_requires=[
26
- "python-dateutil",
27
- 'windows-curses != 2.3.1; platform_system == "Windows"', # 1841
28
- "importlib-metadata >= 3.6",
29
- 'importlib_resources; python_version<"3.9"',
30
- ],
55
+ install_requires=install_requires,
31
56
  packages=[
32
57
  "visidata",
33
58
  "visidata.loaders",
@@ -71,7 +96,9 @@ setup(
71
96
  "tomli",
72
97
  "wcwidth",
73
98
  "xport>=3.0",
74
- ]
99
+ ],"windows-curses": ['windows-curses >= 2.4.1; platform_system == "Windows"', # 2119
100
+ ],
101
+ "all": all_requirements(),
75
102
  },
76
103
  package_data={
77
104
  "visidata.man": ["vd.1", "vd.txt"],
@@ -1,10 +1,10 @@
1
1
  'VisiData: a curses interface for exploring and arranging tabular data'
2
2
 
3
- __version__ = '3.1'
3
+ __version__ = '3.2'
4
4
  __version_info__ = 'VisiData v' + __version__
5
5
  __author__ = 'Saul Pwanson <vd@saul.pw>'
6
6
  __status__ = 'Production/Stable'
7
- __copyright__ = 'Copyright (c) 2016-2021 ' + __author__
7
+ __copyright__ = 'Copyright (c) 2016-2024 ' + __author__
8
8
 
9
9
 
10
10
  class EscapeException(BaseException):
@@ -4,7 +4,7 @@ import curses
4
4
  import visidata
5
5
 
6
6
  from visidata import EscapeException, ExpectedException, clipdraw, Sheet, VisiData, BaseSheet
7
- from visidata import vd, colors, dispwidth, ColorAttr
7
+ from visidata import vd, colors, dispwidth, ColorAttr, clipstr_start
8
8
  from visidata import AttrDict
9
9
 
10
10
 
@@ -112,7 +112,8 @@ def delchar(s, i, remove=1):
112
112
  'Delete `remove` characters from str `s` beginning at position `i`.'
113
113
  return s if i < 0 else s[:i] + s[i+remove:]
114
114
 
115
- def find_nonword(s, a, b, incr):
115
+ def find_word(s, a, b, incr):
116
+ '''Return first index of word boundary in s[a:b], going forward if incr is +1 and backward if incr is -1.'''
116
117
  if not s: return 0
117
118
  a = min(max(a, 0), len(s)-1)
118
119
  b = min(max(b, 0), len(s)-1)
@@ -124,9 +125,9 @@ def find_nonword(s, a, b, incr):
124
125
  b += incr
125
126
  return min(max(b, -1), len(s))
126
127
  else:
127
- while not s[a].isalnum() and a < b: # first skip non-word chars
128
+ while s[a].isalnum() and a < b: # first skip word chars
128
129
  a += incr
129
- while s[a].isalnum() and a < b:
130
+ while not s[a].isalnum() and a < b: # then skip non-word chars
130
131
  a += incr
131
132
  return min(max(a, 0), len(s))
132
133
 
@@ -173,15 +174,14 @@ class InputWidget:
173
174
  self.former_i = None
174
175
  self.just_completed = False
175
176
 
176
- def editline(self, scr, y, x, w, attr=ColorAttr(), updater=lambda val: None, bindings={}, clear=True) -> str:
177
+ def editline(self, scr, y, x, w, attr=ColorAttr(), updater=lambda val:None, bindings={}, clear=True) -> str:
177
178
  'If *clear* is True, clear whole editing area before displaying.'
178
179
  with EnableCursor():
179
180
  while True:
180
- vd.drawSheet(scr, vd.activeSheet)
181
- if updater:
181
+ if len(vd.pendingKeys) <= 3: #speed up paste of long strings by skipping redraws
182
+ vd.drawSheet(scr, vd.activeSheet)
182
183
  updater(self.value)
183
-
184
- vd.drawInputHelp(scr)
184
+ vd.drawInputHelp(scr)
185
185
 
186
186
  self.draw(scr, y, x, w, attr, clear=clear)
187
187
  ch = vd.getkeystroke(scr)
@@ -194,31 +194,57 @@ class InputWidget:
194
194
 
195
195
  def draw(self, scr, y, x, w, attr=ColorAttr(), clear=True):
196
196
  i = self.current_i # the onscreen offset within the field where v[i] is displayed
197
- left_truncchar = right_truncchar = self.truncchar
197
+ trunch = self.truncchar
198
+ tr_w = dispwidth(trunch)
199
+ fill_w = dispwidth(self.fillchar)
200
+
201
+ def _calc_display(dispval, i):
202
+ '''Return a formatted substring of *dispval* that fills the on-screen width *w*.'''
203
+ if i == len(dispval): # add a fillchar so the user perceives room to type
204
+ dispval += self.fillchar
205
+ dw = dispwidth(dispval)
206
+ if dw <= w: # entire value fits
207
+ dispval += self.fillchar*(w-dw)
208
+ return dispval, i
209
+ if w <= tr_w: # column is too narrow to hold a left and right truncation
210
+ return trunch, 0
211
+
212
+ dw = dispwidth(dispval[i:])
213
+ if dw + tr_w <= w and dw <= w//2: #cursor is within half-colwidth of end
214
+ #truncate the left and show the end
215
+ frag, n = clipstr_start(dispval, w-tr_w)
216
+ offset = len(dispval) - i
217
+ dispval = ' '*(w-tr_w - n) + trunch + frag
218
+ i = len(dispval) - offset
219
+ return dispval, i
220
+
221
+ # the remaining cases need the right side truncated, after the new dispval is returned
222
+ dw = dispwidth(dispval[:i+1])
223
+ if dw + tr_w <= w and dispwidth(dispval[:i]) <= w//2: #cursor is within half-colwidth of start
224
+ #truncate the right, and show the string start
225
+ pass
226
+ else: # truncate left and right sides
227
+ # Place the cursor at the midpoint of the available colwidth
228
+ left_w = (w - 2*tr_w)//2
229
+ # calculate the fragment to the left of the cursor
230
+ l_frag, n = clipstr_start(dispval[:i], left_w)
231
+ dispval = ' '*(left_w-n) + trunch + l_frag + dispval[i:]
232
+ i = left_w-n + len(trunch) + len(l_frag)
233
+ return dispval, i
198
234
 
199
235
  if self.display:
200
236
  dispval = clean_printable(self.value)
201
237
  else:
202
238
  dispval = '*' * len(self.value)
239
+ dispval, i = _calc_display(dispval, i)
203
240
 
204
- if len(dispval) < w: # entire value fits
205
- dispval += self.fillchar*(w-len(dispval)-1)
206
- elif i == len(dispval): # cursor after value (will append)
207
- i = w-1
208
- dispval = left_truncchar + dispval[len(dispval)-w+2:] + self.fillchar
209
- elif i >= len(dispval)-w//2: # cursor within halfwidth of end
210
- i = w-(len(dispval)-i)
211
- dispval = left_truncchar + dispval[len(dispval)-w+1:]
212
- elif i <= w//2: # cursor within halfwidth of beginning
213
- dispval = dispval[:w-1] + right_truncchar
214
- else:
215
- i = w//2 # visual cursor stays right in the middle
216
- k = 1 if w%2==0 else 0 # odd widths have one character more
217
- dispval = left_truncchar + dispval[self.current_i-w//2+1:self.current_i+w//2-k] + right_truncchar
218
-
219
- prew = clipdraw(scr, y, x, dispval[:i], attr, w, clear=clear, literal=True)
220
- clipdraw(scr, y, x+prew, dispval[i:], attr, w-prew+1, clear=clear, literal=True)
241
+ #clipdraw will truncate the right side of dispval with trunch as needed
242
+ clipdraw(scr, y, x, dispval, attr, w, clear=clear, literal=True)
243
+ if x+w < scr.getmaxyx()[1]:
244
+ #draw a space to indicate that the user can scroll right of the cell's final char
245
+ clipdraw(scr, y, x+w, ' ', attr, 1, clear=False, literal=True)
221
246
  if scr:
247
+ prew = dispwidth(dispval[:i])
222
248
  scr.move(y, x+prew)
223
249
 
224
250
  def handle_key(self, ch:str, scr) -> bool:
@@ -249,17 +275,25 @@ class InputWidget:
249
275
  c = vd.prettykeys(c)
250
276
  i += len(c)
251
277
  v += c
252
- elif ch == '^O': self.value = vd.launchExternalEditor(v); return True # auto-accept after $EDITOR
278
+ elif ch == '^O':
279
+ edit_v = vd.launchExternalEditor(v)
280
+ if self.value == edit_v:
281
+ # leave cell unmodified when the editor exits with no change
282
+ raise EscapeException(ch)
283
+ else:
284
+ self.value = edit_v
285
+ return True
253
286
  elif ch == '^R': v = self.orig_value # ^Reload initial value
254
287
  elif ch == '^T': v = delchar(splice(v, i-2, v[i-1:i]), i) # swap chars
255
288
  elif ch == '^U': v = v[i:]; i = 0 # clear to beginning
256
289
  elif ch == '^V': v = splice(v, i, until_get_wch(scr)); i += 1 # literal character
257
- elif ch == '^W': j = find_nonword(v, 0, i-1, -1); v = v[:j+1] + v[i:]; i = j+1 # erase word
290
+ elif ch == '^W': j = find_word(v, 0, i-1, -1); v = v[:j+1] + v[i:]; i = j+1 # erase word
291
+ elif ch in ('KEY_DC5','kDC5','kDC3'): j = find_word(v, i, len(v), +1); v = v[:i] + v[j+1:] # erase word forward
258
292
  elif ch == '^Y': v = splice(v, i, str(vd.memory.clipval))
259
293
  elif ch == '^Z': vd.suspend()
260
294
  # CTRL+arrow
261
- elif ch == 'kLFT5': i = find_nonword(v, 0, i-1, -1)+1; # word left
262
- elif ch == 'kRIT5': i = find_nonword(v, i+1, len(v)-1, +1)+1; # word right
295
+ elif ch == 'kLFT5': i = find_word(v, 0, i-1, -1)+1; # word left
296
+ elif ch == 'kRIT5': i = find_word(v, i, len(v)-1, +1); # word right
263
297
  elif ch == 'kUP5': pass
264
298
  elif ch == 'kDN5': pass
265
299
  elif self.history and ch == 'KEY_UP': v, i = self.prev_history(v, i)
@@ -337,7 +371,7 @@ class InputWidget:
337
371
  @VisiData.api
338
372
  def editText(vd, y, x, w, attr=ColorAttr(), value='',
339
373
  help='',
340
- updater=None, bindings={},
374
+ updater=lambda val: None, bindings={},
341
375
  display=True, record=True, clear=True, **kwargs):
342
376
  'Invoke modal single-line editor at (*y*, *x*) for *w* terminal chars. Use *display* is False for sensitive input like passphrases. If *record* is True, get input from the cmdlog in batch mode, and save input to the cmdlog if *display* is also True. Return new value as string.'
343
377
  v = None
@@ -569,7 +603,7 @@ def input(vd, prompt, type=None, defaultLast=False, history=[], dy=0, attr=None,
569
603
  @VisiData.api
570
604
  def confirm(vd, prompt, exc=EscapeException):
571
605
  'Display *prompt* on status line and demand input that starts with "Y" or "y" to proceed. Raise *exc* otherwise. Return True.'
572
- if vd.options.batch and not vd.options.interactive:
606
+ if vd.options.batch:
573
607
  return vd.fail('cannot confirm in batch mode: ' + prompt)
574
608
 
575
609
  yn = vd.input(prompt, value='no', record=False)[:1]
@@ -594,7 +628,7 @@ class CompleteKey:
594
628
  @Sheet.api
595
629
  def editCell(self, vcolidx=None, rowidx=None, value=None, **kwargs):
596
630
  '''Call vd.editText for the cell at (*rowidx*, *vcolidx*). Return the new value, properly typed.
597
-
631
+ - *vcolidx*: numeric index into ``self.availCols``. When None, use current column.
598
632
  - *rowidx*: numeric index into ``self.rows``. If negative, indicates the column name in the header.
599
633
  - *value*: if given, the starting input; otherwise the starting input is the cell value or column name as appropriate.
600
634
  - *kwargs*: passthrough args to ``vd.editText``.
@@ -604,7 +638,7 @@ def editCell(self, vcolidx=None, rowidx=None, value=None, **kwargs):
604
638
  vcolidx = self.cursorVisibleColIndex
605
639
  x, w = self._visibleColLayout.get(vcolidx, (0, 0))
606
640
 
607
- col = self.visibleCols[vcolidx]
641
+ col = self.availCols[vcolidx]
608
642
  if rowidx is None:
609
643
  rowidx = self.cursorRowIndex
610
644
 
@@ -626,7 +660,7 @@ def editCell(self, vcolidx=None, rowidx=None, value=None, **kwargs):
626
660
  'KEY_BTAB': acceptThenFunc('go-left', 'rename-col' if rowidx < 0 else 'edit-cell'),
627
661
  }
628
662
 
629
- if vcolidx >= self.nVisibleCols-1:
663
+ if vcolidx == self.nVisibleCols-1 or vcolidx >= self.nCols-1:
630
664
  bindings['^I'] = acceptThenFunc('go-down', 'go-leftmost', 'edit-cell')
631
665
 
632
666
  if vcolidx <= 0:
@@ -81,6 +81,10 @@ def guess_extension(vd, path):
81
81
  def openPath(vd, p, filetype=None, create=False):
82
82
  '''Call ``open_<filetype>(p)`` or ``openurl_<p.scheme>(p, filetype)``. Return constructed but unloaded sheet of appropriate type.
83
83
  If True, *create* will return a new, blank **Sheet** if file does not exist.'''
84
+ # allow user to assign a filetype to a pathname: options.set('filetype', 'csv', '-')
85
+ filetype = filetype or vd.options.getonly('filetype', str(p), None) #1710
86
+ filetype = filetype or vd.options.getonly('filetype', 'global', None)
87
+
84
88
  if p.scheme and not p.has_fp():
85
89
  schemes = p.scheme.split('+')
86
90
  openfuncname = 'openurl_' + schemes[-1]
@@ -94,8 +98,10 @@ def openPath(vd, p, filetype=None, create=False):
94
98
  if not p.exists() and not create:
95
99
  return None
96
100
 
97
- if not filetype:
98
- filetype = p.ext or vd.options.filetype
101
+ # assign filetype from extension, but only for files, not directories
102
+ if not p.is_dir(): #2547
103
+ filetype = filetype or p.ext
104
+ filetype = filetype or vd.options.filetype
99
105
 
100
106
  filetype = filetype.lower()
101
107
 
@@ -147,15 +153,12 @@ def openSource(vd, p, filetype=None, create=False, **kwargs):
147
153
  if isinstance(p, BaseSheet):
148
154
  return p
149
155
 
150
- filetype = filetype or vd.options.getonly('filetype', str(p), '') #1710
151
- filetype = filetype or vd.options.getonly('filetype', 'global', '')
152
-
153
156
  vs = None
154
157
  if isinstance(p, str):
155
158
  if '://' in p:
156
159
  vs = vd.openPath(Path(p), filetype=filetype) # convert to Path and recurse
157
160
  elif p == '-':
158
- if sys.stdin.isatty():
161
+ if vd.stdinSource.fptext.isatty():
159
162
  vd.fail('cannot open stdin when it is a tty')
160
163
  vs = vd.openPath(vd.stdinSource, filetype=filetype)
161
164
  else:
@@ -1,7 +1,7 @@
1
1
  # VisiData uses Python native int, float, str, and adds simple anytype.
2
2
 
3
3
  import locale
4
- from visidata import options, TypedWrapper, vd, VisiData
4
+ from visidata import vd, VisiData
5
5
 
6
6
  vd.help_float_fmt = '''
7
7
  - fmt starting with `'%'` (like `%0.2f`) will use [:onclick https://docs.python.org/3.6/library/locale.html#locale.format_string]locale.format_string[/]
@@ -40,7 +40,7 @@ anytype.__name__ = ''
40
40
  @VisiData.global_api
41
41
  def numericFormatter(vd, fmtstr, typedval):
42
42
  try:
43
- fmtstr = fmtstr or options['disp_'+type(typedval).__name__+'_fmt']
43
+ fmtstr = fmtstr or vd.options['disp_'+type(typedval).__name__+'_fmt']
44
44
  if fmtstr[0] == '%':
45
45
  return locale.format_string(fmtstr, typedval, grouping=False)
46
46
  else:
@@ -3,9 +3,11 @@ import math
3
3
  import functools
4
4
  import collections
5
5
  import statistics
6
+ from copy import copy
7
+ import itertools
6
8
 
7
- from visidata import Progress, Sheet, Column, ColumnsSheet, VisiData
8
- from visidata import vd, anytype, vlen, asyncthread, wrapply, AttrDict, date, INPROGRESS
9
+ from visidata import Progress, Sheet, Column, ColumnsSheet, VisiData, SettableColumn
10
+ from visidata import vd, anytype, vlen, asyncthread, wrapply, AttrDict, date, INPROGRESS, dispwidth, stacktrace, TypedExceptionWrapper
9
11
 
10
12
  vd.help_aggregators = '''# Choose Aggregators
11
13
  Start typing an aggregator name or description.
@@ -76,7 +78,7 @@ Column.aggregators = property(aggregators_get, aggregators_set)
76
78
 
77
79
 
78
80
  class Aggregator:
79
- def __init__(self, name, type, funcValues=None, helpstr='foo'):
81
+ def __init__(self, name, type, funcValues=None, helpstr=''):
80
82
  'Define aggregator `name` that calls funcValues(values)'
81
83
  self.type = type
82
84
  self.funcValues = funcValues # funcValues(values)
@@ -92,6 +94,33 @@ class Aggregator:
92
94
  return None
93
95
  raise e
94
96
 
97
+ class ListAggregator(Aggregator):
98
+ '''A list aggregator is an aggregator that returns a list of values, generally
99
+ one value per input row, unlike ordinary aggregators that operate on rows
100
+ and return only a single value.
101
+ To implement a new list aggregator, subclass ListAggregator,
102
+ and override aggregate() and aggregate_list().'''
103
+ def __init__(self, name, type, helpstr='', listtype=None):
104
+ '''*listtype* determines the type of the column created by addcol_aggregate()
105
+ for list aggrs. If it is None, then the new column will match the type of the input column'''
106
+ super().__init__(name, type, helpstr=helpstr)
107
+ self.listtype = listtype
108
+
109
+ def aggregate(self, col, rows) -> list:
110
+ '''Return a list, which can be shorter than *rows*, because it filters out nulls and errors.
111
+ Override in subclass.'''
112
+ vals = self.aggregate_list(col, rows)
113
+ # filter out nulls and errors
114
+ vals = [ v for v in vals if not col.sheet.isNullFunc()(v) ]
115
+ return vals
116
+
117
+ def aggregate_list(self, col, row_group) -> list:
118
+ '''Return a list of results, which will be one result per input row.
119
+ *row_group* is an iterable that holds a "group" of rows to run the aggregator on.
120
+ rows in *row_group* are not necessarily in the same order they are in the sheet.
121
+ Override in subclass.'''
122
+ vals = [ col.getTypedValue(r) for r in row_group ]
123
+ return vals
95
124
 
96
125
  @VisiData.api
97
126
  def aggregator(vd, name, funcValues, helpstr='', *, type=None):
@@ -99,6 +128,14 @@ def aggregator(vd, name, funcValues, helpstr='', *, type=None):
99
128
  Use *type* to force type of aggregated column (default to use type of source column).'''
100
129
  vd.aggregators[name] = Aggregator(name, type, funcValues=funcValues, helpstr=helpstr)
101
130
 
131
+ @VisiData.api
132
+ def aggregator_list(vd, name, helpstr='', type=anytype, listtype=anytype):
133
+ '''Define simple aggregator *name* that calls ``funcValues(values)`` to aggregate *values*.
134
+ Use *type* to force type of aggregated column (default to use type of source column).
135
+ Use *listtype* to force the type of the new column created by addcol-aggregate.
136
+ If *listtype* is None, it will match the type of the source column.'''
137
+ vd.aggregators[name] = ListAggregator(name, type, helpstr=helpstr, listtype=listtype)
138
+
102
139
  ## specific aggregator implementations
103
140
 
104
141
  def mean(vals):
@@ -109,6 +146,16 @@ def mean(vals):
109
146
  def vsum(vals):
110
147
  return sum(vals, start=type(vals[0] if len(vals) else 0)()) #1996
111
148
 
149
+ def stdev(vals):
150
+ # because statistics.stdev can raise an exception, we put it in a wrapper.
151
+ # The wrapper lets the exception be seen as an error string in the stdev
152
+ # aggregator, shown at the bottom of the sheet as part of allAggregators.
153
+ try:
154
+ return statistics.stdev(vals)
155
+ except statistics.StatisticsError as e: #when vals holds only 1 element
156
+ e.stacktrace = stacktrace()
157
+ return TypedExceptionWrapper(None, exception=e)
158
+
112
159
  # http://code.activestate.com/recipes/511478-finding-the-percentile-of-the-values/
113
160
  def _percentile(N, percent, key=lambda x:x):
114
161
  """
@@ -140,10 +187,49 @@ class PercentileAggregator(Aggregator):
140
187
  def aggregate(self, col, rows):
141
188
  return _percentile(sorted(col.getValues(rows)), self.pct/100, key=float)
142
189
 
143
-
144
190
  def quantiles(q, helpstr):
145
191
  return [PercentileAggregator(round(100*i/q), helpstr) for i in range(1, q)]
146
192
 
193
+ def aggregate_groups(sheet, col, rows, aggr) -> list:
194
+ '''Returns a list, containing the result of the aggregator applied to each row.
195
+ *col* is a column whose values determine each row's rank within a group.
196
+ *rows* is a list of visidata rows.
197
+ *aggr* is an Aggregator object.
198
+ Rows are grouped by their key columns. Null key column cells are considered equal,
199
+ so nulls are grouped together. Cells with exceptions do not group together.
200
+ Each exception cell is grouped by itself, with only one row in the group.
201
+ '''
202
+ def _key_progress(prog):
203
+ def identity(val):
204
+ prog.addProgress(1)
205
+ return val
206
+ return identity
207
+
208
+ with Progress(gerund='ranking', total=4*sheet.nRows) as prog:
209
+ p = _key_progress(prog) # increment progress every time p() is called
210
+ # compile row data, for each row a list of tuples: (group_key, rank_key, rownum)
211
+ rowdata = [(sheet.rowkey(r), col.getTypedValue(r), p(rownum)) for rownum, r in enumerate(rows)]
212
+ # sort by row key and column value to prepare for grouping
213
+ try:
214
+ rowdata.sort(key=p)
215
+ except TypeError as e:
216
+ vd.fail(f'elements in a ranking column must be comparable: {e.args[0]}')
217
+ rowvals = []
218
+ #group by row key
219
+ for _, group in itertools.groupby(rowdata, key=lambda v: v[0]):
220
+ # within a group, the rows have already been sorted by col_val
221
+ group = list(group)
222
+ if isinstance(aggr, ListAggregator): # for list aggregators, each row gets its own value
223
+ aggr_vals = aggr.aggregate_list(col, [rows[rownum] for _, _, rownum in group])
224
+ rowvals += [(rownum, v) for (_, _, rownum), v in zip(group, aggr_vals)]
225
+ else: # for normal aggregators, each row in the group gets the same value
226
+ aggr_val = aggr.aggregate(col, [rows[rownum] for _, _, rownum in group])
227
+ rowvals += [(rownum, aggr_val) for _, _, rownum in group]
228
+ prog.addProgress(len(group))
229
+ # sort by unique rownum, to make rank results match the original row order
230
+ rowvals.sort(key=p)
231
+ rowvals = [ v for rownum, v in rowvals ]
232
+ return rowvals
147
233
 
148
234
  vd.aggregator('min', min, 'minimum value')
149
235
  vd.aggregator('max', max, 'maximum value')
@@ -154,8 +240,8 @@ vd.aggregator('mode', statistics.mode, 'mode of values')
154
240
  vd.aggregator('sum', vsum, 'sum of values')
155
241
  vd.aggregator('distinct', set, 'distinct values', type=vlen)
156
242
  vd.aggregator('count', lambda values: sum(1 for v in values), 'number of values', type=int)
157
- vd.aggregator('list', list, 'list of values', type=anytype)
158
- vd.aggregator('stdev', statistics.stdev, 'standard deviation of values', type=float)
243
+ vd.aggregator_list('list', 'list of values', type=anytype, listtype=None)
244
+ vd.aggregator('stdev', stdev, 'standard deviation of values', type=float)
159
245
 
160
246
  vd.aggregators['q3'] = quantiles(3, 'tertiles (33/66th pctile)')
161
247
  vd.aggregators['q4'] = quantiles(4, 'quartiles (25/50/75th pctile)')
@@ -205,10 +291,9 @@ def addAggregators(sheet, cols, aggrnames):
205
291
  for aggrname in aggrnames:
206
292
  aggrs = vd.aggregators.get(aggrname)
207
293
  aggrs = aggrs if isinstance(aggrs, list) else [aggrs]
208
- for aggr in aggrs:
209
- for c in cols:
210
- if not hasattr(c, 'aggregators'):
211
- c.aggregators = []
294
+ for c in cols:
295
+ vd.addUndo(setattr, c, 'aggregators', copy(c.aggregators))
296
+ for aggr in aggrs:
212
297
  if aggr and aggr not in c.aggregators:
213
298
  c.aggregators += [aggr]
214
299
 
@@ -243,7 +328,8 @@ def memo_aggregate(col, agg_choices, rows):
243
328
  for agg in aggs:
244
329
  aggval = agg.aggregate(col, rows)
245
330
  typedval = wrapply(agg.type or col.type, aggval)
246
- dispval = col.format(typedval)
331
+ # limit width to limit formatting time when typedval is a long list
332
+ dispval = col.format(typedval, width=1000)
247
333
  k = col.name+'_'+agg.name
248
334
  vd.status(f'{k}={dispval}')
249
335
  vd.memory[k] = typedval
@@ -254,17 +340,16 @@ def aggregator_choices(vd):
254
340
  return [
255
341
  AttrDict(key=agg, desc=v[0].helpstr if isinstance(v, list) else v.helpstr)
256
342
  for agg, v in vd.aggregators.items()
257
- if not agg.startswith('p') # skip all the percentiles, user should use q# instead
343
+ if not (agg.startswith('p') and agg[1:].isdigit()) # skip all the percentiles like 'p10', user should use q# instead
258
344
  ]
259
345
 
260
346
 
261
347
  @VisiData.api
262
- def chooseAggregators(vd):
348
+ def chooseAggregators(vd, prompt = 'choose aggregators: '):
263
349
  '''Return a list of aggregator name strings chosen or entered by the user. User-entered names may be invalid.'''
264
- prompt = 'choose aggregators: '
265
350
  def _fmt_aggr_summary(match, row, trigger_key):
266
351
  formatted_aggrname = match.formatted.get('key', row.key) if match else row.key
267
- r = ' '*(len(prompt)-3)
352
+ r = ' '*(dispwidth(prompt)-3)
268
353
  r += f'[:keystrokes]{trigger_key}[/] '
269
354
  r += formatted_aggrname
270
355
  if row.desc:
@@ -288,10 +373,34 @@ def chooseAggregators(vd):
288
373
  vd.warning(f'aggregator does not exist: {aggr}')
289
374
  return aggrs
290
375
 
291
- Sheet.addCommand('+', 'aggregate-col', 'addAggregators([cursorCol], chooseAggregators())', 'add aggregator to current column')
376
+ @Sheet.api
377
+ @asyncthread
378
+ def addcol_aggregate(sheet, col, aggrnames):
379
+ for aggrname in aggrnames:
380
+ aggrs = vd.aggregators.get(aggrname)
381
+ aggrs = aggrs if isinstance(aggrs, list) else [aggrs]
382
+ if not aggrs: continue
383
+ for aggr in aggrs:
384
+ rows = aggregate_groups(sheet, col, sheet.rows, aggr)
385
+ if isinstance(aggr, ListAggregator):
386
+ t = aggr.listtype or col.type
387
+ else:
388
+ t = aggr.type or col.type
389
+ c = SettableColumn(name=f'{col.name}_{aggr.name}', type=t)
390
+ sheet.addColumnAtCursor(c)
391
+ c.setValues(sheet.rows, *rows)
392
+
393
+ Sheet.addCommand('+', 'aggregate-col', 'addAggregators([cursorCol], chooseAggregators())', 'Add aggregator to current column')
292
394
  Sheet.addCommand('z+', 'memo-aggregate', 'cursorCol.memo_aggregate(chooseAggregators(), selectedRows or rows)', 'memo result of aggregator over values in selected rows for current column')
293
395
  ColumnsSheet.addCommand('g+', 'aggregate-cols', 'addAggregators(selectedRows or source[0].nonKeyVisibleCols, chooseAggregators())', 'add aggregators to selected source columns')
396
+ Sheet.addCommand('', 'addcol-aggregate', 'addcol_aggregate(cursorCol, chooseAggregators(prompt="aggregator for groups: "))', 'add column(s) with aggregator of rows grouped by key columns')
397
+
398
+ vd.addGlobals(
399
+ ListAggregator=ListAggregator
400
+ )
294
401
 
295
402
  vd.addMenuItems('''
296
403
  Column > Add aggregator > aggregate-col
404
+ Column > Add column > aggregate > addcol-aggregate
297
405
  ''')
406
+