visidata 2.11.1__py3-none-any.whl → 3.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. visidata/__init__.py +72 -91
  2. visidata/_input.py +259 -42
  3. visidata/_open.py +84 -29
  4. visidata/_types.py +21 -3
  5. visidata/_urlcache.py +17 -4
  6. visidata/aggregators.py +78 -25
  7. visidata/apps/__init__.py +0 -0
  8. visidata/apps/vdsql/__about__.py +8 -0
  9. visidata/apps/vdsql/__init__.py +5 -0
  10. visidata/apps/vdsql/__main__.py +27 -0
  11. visidata/apps/vdsql/_ibis.py +748 -0
  12. visidata/apps/vdsql/bigquery.py +61 -0
  13. visidata/apps/vdsql/clickhouse.py +53 -0
  14. visidata/apps/vdsql/setup.py +40 -0
  15. visidata/apps/vdsql/snowflake.py +67 -0
  16. visidata/apps/vgit/__init__.py +13 -0
  17. {vgit → visidata/apps/vgit}/blame.py +5 -2
  18. {vgit → visidata/apps/vgit}/branch.py +31 -16
  19. {vgit → visidata/apps/vgit}/config.py +3 -3
  20. visidata/apps/vgit/diff.py +169 -0
  21. visidata/apps/vgit/gitsheet.py +161 -0
  22. {vgit → visidata/apps/vgit}/grep.py +6 -5
  23. visidata/apps/vgit/log.py +81 -0
  24. {vgit → visidata/apps/vgit}/main.py +18 -5
  25. {vgit → visidata/apps/vgit}/remote.py +8 -4
  26. visidata/apps/vgit/repos.py +71 -0
  27. {vgit → visidata/apps/vgit}/setup.py +6 -4
  28. visidata/apps/vgit/stash.py +69 -0
  29. visidata/apps/vgit/status.py +204 -0
  30. {vgit → visidata/apps/vgit}/statusbar.py +2 -0
  31. visidata/basesheet.py +63 -51
  32. visidata/canvas.py +208 -93
  33. visidata/choose.py +6 -6
  34. visidata/clean_names.py +29 -0
  35. visidata/clipboard.py +73 -17
  36. visidata/cliptext.py +220 -46
  37. visidata/cmdlog.py +88 -114
  38. visidata/color.py +142 -56
  39. visidata/column.py +121 -129
  40. visidata/ddw/input.ddw +74 -79
  41. visidata/ddw/regex.ddw +57 -0
  42. visidata/ddwplay.py +33 -14
  43. visidata/deprecated.py +77 -3
  44. visidata/desktop/visidata.desktop +7 -0
  45. visidata/editor.py +12 -6
  46. visidata/errors.py +6 -2
  47. visidata/experimental/__init__.py +0 -0
  48. visidata/experimental/diff_sheet.py +29 -0
  49. visidata/experimental/digit_autoedit.py +6 -0
  50. visidata/experimental/gdrive.py +89 -0
  51. visidata/experimental/google.py +37 -0
  52. visidata/experimental/gsheets.py +79 -0
  53. visidata/experimental/live_search.py +37 -0
  54. visidata/experimental/liveupdate.py +45 -0
  55. visidata/experimental/mark.py +133 -0
  56. visidata/experimental/noahs_tapestry/__init__.py +1 -0
  57. visidata/experimental/noahs_tapestry/tapestry.py +147 -0
  58. visidata/experimental/rownum.py +73 -0
  59. visidata/experimental/slide_cells.py +26 -0
  60. visidata/expr.py +8 -4
  61. visidata/extensible.py +22 -4
  62. visidata/features/__init__.py +0 -0
  63. visidata/features/addcol_audiometadata.py +42 -0
  64. visidata/features/addcol_histogram.py +34 -0
  65. visidata/features/canvas_save_svg.py +69 -0
  66. visidata/features/change_precision.py +46 -0
  67. visidata/features/cmdpalette.py +197 -0
  68. visidata/features/colorbrewer.py +363 -0
  69. visidata/{colorsheet.py → features/colorsheet.py} +17 -16
  70. visidata/features/command_server.py +105 -0
  71. visidata/features/currency_to_usd.py +70 -0
  72. visidata/{customdate.py → features/customdate.py} +2 -0
  73. visidata/features/dedupe.py +132 -0
  74. visidata/{describe.py → features/describe.py} +17 -15
  75. visidata/features/errors_guide.py +26 -0
  76. visidata/features/expand_cols.py +202 -0
  77. visidata/{fill.py → features/fill.py} +3 -1
  78. visidata/{freeze.py → features/freeze.py} +11 -6
  79. visidata/features/graph_seaborn.py +79 -0
  80. visidata/features/helloworld.py +10 -0
  81. visidata/features/hint_types.py +17 -0
  82. visidata/{incr.py → features/incr.py} +5 -0
  83. visidata/{join.py → features/join.py} +107 -53
  84. visidata/features/known_cols.py +21 -0
  85. visidata/features/layout.py +62 -0
  86. visidata/{melt.py → features/melt.py} +32 -21
  87. visidata/features/normcol.py +118 -0
  88. visidata/features/open_config.py +7 -0
  89. visidata/features/open_syspaste.py +18 -0
  90. visidata/features/ping.py +157 -0
  91. visidata/features/procmgr.py +208 -0
  92. visidata/features/random_sample.py +6 -0
  93. visidata/{regex.py → features/regex.py} +47 -31
  94. visidata/features/reload_every.py +55 -0
  95. visidata/features/rename_col_cascade.py +30 -0
  96. visidata/features/scroll_context.py +60 -0
  97. visidata/features/select_equal_selected.py +11 -0
  98. visidata/features/setcol_fake.py +65 -0
  99. visidata/{slide.py → features/slide.py} +77 -21
  100. visidata/features/sparkline.py +48 -0
  101. visidata/features/status_source.py +20 -0
  102. visidata/{sysedit.py → features/sysedit.py} +2 -1
  103. visidata/features/sysopen_mailcap.py +46 -0
  104. visidata/features/term_extras.py +13 -0
  105. visidata/{transpose.py → features/transpose.py} +5 -4
  106. visidata/features/type_ipaddr.py +73 -0
  107. visidata/features/type_url.py +11 -0
  108. visidata/{unfurl.py → features/unfurl.py} +9 -9
  109. visidata/{window.py → features/window.py} +2 -2
  110. visidata/form.py +50 -21
  111. visidata/freqtbl.py +81 -33
  112. visidata/fuzzymatch.py +414 -0
  113. visidata/graph.py +105 -33
  114. visidata/guide.py +200 -0
  115. visidata/help.py +75 -44
  116. visidata/hint.py +39 -0
  117. visidata/indexsheet.py +109 -0
  118. visidata/input_history.py +55 -0
  119. visidata/interface.py +58 -0
  120. visidata/keys.py +20 -16
  121. visidata/loaders/__init__.py +9 -0
  122. visidata/loaders/_pandas.py +61 -21
  123. visidata/loaders/api_airtable.py +70 -0
  124. visidata/loaders/api_bitio.py +102 -0
  125. visidata/loaders/api_matrix.py +148 -0
  126. visidata/loaders/api_reddit.py +306 -0
  127. visidata/loaders/api_zulip.py +249 -0
  128. visidata/loaders/archive.py +41 -7
  129. visidata/loaders/arrow.py +7 -7
  130. visidata/loaders/conll.py +49 -0
  131. visidata/loaders/csv.py +25 -7
  132. visidata/loaders/eml.py +3 -4
  133. visidata/loaders/f5log.py +1204 -0
  134. visidata/loaders/fec.py +325 -0
  135. visidata/loaders/fixed_width.py +2 -4
  136. visidata/loaders/frictionless.py +3 -3
  137. visidata/loaders/geojson.py +8 -5
  138. visidata/loaders/google.py +48 -0
  139. visidata/loaders/graphviz.py +4 -4
  140. visidata/loaders/hdf5.py +4 -4
  141. visidata/loaders/html.py +54 -12
  142. visidata/loaders/http.py +84 -30
  143. visidata/loaders/imap.py +20 -10
  144. visidata/loaders/jrnl.py +52 -0
  145. visidata/loaders/json.py +83 -29
  146. visidata/loaders/jsonla.py +74 -0
  147. visidata/loaders/lsv.py +15 -11
  148. visidata/loaders/mailbox.py +40 -0
  149. visidata/loaders/markdown.py +1 -3
  150. visidata/loaders/mbtiles.py +4 -5
  151. visidata/loaders/mysql.py +11 -13
  152. visidata/loaders/npy.py +7 -7
  153. visidata/loaders/odf.py +4 -1
  154. visidata/loaders/orgmode.py +428 -0
  155. visidata/loaders/pandas_freqtbl.py +14 -20
  156. visidata/loaders/parquet.py +62 -6
  157. visidata/loaders/pcap.py +3 -3
  158. visidata/loaders/pdf.py +4 -3
  159. visidata/loaders/png.py +19 -13
  160. visidata/loaders/postgres.py +9 -8
  161. visidata/loaders/rec.py +7 -3
  162. visidata/loaders/s3.py +342 -0
  163. visidata/loaders/sas.py +5 -5
  164. visidata/loaders/scrape.py +186 -0
  165. visidata/loaders/shp.py +6 -5
  166. visidata/loaders/spss.py +5 -6
  167. visidata/loaders/sqlite.py +68 -28
  168. visidata/loaders/texttables.py +1 -1
  169. visidata/loaders/toml.py +60 -0
  170. visidata/loaders/tsv.py +61 -19
  171. visidata/loaders/ttf.py +19 -7
  172. visidata/loaders/unzip_http.py +6 -5
  173. visidata/loaders/usv.py +1 -1
  174. visidata/loaders/vcf.py +16 -16
  175. visidata/loaders/vds.py +10 -7
  176. visidata/loaders/vdx.py +30 -5
  177. visidata/loaders/xlsb.py +8 -1
  178. visidata/loaders/xlsx.py +145 -25
  179. visidata/loaders/xml.py +6 -3
  180. visidata/loaders/xword.py +4 -4
  181. visidata/loaders/yaml.py +15 -5
  182. visidata/macros.py +129 -42
  183. visidata/main.py +119 -94
  184. visidata/mainloop.py +101 -155
  185. visidata/man/parse_options.py +2 -2
  186. visidata/man/vd.1 +302 -149
  187. visidata/man/vd.txt +291 -154
  188. visidata/memory.py +3 -3
  189. visidata/menu.py +104 -423
  190. visidata/metasheets.py +59 -141
  191. visidata/modify.py +78 -23
  192. visidata/motd.py +3 -3
  193. visidata/mouse.py +137 -0
  194. visidata/movement.py +43 -35
  195. visidata/optionssheet.py +99 -0
  196. visidata/path.py +113 -32
  197. visidata/pivot.py +73 -47
  198. visidata/plugins.py +65 -192
  199. visidata/pyobj.py +55 -205
  200. visidata/rename_col.py +20 -0
  201. visidata/save.py +37 -20
  202. visidata/search.py +54 -10
  203. visidata/selection.py +84 -5
  204. visidata/settings.py +162 -25
  205. visidata/sheets.py +239 -260
  206. visidata/shell.py +51 -21
  207. visidata/sidebar.py +162 -0
  208. visidata/sort.py +11 -4
  209. visidata/statusbar.py +114 -104
  210. visidata/stored_list.py +43 -0
  211. visidata/stored_prop.py +38 -0
  212. visidata/tests/benchmark.csv +52 -0
  213. visidata/tests/conftest.py +3 -3
  214. visidata/tests/test_cliptext.py +39 -0
  215. visidata/tests/test_commands.py +65 -7
  216. visidata/tests/test_edittext.py +2 -2
  217. visidata/tests/test_features.py +28 -0
  218. visidata/tests/test_menu.py +14 -0
  219. visidata/tests/test_path.py +13 -4
  220. visidata/text_source.py +53 -0
  221. visidata/textsheet.py +10 -3
  222. visidata/theme.py +44 -0
  223. visidata/themes/__init__.py +0 -0
  224. visidata/themes/ascii8.py +84 -0
  225. visidata/themes/asciimono.py +84 -0
  226. visidata/themes/light.py +17 -0
  227. visidata/threads.py +89 -40
  228. visidata/tuiwin.py +22 -0
  229. visidata/type_currency.py +22 -3
  230. visidata/type_date.py +31 -9
  231. visidata/type_floatsi.py +5 -1
  232. visidata/undo.py +17 -5
  233. visidata/utils.py +106 -23
  234. visidata/vdobj.py +28 -17
  235. visidata/windows.py +10 -0
  236. visidata/wrappers.py +9 -3
  237. visidata-3.0.1.data/data/share/applications/visidata.desktop +7 -0
  238. {visidata-2.11.1.data → visidata-3.0.1.data}/data/share/man/man1/vd.1 +302 -149
  239. {visidata-2.11.1.data → visidata-3.0.1.data}/data/share/man/man1/visidata.1 +302 -149
  240. visidata-3.0.1.data/scripts/vd2to3.vdx +9 -0
  241. {visidata-2.11.1.dist-info → visidata-3.0.1.dist-info}/METADATA +12 -8
  242. visidata-3.0.1.dist-info/RECORD +258 -0
  243. {visidata-2.11.1.dist-info → visidata-3.0.1.dist-info}/WHEEL +1 -1
  244. vgit/__init__.py +0 -1
  245. vgit/gitsheet.py +0 -164
  246. visidata/layout.py +0 -44
  247. visidata/misc.py +0 -5
  248. visidata-2.11.1.data/scripts/vgit +0 -9
  249. visidata-2.11.1.dist-info/RECORD +0 -155
  250. {vgit → visidata/apps/vgit}/__main__.py +0 -0
  251. {vgit → visidata/apps/vgit}/abort.py +0 -0
  252. /visidata/{repeat.py → features/repeat.py} +0 -0
  253. {visidata-2.11.1.data → visidata-3.0.1.data}/scripts/vd +0 -0
  254. {visidata-2.11.1.dist-info → visidata-3.0.1.dist-info}/LICENSE.gpl3 +0 -0
  255. {visidata-2.11.1.dist-info → visidata-3.0.1.dist-info}/entry_points.txt +0 -0
  256. {visidata-2.11.1.dist-info → visidata-3.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,43 @@
1
+ import json
2
+
3
+ from visidata import vd, VisiData, Path, AttrDict
4
+
5
+
6
+ @VisiData.api
7
+ class StoredList(list):
8
+ 'Read existing persisted list from filesystem, and append new elements to .jsonl in .visidata'
9
+ def __init__(self, *args, name:str='', **kwargs):
10
+ super().__init__(*args, **kwargs)
11
+ self.name = name
12
+
13
+ @property
14
+ def path(self):
15
+ vdpath = Path(vd.options.visidata_dir)
16
+ if vdpath.exists():
17
+ return vdpath/(self.name + '.jsonl')
18
+
19
+ def reload(self):
20
+ p = self.path
21
+ if not p or not p.exists():
22
+ return
23
+
24
+ ret = []
25
+ with p.open(encoding='utf-8-sig') as fp:
26
+ for line in fp:
27
+ value = vd.callNoExceptions(json.loads, line)
28
+ if value is not None:
29
+ if isinstance(value, dict):
30
+ value = AttrDict(value)
31
+ ret.append(value)
32
+
33
+ self[:] = ret # replace without using .append
34
+
35
+ def append(self, v):
36
+ super().append(v)
37
+
38
+ p = self.path
39
+ if p is None:
40
+ return
41
+
42
+ with p.open(encoding='utf-8', mode='a') as fp:
43
+ fp.write(json.dumps(v) + '\n')
@@ -0,0 +1,38 @@
1
+ from functools import wraps
2
+ import json
3
+ import atexit
4
+
5
+ from visidata import vd, VisiData, Path
6
+
7
+ vd.stored_properties = {}
8
+
9
+ @VisiData.class_api
10
+ @classmethod
11
+ def stored_property(vdcls, f):
12
+ def _save_on_exit():
13
+ if vd.stored_properties.get(f, None):
14
+ p = Path(vd.options.visidata_dir)/(f.__name__ + '.json')
15
+ with p.open(mode='w', encoding='utf-8') as fp:
16
+ fp.write(json.dumps(vd.stored_properties[f]))
17
+
18
+ @property
19
+ @wraps(f)
20
+ def _decorator(*args, **kwargs):
21
+ 'Read persisted value from filesystem if available; otherwise call the decorated function to create a new instance.'
22
+ value = vd.stored_properties.get(f, None)
23
+ if value is not None:
24
+ return value
25
+
26
+ p = Path(vd.options.visidata_dir)/(f.__name__ + '.json')
27
+ if p.exists():
28
+ value = json.loads(p.open(encoding='utf-8-sig').read())
29
+
30
+ if value is None:
31
+ value = f(*args, **kwargs)
32
+
33
+ vd.stored_properties[f] = value
34
+ return value
35
+
36
+ atexit.register(_save_on_exit)
37
+ setattr(vdcls, f.__name__, _decorator)
38
+ return _decorator
@@ -0,0 +1,52 @@
1
+ Date,Customer,SKU,Item,Quantity,Unit,Paid
2
+ 7/3/2018 1:47p,Robert Armstrong,FOOD213,BFF Oh My Gravy! Beef & Salmon 2.8oz,4,$12.95,$51.8
3
+ 7/3/2018 3:32p,Kyle Kennedy,FOOD121,"Food, Adult Cat - 3.5 oz",1,$4.22,$4.22
4
+ 7/5/2018 4:15p,"Douglas ""Dougie"" Powers",FOOD121,"Food, Adult Cat 3.5 oz",1,$4.22,$4.22
5
+ 7/6/2018 12:15p,桜 高橋 (Sakura Takahashi),FOOD122,"Food, Senior Wet Cat - 3 oz",12,$1.29,157¥
6
+ 7/10/2018 10:28a,David Attenborough,NSCT201,"Food, Salamander",30,$.05,$1.5
7
+ 7/10/2018 5:23p,Susan Ashworth,CAT060,"Cat, Korat (Felis catus)",1,$720.42,$720.42
8
+ 7/10/2018 5:23p,Susan Ashworth,FOOD130,"Food, Kitten 3kg",1,$14.94,$14.94
9
+ 7/13/2018 10:26a,Wil Wheaton,NSCT523,"Monster, Rust (Monstrus gygaxus)",1,$39.95,$39.95
10
+ 7/13/2018 3:49p,Robert Armstrong,FOOD216,BFF Oh My Gravy! Chicken & Shrimp 2.8oz,4,$12.95,$51.8
11
+ 7/17/2018 9:01a,Robert Armstrong,FOOD217,BFF Oh My Gravy! Duck & Tuna 2.8oz,4,$12.95,$51.8
12
+ 7/17/2018 11:30a,Helen Halestorm,LAGO342,Rabbit (Oryctolagus cuniculus),2,$32.94,$65.88
13
+ 7/18/2018 12:16p,桜 高橋 (Sakura Takahashi),FOOD122,"Food, Senior Wet Cat - 3 oz",6,$1.29,157¥
14
+ 7/19/2018 10:28a,Rubeus Hagrid,FOOD170,"Food, Dog - 5kg",5,$44.95,$224.75
15
+ 7/20/2018 2:13p,Jon Arbuckle,FOOD167,"Food, Premium Wet Cat - 3.5 oz",50,$3.95,$197.5
16
+ 7/23/2018 1:41p,Robert Armstrong,FOOD215,BFF Oh My Gravy! Lamb & Tuna 2.8oz,4,$12.95,$51.8
17
+ 7/23/2018 4:23p,"Douglas ""Dougie"" Powers",TOY235,Laser Pointer,1,$16.12,$16.12
18
+ 7/24/2018 12:16p,桜 高橋 (Sakura Takahashi),FOOD122,"Food, Senior Wet Cat - 3 oz",3,$1.29,157¥
19
+ 7/26/2018 4:39p,"Douglas ""Dougie"" Powers",FOOD420,"Food, Shark - 10 kg",1,$15.70,$15.7
20
+ 7/27/2018 12:16p,桜 高橋 (Sakura Takahashi),FOOD122,"Food, Senior Wet Cat - 3 oz",3,$1.29,157¥
21
+ 7/30/2018 12:17p,桜 高橋 (Sakura Takahashi),RETURN,"Food, Senior Wet Cat - 3 oz",1,$1.29,157¥
22
+ 7/31/2018 5:42p,Rubeus Hagrid,CAT060,"Food, Dragon - 50kg",5,$720.42,$3602.1
23
+ 8/1/2018 2:44p,David Attenborough,FOOD360,"Food, Rhinocerous - 50kg",4,$5.72,$22.88
24
+ 8/2/2018 5:12p,Susan Ashworth,CAT110,"Cat, Maine Coon (Felix catus)",1,"$1,309.68",$1309.68
25
+ 8/2/2018 5:12p,Susan Ashworth,FOOD130,"Food, Kitten 3kg",3,$14.94,$44.82
26
+ 8/6/2018 10:21a,Robert Armstrong,FOOD212,BFF Oh My Gravy! Beef & Chicken 2.8oz,4,$12.95,$51.8
27
+ 8/7/2018 4:12p,Juan Johnson,REPT082,"Kingsnake, California (Lampropeltis getula)",1,$89.95,$89.95
28
+ 8/7/2018 4:12p,Juan Johnson,RDNT443,"Mouse, Pinky (Mus musculus)",1,$1.49,$1.49
29
+ 8/10/2018 4:31p,Robert Armstrong,FOOD211,BFF Oh My Gravy! Chicken & Turkey 2.8oz,4,$12.95,$51.8
30
+ 8/13/2018 2:07p,Monica Johnson,RDNT443,"Mouse, Pinky (Mus musculus)",1,$1.49,$1.49
31
+ 8/13/2018 2:08p,María Fernández,FOOD146,Forti Diet Prohealth Mouse/Rat 3lbs,2,$2.00,$4.0
32
+ 8/15/2018 11:57a,Mr. Praline,RETURN,"Parrot, Norwegian Blue (Mopsitta tanta)",1,$2300.00,-$2300.0
33
+ 8/15/2018 3:48p,Kyle Kennedy,FOOD121,"Food, Adult Cat - 3.5 oz",2,$4.22,$8.44
34
+ 8/16/2018 11:50a,Helen Halestorm,RETURN,Rabbit (Oryctolagus cuniculus),6,$0,$0.0
35
+ 8/16/2018 4:00p,Kyle Kennedy,DOG010,"Dog, Golden Retriever (Canis lupus familiaris)",1,"$2,495.99",$2495.99
36
+ 8/16/2018 5:15p,Michael Smith,BIRD160,"Parakeet, Blue (Melopsittacus undulatus)",1,29.95,$31.85
37
+ 8/17/2018 9:26a,Rubeus Hagrid,NSCT201,"Food, Spider",5,$.05,$0.25
38
+ 8/20/2018 9:36a,Kyle Kennedy,RETURN,"Dog, Golden Retriever (Canis lupus familiaris)",1,"$1,247.99",-$1247.99
39
+ 8/20/2018 1:47p,מרוסיה ניסנהולץ אבולעפיה,GOAT224,"Goat, American Pygmy (Capra hircus)",1,₪499,$160.51
40
+ 8/20/2018 3:31p,Monica Johnson,NSCT201,"Crickets, Adult Live (Gryllus assimilis)",30,$.05,$1.5
41
+ 8/20/2018 5:12p,David Attenborough,NSCT084,"Food, Pangolin",30,$.17,$5.10
42
+ 8/21/2018 12:13p,Robert Armstrong,FOOD214,BFF Oh My Gravy! Duck & Salmon 2.8oz,4,$12.95,$51.8
43
+ 8/22/2018 9:38a,David Attenborough,BIRD160,"Food, Quoll",1,29.95,$29.95
44
+ 8/22/2018 2:13p,Jon Arbuckle,FOOD170,"Food, Adult Dog - 5kg",1,$44.95,$44.95
45
+ 8/22/2018 5:49p,מרוסיה ניסנהולץ,SFTY052,"Fire Extinguisher, kitchen-rated",1,$61.70,$61.70
46
+ 8/24/2018 11:42a,Robert Armstrong,FOOD218,BFF Oh My Gravy! Chicken & Salmon 2.8oz,4,$12.95,$51.8
47
+ 8/27/2018 3:05p,Monica Johnson,NSCT443,"Mealworms, Large (Tenebrio molitor) 100ct",1,$1.99,$1.99
48
+ 8/28/2018 5:32p,Susan Ashworth,CAT020,"Cat, Scottish Fold (Felis catus)",1,"$1,964.53",$1964.53
49
+ 8/28/2018 5:32p,Susan Ashworth,FOOD130,"Food, Kitten 3kg",2,$14.94,$29.88
50
+ 8/29/2018 10:07a,Robert Armstrong,FOOD219,BFF Oh My Gravy! Chicken & Pumpkin 2.8oz,4,$12.95,$51.8
51
+ 8/31/2018 12:00a,Robert Armstrong,FOOD219,BFF Oh My Gravy! Chicken & Pumpkin 2.8oz,144,$12.95,$1864.8
52
+ 8/31/2018 5:57p,Juan Johnson,REPT217,"Lizard, Spinytail (Uromastyx ornatus)",1,$99.95,$99.95
@@ -10,7 +10,7 @@ def curses_setup():
10
10
  import visidata
11
11
 
12
12
  curses.curs_set = lambda v: None
13
- visidata.options.confirm_overwrite = False
13
+ visidata.options.overwrite = 'always'
14
14
 
15
15
 
16
16
  @pytest.fixture(scope="function")
@@ -18,8 +18,8 @@ def mock_screen():
18
18
  """Set up and return a mock curses screen object."""
19
19
 
20
20
  scr = Mock()
21
- scr.addstr = Mock()
22
- scr.move = Mock()
21
+ scr.addstr = lambda *args, **kwargs: None
22
+ scr.move = lambda *args, **kwargs: None
23
23
  scr.getmaxyx = lambda: (25, 80)
24
24
 
25
25
  return scr
@@ -0,0 +1,39 @@
1
+ import pytest
2
+ from unittest.mock import Mock, call
3
+
4
+ import visidata
5
+
6
+
7
+ class TestClipText:
8
+ @pytest.mark.parametrize('s, dispw', [
9
+ ('abcdef', 6),
10
+ ('桜 高橋', 7),
11
+ ('[:onclick sidebar-toggle][:reverse] b to toggle sidebar [:]', 21),
12
+ ])
13
+ def test_dispwidth(self, s, dispw):
14
+ assert visidata.dispwidth(s) == dispw
15
+
16
+ @pytest.mark.parametrize('s, w, clippeds, clippedw', [
17
+ ('b to', 4, 'b to', 4),
18
+ ('abcde', 8, 'abcde', 5),
19
+ (' jsonl', 5, ' jso…', 5),
20
+ ('abcdで', 6, 'abcdで', 6),
21
+ ('abcdで', 5, 'abcd…', 5),
22
+ ])
23
+ def test_clipstr(self, s, w, clippeds, clippedw):
24
+ clips, clipw = visidata.clipstr(s, w)
25
+ assert clips == clippeds
26
+ assert clipw == clippedw
27
+
28
+ def test_clipdraw_chunks(self):
29
+ prechunks = [
30
+ ('', 'x'),
31
+ ('', 'jsonl'),
32
+ ]
33
+ scr = Mock()
34
+ scr.getmaxyx.return_value = (80,25)
35
+ visidata.clipdraw_chunks(scr, 0, 0, prechunks, visidata.ColorAttr(), w=5)
36
+ scr.addstr.assert_has_calls([
37
+ call(0, 0, 'x', 0),
38
+ call(0, 1, 'jso…', 0),
39
+ ], any_order=True)
@@ -1,9 +1,9 @@
1
- import pkg_resources
2
1
  import pytest
3
2
  from unittest.mock import Mock
4
3
 
5
4
  import itertools
6
5
  import visidata
6
+ from pathlib import Path
7
7
 
8
8
  # test separately as needed
9
9
 
@@ -11,11 +11,22 @@ import visidata
11
11
  # commands that require curses, and are not
12
12
  # replayable
13
13
  nonTested = (
14
+ 'toggle-profile',
14
15
  'syscopy',
15
16
  'syspaste',
17
+ 'open-syspaste',
16
18
  'macro',
17
19
  'mouse',
20
+ 'add-subreddits',
21
+ 'add-submissions',
22
+ 'open-zulip',
18
23
  'suspend',
24
+ 'open-memstats', # TODO add testing support
25
+ 'plot-column-ext',
26
+ 'plot-numerics-ext',
27
+ 'reload-every',
28
+ 'reload-modified',
29
+ 'reload-rows',
19
30
  'breakpoint',
20
31
  'redraw',
21
32
  'menu',
@@ -36,38 +47,72 @@ inputLines = { 'save-sheet': 'jetsam.csv', # save to some tmp file
36
47
  'save-col-keys': 'debris.csv',
37
48
  'pyobj-expr': '2+2', # open the python object for '4'
38
49
  'edit-cell': '3',
50
+ 'search-keys': 'foo',
39
51
  'search-col': 'foo',
40
52
  'searchr-col': 'bar',
41
53
  'select-col-regex': '.',
42
54
  'select-cols-regex': '.',
43
55
  'unselect-col-regex': '.',
56
+ 'exec-python': 'import time',
44
57
  'unselect-cols-regex': '.',
45
- 'edit-cell': '', # no change should not error
46
58
  'go-col-regex': 'Units', # column name in sample
47
59
  'go-col-number': '2',
48
60
  'go-row-number': '5', # go to row 5
49
61
  'addcol-bulk': '1',
50
62
  'addcol-expr': 'Units', # just copy the column
63
+ 'assert-expr': 'sheet.column(\"Units\")',
64
+ 'show-command-info': 'select-row',
65
+ 'assert-expr-row': 'Units',
51
66
  'addcol-incr-step': '2',
52
67
  'setcol-incr-step': '2',
68
+ 'setcol-iter': 'range(1, 100)',
53
69
  'setcol-format-enum': '1=cat',
54
- 'split-col': '-',
70
+ 'setcol-input': '5',
55
71
  'show-expr': 'OrderDate',
56
72
  'setcol-expr': 'OrderDate',
73
+ 'open-ping': 'localhost',
57
74
  'setcell-expr': 'OrderDate',
58
75
  'setcol-range': 'range(100)',
59
76
  'repeat-input-n': '1',
60
- 'capture-col': '(.)(.*)',
61
- 'addcol-subst': r'Units/(\w)/\1', # the first character
77
+ 'addcol-regex-subst': dict(before=r'Units/(\w)', after=r'\1'), # the first character
62
78
  'search-cols': 'foo',
63
79
  'searchr-cols': 'bar',
64
80
  'select-cols-regex': '.',
65
81
  'select-expr': 'OrderDate',
82
+ 'setcol-fake': 'name',
66
83
  'unselect-expr': 'OrderDate',
67
84
  'unselect-cols-regex': '.',
68
85
  'random-rows': '3',
86
+ 'select-random': '3',
69
87
  'import-python': 'math',
70
88
  'pyobj-expr-row': 'Units + "s"', # open the python object for '4'
89
+ 'expand-col-depth': '0',
90
+ 'contract-col-depth': '0',
91
+ 'contract-cols-depth': '0',
92
+ 'expand-cols-depth': '0',
93
+ 'save-cmdlog': 'test_commands.vdj',
94
+ 'aggregate-col': 'mean',
95
+ 'memo-aggregate': 'mean',
96
+ 'addcol-shell': '',
97
+ 'theme-input': 'light',
98
+ 'add-rows': '1',
99
+ 'join-sheets-top2': 'append',
100
+ 'join-sheets-all': 'append',
101
+ 'resize-col-input': '10',
102
+ 'resize-cols-input': '10',
103
+ 'resize-height-input': '10',
104
+ 'melt-regex': '(.*)_(.*)',
105
+ 'addcol-split': '-',
106
+ 'addcol-capture': '(.*)_(.*)',
107
+ 'slide-left-n': '2',
108
+ 'slide-right-n': '1',
109
+ 'slide-down-n': '1',
110
+ 'slide-up-n': '1',
111
+ 'addcol-window': '0 2',
112
+ 'select-around-n': '1',
113
+ 'sheet': '',
114
+ 'col': 'Units',
115
+ 'row': '5',
71
116
  }
72
117
 
73
118
  @pytest.mark.usefixtures('curses_setup')
@@ -86,6 +131,9 @@ class TestCommands:
86
131
  nerrs = 0
87
132
  ntotal = 0
88
133
  for longname in cmdlist.keys():
134
+ cmd = vs.getCommand(longname)
135
+ if cmd and cmd.deprecated:
136
+ continue
89
137
  if not isTestableCommand(longname, cmdlist):
90
138
  continue
91
139
  ntotal += 1
@@ -102,6 +150,12 @@ class TestCommands:
102
150
  if nerrs > 0:
103
151
  assert False
104
152
 
153
+ # cleanup
154
+ for f in ['flotsam.csv', 'debris.csv', 'jetsam.csv', 'lagan.csv', 'test_commands.vdj']:
155
+ pf = Path(f)
156
+ if pf.exists: pf.unlink()
157
+
158
+
105
159
  def runOneTest(self, mock_screen, longname):
106
160
  visidata.vd.clearCaches() # we want vd to return a new VisiData object for each command
107
161
  vd = visidata.vd
@@ -114,7 +168,7 @@ class TestCommands:
114
168
  else:
115
169
  vd.getkeystroke = Mock(side_effect=['^J'])
116
170
 
117
- sample_file = pkg_resources.resource_filename('visidata', 'tests/sample.tsv')
171
+ sample_file = vd.pkg_resources_files(visidata) / 'tests/sample.tsv'
118
172
  vs = visidata.TsvSheet('test_commands', source=visidata.Path(sample_file))
119
173
  vs.reload.__wrapped__(vs)
120
174
  vs.vd = vd
@@ -122,4 +176,8 @@ class TestCommands:
122
176
  vd.allSheets = [vs]
123
177
  vs.mouseX, vs.mouseY = (4, 4)
124
178
  vs.draw(mock_screen)
125
- vs.execCommand(longname, vdglobals=vars(visidata))
179
+ if longname in inputLines:
180
+ vd.currentReplayRow = vd.cmdlog.newRow(longname=longname, input=inputLines[longname])
181
+ else:
182
+ vd.currentReplayRow = vd.cmdlog.newRow(longname=longname)
183
+ vs.execCommand(longname, vdglobals=vd.getGlobals())
@@ -48,7 +48,7 @@ class TestEditText:
48
48
  exception = kwargs.pop('exception', None)
49
49
  if exception:
50
50
  with pytest.raises(exception):
51
- visidata.vd.editline(mock_screen, 0, 0, 0, **kwargs)
51
+ visidata.vd.editline(mock_screen, 0, 0, 0, attr=visidata.ColorAttr(), **kwargs)
52
52
  else:
53
- r = visidata.vd.editline(mock_screen, 0, 0, 0, **kwargs)
53
+ r = visidata.vd.editline(mock_screen, 0, 0, 0, attr=visidata.ColorAttr(), **kwargs)
54
54
  assert r == result
@@ -0,0 +1,28 @@
1
+ import pytest
2
+ import visidata
3
+
4
+ def pytest_generate_tests(metafunc):
5
+ """Split feature tests into separate test cases
6
+
7
+ Look up test methods in imported modules. Turn each one into a single test_feature()
8
+ case, with "module::method" as the test id.
9
+ """
10
+ tests = [
11
+ (mod, getattr(mod, k))
12
+ for mod in visidata.vd.importedModules
13
+ for k in dir(mod)
14
+ if k.startswith("test_")
15
+ ]
16
+ argvalues = [[testfunc] for _, testfunc in tests]
17
+ testids = [
18
+ f"{mod.__name__}::{testfunc.__name__}"
19
+ for mod, testfunc in tests
20
+ ]
21
+ metafunc.parametrize(argnames=["testfunc"], argvalues=argvalues, ids=testids)
22
+
23
+
24
+ @pytest.mark.usefixtures("curses_setup")
25
+ def test_feature(mock_screen, testfunc):
26
+ visidata.vd.resetVisiData()
27
+ visidata.vd.scr = mock_screen
28
+ testfunc(visidata.vd)
@@ -0,0 +1,14 @@
1
+
2
+ from visidata import vd, TableSheet
3
+ import pytest
4
+
5
+
6
+ class TestMenu:
7
+ def test_menuitems(self):
8
+ vd.addMenuItems('''Column > Add column > foobar > hello-world''')
9
+
10
+ m = TableSheet().getMenuItem(['Column', 'Add column', 'foobar'])
11
+ assert m
12
+
13
+ with pytest.raises(AssertionError):
14
+ vd.addMenuItems('''Column > Add column > non-command''')
@@ -1,3 +1,4 @@
1
+ import io
1
2
  import pytest
2
3
 
3
4
  from visidata import Path
@@ -16,11 +17,19 @@ class TestVisidataPath:
16
17
  assert "https://visidata.org/hello/b.tsv" == str(url_path.with_name('b.tsv')), '{} should be https://visidata.org/hello/b.tsv'.format(url_path.with_name('b.tsv'))
17
18
  assert "https://visidata.org/hello/a/b.tsv" == str(url_path.with_name('a/b.tsv')), '{} should be https://visidata.org/hello/a/b.tsv'.format(url_path.with_name('a/b.tsv'))
18
19
 
19
- assert Path('foo.a.b').name == 'foo.a'
20
+ assert Path('foo.a.b').base_stem == 'foo.a'
20
21
  assert Path('foo.a.b').ext == 'b'
21
22
  assert Path('foo').ext == ''
22
- assert Path('foo').name == 'foo'
23
+ assert Path('foo').base_stem == 'foo'
23
24
  assert Path('foo.').ext == ''
24
- assert Path('foo.').name == 'foo.'
25
+ assert Path('foo.').base_stem == 'foo.'
25
26
  assert Path('.foo').ext == ''
26
- assert Path('.foo').name == '.foo'
27
+ assert Path('.foo').base_stem == '.foo'
28
+
29
+
30
+ def test_opentwice(self):
31
+ 'fresh iterator for each open'
32
+ p = Path('test', fptext=io.StringIO('<html>'))
33
+ a = next(p.open())
34
+ b = next(p.open())
35
+ assert a == b
@@ -0,0 +1,53 @@
1
+ import re
2
+
3
+ from visidata import vd, BaseSheet
4
+
5
+ vd.option('regex_skip', '', 'regex of lines to skip in text sources', help='regex')
6
+ vd.option('regex_flags', 'I', 'flags to pass to re.compile() [AILMSUX]', replay=True)
7
+
8
+ @BaseSheet.api
9
+ def regex_flags(sheet):
10
+ 'Return flags to pass to regex functions from options'
11
+ return sum(getattr(re, f.upper()) for f in sheet.options.regex_flags)
12
+
13
+
14
+ class FilterFile:
15
+ def __init__(self, fp, regex:str, regex_flags:int=0):
16
+ import re
17
+ self._fp = fp
18
+ self._regex_skip = re.compile(regex, regex_flags)
19
+
20
+ def readline(self) -> str:
21
+ while True:
22
+ line = self._fp.readline()
23
+ if self._regex_skip.match(line):
24
+ continue
25
+ return line
26
+
27
+ def __getattr__(self, k):
28
+ return getattr(self._fp, k)
29
+
30
+ def __iter__(self):
31
+ return self
32
+
33
+ def __next__(self):
34
+ line = self.readline()
35
+ if not line:
36
+ raise StopIteration
37
+ return line
38
+
39
+ def __enter__(self):
40
+ return self
41
+
42
+ def __exit__(self, *args, **kwargs):
43
+ return self._fp.__exit__(*args, **kwargs)
44
+
45
+
46
+ @BaseSheet.api
47
+ def open_text_source(sheet):
48
+ 'Open sheet source as text, using sheet options for encoding and regex_skip.'
49
+ fp = sheet.source.open(encoding=sheet.options.encoding, encoding_errors=sheet.options.encoding_errors)
50
+ regex_skip = sheet.options.regex_skip
51
+ if regex_skip:
52
+ return FilterFile(fp, regex_skip, sheet.regex_flags())
53
+ return fp
visidata/textsheet.py CHANGED
@@ -6,7 +6,7 @@ from visidata import globalCommand, VisiData
6
6
  import visidata
7
7
 
8
8
 
9
- vd.option('wrap', False, 'wrap text to fit window width on TextSheet')
9
+ vd.option('wrap', False, 'wrap text to fit window width on TextSheet', max_help=0)
10
10
  vd.option('save_filetype', 'tsv', 'specify default file type to save as', replay=True)
11
11
 
12
12
 
@@ -32,7 +32,7 @@ class TextSheet(Sheet):
32
32
  for i, L in enumerate(textwrap.wrap(str(text), width=winWidth)):
33
33
  yield [startingLine+i+1, L]
34
34
  else:
35
- yield [startingLine+1, text]
35
+ yield [startingLine+1, text.strip()]
36
36
 
37
37
  def sysopen(sheet, linenum=0):
38
38
  @asyncthread
@@ -44,6 +44,7 @@ class TextSheet(Sheet):
44
44
 
45
45
  import tempfile
46
46
  with tempfile.NamedTemporaryFile() as temp:
47
+ temp.close() #2118
47
48
  writelines(sheet, temp.name)
48
49
  vd.launchEditor(temp.name, '+%s' % linenum)
49
50
  sheet.rows = []
@@ -82,7 +83,7 @@ def recentErrorsSheet(self):
82
83
  BaseSheet.addCommand('^E', 'error-recent', 'vd.lastErrors and vd.push(recentErrorsSheet) or status("no error")', 'view traceback for most recent error')
83
84
  BaseSheet.addCommand('g^E', 'errors-all', 'vd.push(vd.allErrorsSheet)', 'view traceback for most recent errors')
84
85
 
85
- Sheet.addCommand(None, 'view-cell', 'vd.push(ErrorSheet("%s[%s].%s" % (name, cursorRowIndex, cursorCol.name), sourceSheet=sheet, source=cursorDisplay.splitlines()))', 'view contents of current cell in a new sheet'),
86
+ Sheet.addCommand(None, 'view-cell', 'vd.push(ErrorSheet("%s[%s].%s" % (name, cursorRowIndex, cursorCol.name), sourceSheet=sheet, source=cursorDisplay.splitlines()))', 'view contents of current cell in a new sheet')
86
87
  Sheet.addCommand('z^E', 'error-cell', 'vd.push(ErrorSheet(sheet.name+"_cell_error", sourceSheet=sheet, source=getattr(cursorCell, "error", None) or fail("no error this cell")))', 'view traceback for error in current cell')
87
88
 
88
89
  TextSheet.addCommand('^O', 'sysopen-sheet', 'sheet.sysopen(sheet.cursorRowIndex)', 'open copy of text sheet in $EDITOR and reload on exit')
@@ -91,3 +92,9 @@ TextSheet.addCommand('^O', 'sysopen-sheet', 'sheet.sysopen(sheet.cursorRowIndex)
91
92
  TextSheet.options.save_filetype = 'txt'
92
93
 
93
94
  vd.addGlobals({'TextSheet': TextSheet, 'ErrorSheet': ErrorSheet})
95
+
96
+ vd.addMenuItems('''
97
+ View > Errors > recent > error-recent
98
+ View > Errors > all > errors-all
99
+ View > Errors > in cell > error-cell
100
+ ''')
visidata/theme.py ADDED
@@ -0,0 +1,44 @@
1
+ 'Switch between packaged themes (colors and display characters)'
2
+
3
+ from visidata import vd, VisiData, Sheet, BaseSheet
4
+
5
+
6
+ vd.option('theme', '', 'display/color theme to use')
7
+
8
+ vd.themes = {}
9
+
10
+
11
+ @VisiData.before
12
+ def run(vd, *args, **kwargs):
13
+ t = vd.options.theme
14
+ if t:
15
+ vd.set_theme(t)
16
+
17
+
18
+ @Sheet.api
19
+ @VisiData.api
20
+ def set_theme(obj, theme=''):
21
+ if theme and theme not in vd.themes:
22
+ vd.warning(f'no "{theme}" theme available')
23
+ return
24
+
25
+ # unset everything first
26
+ for k in vd.options.keys():
27
+ if k.startswith(tuple('color_ disp_ note_'.split())):
28
+ obj.options.unset(k)
29
+
30
+ if not theme:
31
+ return
32
+
33
+ if isinstance(theme, str):
34
+ theme = vd.themes[theme]
35
+
36
+ for k, v in theme.items():
37
+ obj.options[k] = v
38
+
39
+
40
+ BaseSheet.addCommand('', 'theme-input', 'vd.set_theme(chooseOne([dict(key=k) for k in themes.keys()], type="theme"))', 'choose from available themes')
41
+ BaseSheet.addCommand('', 'theme-default', 'vd.set_theme()', 'reset theme to VisiData defaults')
42
+
43
+ vd.addMenuItem('View', 'Set theme', 'choose', 'theme-input')
44
+ vd.addMenuItem('View', 'Set theme', 'default', 'theme-default')
File without changes
@@ -0,0 +1,84 @@
1
+ 'ASCII theme using the first 8 colors'
2
+
3
+ from visidata import vd
4
+
5
+
6
+ vd.themes['ascii8'] = dict(
7
+ disp_note_none='',
8
+ disp_truncator='>',
9
+ disp_oddspace='.',
10
+ disp_more_left='<',
11
+ disp_more_right='>',
12
+ disp_error_val='',
13
+ disp_ambig_width=1,
14
+
15
+ disp_pending='',
16
+ note_pending=':',
17
+ note_format_exc='?',
18
+ note_getter_exc='!',
19
+ note_type_exc='!',
20
+
21
+ color_note_pending='bold magenta',
22
+ color_note_type='yellow',
23
+ color_note_row='yellow',
24
+
25
+ disp_column_sep='|',
26
+ disp_keycol_sep='|',
27
+ disp_rowtop_sep='|',
28
+ disp_rowmid_sep='|',
29
+ disp_rowbot_sep='|',
30
+ disp_rowend_sep='|',
31
+ disp_keytop_sep='|',
32
+ disp_keymid_sep='|',
33
+ disp_keybot_sep='|',
34
+ disp_endtop_sep='|',
35
+ disp_endmid_sep='|',
36
+ disp_endbot_sep='|',
37
+ disp_selected_note='+',
38
+ disp_sort_asc='^^^^^^',
39
+ disp_sort_desc='vvvvvv',
40
+ color_default='white on black',
41
+ color_default_hdr='bold',
42
+ color_bottom_hdr='underline',
43
+ color_current_row='reverse',
44
+ color_current_col='bold',
45
+ color_current_hdr='bold reverse',
46
+ color_column_sep='blue',
47
+ color_key_col='cyan',
48
+ color_hidden_col='8',
49
+ color_selected_row='yellow',
50
+ color_edit_cell='white',
51
+ color_graph_hidden='blue',
52
+ color_graph_selected='bold',
53
+ color_status_replay='green',
54
+
55
+ color_graph_axis='bold',
56
+ color_sidebar='black on blue',
57
+ color_add_pending='green',
58
+ color_change_pending='reverse yellow',
59
+ color_delete_pending='red',
60
+ disp_rstatus_fmt=' {sheet.longname} {sheet.nRows:9d} {sheet.rowtype} {sheet.modifiedStatus} {sheet.options.disp_selected_note}{sheet.nSelectedRows}',
61
+ disp_status_fmt='{sheet.shortcut}> {sheet.name}| ',
62
+ disp_lstatus_max=0,
63
+ disp_status_sep=' | ',
64
+ color_keystrokes='bold black on cyan',
65
+ color_status='bold black on cyan',
66
+ color_error='red',
67
+ color_warning='yellow',
68
+ color_top_status='underline',
69
+ color_active_status='black on cyan',
70
+ color_inactive_status='8 on black',
71
+ color_working='green',
72
+
73
+ color_menu='black on cyan',
74
+ color_menu_active='yellow on black',
75
+ color_menu_spec='black on green',
76
+ color_menu_help='black on cyan',
77
+ disp_menu_boxchars='||-- ||',
78
+ disp_menu_more='>',
79
+ disp_menu_push='+',
80
+ disp_menu_input='_',
81
+ disp_menu_fmt='7-bit ASCII 3-bit color',
82
+ plot_colors = 'white',
83
+ disp_histogram='*'
84
+ )