visidata 2.11.1__py3-none-any.whl → 3.0__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 (255) 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 +65 -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 +59 -50
  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 +5 -1
  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 +30 -5
  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 +163 -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} +75 -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 +180 -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 +17 -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 +48 -10
  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 +301 -148
  187. visidata/man/vd.txt +290 -153
  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 +50 -201
  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 +229 -257
  206. visidata/shell.py +51 -21
  207. visidata/sidebar.py +162 -0
  208. visidata/sort.py +11 -4
  209. visidata/statusbar.py +113 -104
  210. visidata/stored_list.py +43 -0
  211. visidata/stored_prop.py +38 -0
  212. visidata/tests/conftest.py +3 -3
  213. visidata/tests/test_cliptext.py +39 -0
  214. visidata/tests/test_commands.py +62 -7
  215. visidata/tests/test_edittext.py +2 -2
  216. visidata/tests/test_features.py +17 -0
  217. visidata/tests/test_menu.py +14 -0
  218. visidata/tests/test_path.py +13 -4
  219. visidata/text_source.py +53 -0
  220. visidata/textsheet.py +10 -3
  221. visidata/theme.py +44 -0
  222. visidata/themes/__init__.py +0 -0
  223. visidata/themes/ascii8.py +84 -0
  224. visidata/themes/asciimono.py +84 -0
  225. visidata/themes/light.py +17 -0
  226. visidata/threads.py +87 -39
  227. visidata/tuiwin.py +22 -0
  228. visidata/type_currency.py +22 -3
  229. visidata/type_date.py +31 -9
  230. visidata/type_floatsi.py +5 -1
  231. visidata/undo.py +17 -5
  232. visidata/utils.py +106 -23
  233. visidata/vdobj.py +28 -17
  234. visidata/windows.py +10 -0
  235. visidata/wrappers.py +9 -3
  236. visidata-3.0.data/data/share/applications/visidata.desktop +7 -0
  237. {visidata-2.11.1.data → visidata-3.0.data}/data/share/man/man1/vd.1 +301 -148
  238. {visidata-2.11.1.data → visidata-3.0.data}/data/share/man/man1/visidata.1 +301 -148
  239. visidata-3.0.data/scripts/vd2to3.vdx +9 -0
  240. {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/METADATA +12 -8
  241. visidata-3.0.dist-info/RECORD +257 -0
  242. {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/WHEEL +1 -1
  243. vgit/__init__.py +0 -1
  244. vgit/gitsheet.py +0 -164
  245. visidata/layout.py +0 -44
  246. visidata/misc.py +0 -5
  247. visidata-2.11.1.data/scripts/vgit +0 -9
  248. visidata-2.11.1.dist-info/RECORD +0 -155
  249. {vgit → visidata/apps/vgit}/__main__.py +0 -0
  250. {vgit → visidata/apps/vgit}/abort.py +0 -0
  251. /visidata/{repeat.py → features/repeat.py} +0 -0
  252. {visidata-2.11.1.data → visidata-3.0.data}/scripts/vd +0 -0
  253. {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/LICENSE.gpl3 +0 -0
  254. {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/entry_points.txt +0 -0
  255. {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,17 @@
1
+ 'Light-mode theme using 256-colors.'
2
+
3
+ from visidata import vd
4
+
5
+ vd.themes['light'] = dict(
6
+ color_default = 'black on white', # the default fg and bg colors
7
+ color_key_col = '20 blue', # color of key columns
8
+ color_edit_cell = '234 black', # cell color to use when editing cell
9
+ color_selected_row = '164 magenta', # color of selected rows
10
+ color_note_row = '164 magenta', # color of row note on left edge
11
+ color_note_type = '88 red', # color of cell note for non-str types in anytype columns
12
+ color_warning = '202 11 yellow',
13
+ color_add_pending = '34 green',
14
+ color_change_pending = '166 yellow',
15
+ plot_colors = '20 red magenta black 28 88 94 99 106'
16
+ )
17
+
visidata/threads.py CHANGED
@@ -5,16 +5,17 @@ import functools
5
5
  import cProfile
6
6
  import threading
7
7
  import collections
8
+ import subprocess
9
+ import curses
8
10
 
9
11
  from visidata import VisiData, vd, options, globalCommand, Sheet, EscapeException
10
- from visidata import ColumnAttr, Column
11
- from visidata import *
12
+ from visidata import ColumnAttr, Column, BaseSheet, ItemColumn
12
13
 
13
14
 
14
- vd.option('profile', False, 'enable profiling on threads')
15
- vd.option('min_memory_mb', 0, 'minimum memory to continue loading and async processing')
15
+ vd.option('profile', False, 'enable profiling on threads', max_help=-1)
16
+ vd.option('min_memory_mb', 0, 'minimum memory to continue loading and async processing', max_help=-1)
16
17
 
17
- vd.option('color_working', 'green', 'color of system running smoothly')
18
+ vd.theme_option('color_working', '118 5', 'color of system running smoothly')
18
19
 
19
20
  BaseSheet.init('currentThreads', list)
20
21
 
@@ -115,29 +116,32 @@ def elapsed_s(t):
115
116
 
116
117
  @VisiData.api
117
118
  def checkMemoryUsage(vd):
118
- min_mem = options.min_memory_mb
119
119
  threads = vd.unfinishedThreads
120
120
  if not threads:
121
- return None
122
- ret = ''
123
- attr = 'color_working'
124
- if min_mem:
125
- try:
126
- freestats = subprocess.run('free --total --mega'.split(), check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE).stdout.strip().splitlines()
127
- except FileNotFoundError as e:
128
- if options.debug:
129
- vd.exceptionCaught(e)
130
- options.min_memory_mb = 0
131
- vd.warning('disabling min_memory_mb: "free" not installed')
132
- return '', attr
133
- tot_m, used_m, free_m = map(int, freestats[-1].split()[1:])
134
- ret = '[%dMB] ' % free_m + ret
135
- if free_m < min_mem:
136
- attr = 'color_warning'
137
- vd.warning('%dMB free < %dMB minimum, stopping threads' % (free_m, min_mem))
138
- vd.cancelThread(*vd.unfinishedThreads)
139
- curses.flash()
140
- return ret, attr
121
+ return ''
122
+
123
+ min_mem = vd.options.min_memory_mb
124
+ if not min_mem:
125
+ return ''
126
+
127
+ try:
128
+ freestats = subprocess.run('free --total --mega'.split(), check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE).stdout.strip().splitlines()
129
+ except FileNotFoundError as e:
130
+ if vd.options.debug:
131
+ vd.exceptionCaught(e)
132
+ vd.options.min_memory_mb = 0
133
+ vd.warning('disabling min_memory_mb: "free" not installed')
134
+ return ''
135
+ tot_m, used_m, free_m = map(int, freestats[-1].split()[1:])
136
+ ret = f' [{free_m}MB] '
137
+ if free_m < min_mem:
138
+ attr = '[:warning]'
139
+ vd.warning(f'{free_m}MB free < {min_mem}MB minimum, stopping threads')
140
+ vd.cancelThread(*vd.unfinishedThreads)
141
+ curses.flash()
142
+ else:
143
+ attr = '[:working]'
144
+ return attr + ret + '[/]'
141
145
 
142
146
 
143
147
  # for progress bar
@@ -170,17 +174,34 @@ def _annotate_thread(t, endTime=None):
170
174
  return t
171
175
 
172
176
  # all long-running threads, including main and finished
173
- VisiData.init('threads', lambda: [_annotate_thread(threading.current_thread(), 0)])
177
+ vd.threads = [_annotate_thread(threading.current_thread(), 0)]
174
178
 
175
179
  @VisiData.api
176
- def execAsync(self, func, *args, sheet=None, **kwargs):
177
- 'Execute ``func(*args, **kwargs)`` in a separate thread.'
180
+ def execSync(vd, func, *args, sheet=None, **kwargs):
181
+ 'Execute ``func(*args, **kwargs)`` in this thread (synchronously). A drop-in substitute for vd.execAsync.'
182
+ vd.callNoExceptions(func, *args, **kwargs)
183
+ t = threading.current_thread()
184
+ t.sheet = sheet or vd.activeSheet
185
+ return t
178
186
 
179
- thread = threading.Thread(target=_toplevelTryFunc, daemon=True, args=(func,)+args, kwargs=kwargs)
180
- self.threads.append(_annotate_thread(thread))
187
+ @VisiData.api
188
+ def execAsync(vd, func, *args, **kwargs):
189
+ '''Execute ``func(*args, **kwargs)`` in a separate thread. `sheet` is a
190
+ special kwarg to indicate which sheet the thread should be associated with;
191
+ by default, uses vd.activeSheet. If `sheet` explicitly given as None, the thread
192
+ will be ignored by vd.sync and thread status indicators.
193
+ '''
194
+
195
+ if 'sheet' not in kwargs:
196
+ sheet = vd.activeSheet
197
+ else:
198
+ sheet = kwargs.pop('sheet')
199
+
200
+ if sheet is not None and (sheet.lastCommandThreads and threading.current_thread() not in sheet.lastCommandThreads):
201
+ vd.fail(f'still running **{sheet.lastCommandThreads[-1].name}** from previous command')
181
202
 
182
- if sheet is None:
183
- sheet = self.activeSheet
203
+ thread = threading.Thread(target=_toplevelTryFunc, daemon=True, args=(func,)+args, kwargs=kwargs)
204
+ vd.threads.append(_annotate_thread(thread))
184
205
 
185
206
  if sheet is not None:
186
207
  sheet.currentThreads.append(thread)
@@ -190,7 +211,7 @@ def execAsync(self, func, *args, sheet=None, **kwargs):
190
211
 
191
212
  return thread
192
213
 
193
- def _toplevelTryFunc(func, *args, status=vd.status, **kwargs):
214
+ def _toplevelTryFunc(func, *args, **kwargs):
194
215
  with ThreadProfiler(threading.current_thread()) as prof:
195
216
  t = threading.current_thread()
196
217
  t.name = func.__name__
@@ -198,8 +219,7 @@ def _toplevelTryFunc(func, *args, status=vd.status, **kwargs):
198
219
  t.status = func(*args, **kwargs)
199
220
  except EscapeException as e: # user aborted
200
221
  t.status = 'aborted by user'
201
- if status:
202
- status('%s aborted' % t.name, priority=2)
222
+ vd.warning(f'{t.name} aborted')
203
223
  except Exception as e:
204
224
  t.exception = e
205
225
  t.status = 'exception'
@@ -208,6 +228,18 @@ def _toplevelTryFunc(func, *args, status=vd.status, **kwargs):
208
228
  if t.sheet:
209
229
  t.sheet.currentThreads.remove(t)
210
230
 
231
+ def asyncignore(func):
232
+ 'Decorator like `@asyncthread` but without attaching to a sheet, so no sheet.threadStatus will show it.'
233
+ @functools.wraps(func)
234
+ def _execAsync(*args, **kwargs):
235
+ @functools.wraps(func)
236
+ def _func(*args, **kwargs):
237
+ func(*args, **kwargs)
238
+
239
+ return vd.execAsync(_func, *args, **kwargs, sheet=None)
240
+
241
+ return _execAsync
242
+
211
243
  def asyncsingle(func):
212
244
  '''Function decorator like `@asyncthread` but as a singleton. When called, `func(...)` spawns a new thread, and cancels any previous thread still running *func*.
213
245
  ``vd.sync()`` does not wait for unfinished asyncsingle threads.
@@ -233,7 +265,7 @@ def asyncsingle(func):
233
265
  @VisiData.property
234
266
  def unfinishedThreads(self):
235
267
  'A list of unfinished threads (those without a recorded `endTime`).'
236
- return [t for t in self.threads if getattr(t, 'endTime', None) is None]
268
+ return [t for t in self.threads if getattr(t, 'endTime', None) is None and getattr(t, 'sheet', None) is not None]
237
269
 
238
270
  @VisiData.api
239
271
  def checkForFinishedThreads(self):
@@ -251,8 +283,9 @@ def sync(self, *joiningThreads):
251
283
  while True:
252
284
  deads = set() # dead threads
253
285
  threads = joiningThreads or set(self.unfinishedThreads)
254
- threads -= set([threading.current_thread(), getattr(vd, 'drawThread', None)])
286
+ threads -= set([threading.current_thread(), getattr(vd, 'drawThread', None), getattr(vd, 'outputProgressThread', None)])
255
287
  threads -= deads
288
+ threads -= set([None])
256
289
  for t in threads:
257
290
  try:
258
291
  if not t.is_alive() or t not in threading.enumerate() or getattr(t, 'noblock', False) is True:
@@ -272,7 +305,7 @@ min_thread_time_s = 0.10 # only keep threads that take longer than this number o
272
305
  @VisiData.api
273
306
  def open_pyprof(vd, p):
274
307
  import pstats
275
- return ProfileStatsSheet(p.name, source=pstats.Stats(p.given).stats)
308
+ return ProfileStatsSheet(p.base_stem, source=pstats.Stats(p.given).stats)
276
309
 
277
310
 
278
311
  @VisiData.api
@@ -308,10 +341,19 @@ class ThreadProfiler:
308
341
  # remove very-short-lived async actions
309
342
  if elapsed_s(self.thread) < min_thread_time_s:
310
343
  vd.threads.remove(self.thread)
344
+ else:
345
+ if vd.options.profile:
346
+ self.thread.profile.dump_stats(f'{self.thread.name}.pyprof')
311
347
 
312
348
 
313
349
  class ProfileSheet(Sheet):
314
350
  rowtype = 'callsites' # rowdef: profiler_entry
351
+ guide = '''
352
+ # Profile Sheet
353
+ - `z Ctrl+S` to save as pyprof file
354
+ - `Ctrl+O` to open current function in $EDITOR
355
+ - `Enter` to open list of calls from current function
356
+ '''
315
357
  columns = [
316
358
  Column('funcname', getter=lambda col,row: codestr(row.code)),
317
359
  Column('filename', getter=lambda col,row: os.path.split(row.code.co_filename)[-1] if not isinstance(row.code, str) else ''),
@@ -404,4 +446,10 @@ vd.addGlobals({
404
446
  'Progress': Progress,
405
447
  'asynccache': asynccache,
406
448
  'asyncsingle': asyncsingle,
449
+ 'asyncignore': asyncignore,
407
450
  })
451
+
452
+ vd.addMenuItems('''
453
+ System > Threads sheet > threads-all
454
+ System > Toggle profiling > toggle-profile
455
+ ''')
visidata/tuiwin.py ADDED
@@ -0,0 +1,22 @@
1
+ from visidata import VisiData, vd
2
+
3
+ vd._parentscrs = {} # scr -> parentscr
4
+
5
+
6
+ @VisiData.api
7
+ def subwindow(vd, scr, x, y, w, h):
8
+ 'Return subwindow with its (0,0) at (x,y) relative to parent scr. Replacement for scr.derwin() to track parent scr.'
9
+ newscr = scr.derwin(h, w, y, x)
10
+ vd._parentscrs[newscr] = scr
11
+ return newscr
12
+
13
+
14
+ @VisiData.api
15
+ def getrootxy(vd, scr): # like scr.getparyx() but for all ancestor scrs
16
+ px, py = 0, 0
17
+ while scr in vd._parentscrs:
18
+ dy, dx = scr.getparyx()
19
+ if dy > 0: py += dy
20
+ if dx > 0: px += dx
21
+ scr = vd._parentscrs[scr]
22
+ return px, py
visidata/type_currency.py CHANGED
@@ -1,6 +1,7 @@
1
- from visidata import vd, Sheet
1
+ from visidata import vd, Sheet, Column
2
2
 
3
- vd.option('disp_currency_fmt', '%.02f', 'default fmtstr to format for currency values', replay=True)
3
+ vd.option('disp_currency_fmt', '%.02f', 'default fmtstr to format for currency values', replay=True, help=vd.help_float_fmt)
4
+ vd.theme_option('color_currency_neg', 'red', 'color for negative values in currency displayer', replay=True)
4
5
 
5
6
 
6
7
  floatchars='+-0123456789.'
@@ -13,4 +14,22 @@ def currency(*args):
13
14
  return float(*args)
14
15
 
15
16
 
16
- Sheet.addCommand('$', 'type-currency', 'cursorCol.type = currency', 'set type of current column to currency')
17
+ @Column.api
18
+ def displayer_currency(col, dw, width=None):
19
+ text = dw.text
20
+
21
+ if isinstance(dw.typedval, (int, float)):
22
+ if dw.typedval < 0:
23
+ text = f'({dw.text[1:]})'.rjust(width-1)
24
+ yield ('currency_neg', '')
25
+ else:
26
+ text = text.rjust(width-2)
27
+
28
+ yield ('', text)
29
+
30
+
31
+ Sheet.addCommand('$', 'type-currency', 'cursorCol.type=currency', 'set type of current column to currency')
32
+
33
+ vd.addMenuItems('''
34
+ Column > Type as > dirty float > type-currency
35
+ ''')
visidata/type_date.py CHANGED
@@ -1,16 +1,33 @@
1
1
  import datetime
2
2
 
3
- from visidata import vd, Sheet
4
-
5
- try:
6
- from dateutil.parser import parse as date_parse
7
- except ImportError:
8
- def date_parse(r=''):
3
+ from visidata import VisiData, vd, Sheet
4
+
5
+ @VisiData.lazy_property
6
+ def date_parse(vd):
7
+ try:
8
+ from dateutil.parser import parse
9
+ return parse
10
+ except ImportError:
9
11
  vd.warning('install python-dateutil for date type')
10
- return r
12
+ return str
13
+
14
+ vd.help_date = '''
15
+ - RFC3339: `%Y-%m-%d %H:%M:%S.%f %z`
16
+ - `%A` Weekday as locale’s full name.
17
+ - `%w` Weekday as a decimal number, where 0 is Sunday and 6 is Saturday.
18
+ - `%d` Day of the month as a zero-padded decimal number.
19
+ - `%b` Month as locale’s abbreviated name.
20
+ - `%B` Month as locale’s full name.
21
+ - `%p` Locale’s equivalent of either AM or PM.
22
+ - `%c` Locale’s appropriate date and time representation.
23
+ - `%x` Locale’s appropriate date representation.
24
+ - `%X` Locale’s appropriate time representation.
25
+ - `%Z` Time zone name (empty string if the object is naive).
11
26
 
27
+ See [:onclick https://strftime.org]Python strftime()[/] for a full list of format codes.
28
+ '''
12
29
 
13
- vd.option('disp_date_fmt','%Y-%m-%d', 'default fmtstr to strftime for date values', replay=True)
30
+ vd.option('disp_date_fmt','%Y-%m-%d', 'default fmtstr passed to strftime for date values', replay=True, help=vd.help_date)
14
31
 
15
32
 
16
33
  @vd.numericType('@', '', formatter=lambda fmtstr,val: val.strftime(fmtstr or vd.options.disp_date_fmt))
@@ -28,7 +45,7 @@ class date(datetime.datetime):
28
45
  if isinstance(s, int) or isinstance(s, float):
29
46
  r = datetime.datetime.fromtimestamp(s)
30
47
  elif isinstance(s, str):
31
- r = date_parse(s)
48
+ r = vd.date_parse(s)
32
49
  elif isinstance(s, (datetime.datetime, datetime.date)):
33
50
  r = s
34
51
  else:
@@ -109,3 +126,8 @@ vd.addGlobals(
109
126
 
110
127
 
111
128
  Sheet.addCommand('@', 'type-date', 'cursorCol.type = date', 'set type of current column to date')
129
+ Sheet.addCommand('', 'type-datetime', 'cursorCol.type=date; cursorCol.fmtstr="%Y-%m-%d %H:%M:%S"', 'set type of current column to datetime')
130
+
131
+ vd.addMenuItems('''
132
+ Column > Type as > date > type-date
133
+ ''')
visidata/type_floatsi.py CHANGED
@@ -22,7 +22,7 @@ def floatsi(*args):
22
22
  if not args:
23
23
  return 0.0
24
24
  if not isinstance(args[0], str):
25
- return args[0]
25
+ return float(args[0])
26
26
 
27
27
  s=args[0].strip()
28
28
  for i, p in enumerate(vd.si_prefixes):
@@ -33,3 +33,7 @@ def floatsi(*args):
33
33
 
34
34
 
35
35
  Sheet.addCommand('z%', 'type-floatsi', 'cursorCol.type = floatsi', 'set type of current column to SI float')
36
+
37
+ vd.addMenuItems('''
38
+ Column > Type as > SI float > type-floatsi
39
+ ''')
visidata/undo.py CHANGED
@@ -18,13 +18,14 @@ def isUndoableCommand(longname):
18
18
  @VisiData.api
19
19
  def addUndo(vd, undofunc, *args, **kwargs):
20
20
  'On undo of latest command, call ``undofunc(*args, **kwargs)``.'
21
- if options.undo:
21
+ if vd.options.undo:
22
22
  # occurs when VisiData is just starting up
23
23
  if getattr(vd, 'activeCommand', UNLOADED) is UNLOADED:
24
24
  return
25
25
  r = vd.modifyCommand
26
26
  # some special commands, like open-file, do not have an undofuncs set
27
- if not r or not isUndoableCommand(r.longname):
27
+ # do not set undofuncs for non-logged commands
28
+ if not r or not isUndoableCommand(r.longname) or not vd.activeCommand or not vd.isLoggableCommand(vd.activeCommand.longname):
28
29
  return
29
30
  if not r.undofuncs:
30
31
  r.undofuncs = []
@@ -33,16 +34,17 @@ def addUndo(vd, undofunc, *args, **kwargs):
33
34
 
34
35
  @VisiData.api
35
36
  def undo(vd, sheet):
36
- if not options.undo:
37
+ if not vd.options.undo:
37
38
  vd.fail("options.undo not enabled")
38
39
 
39
40
  # don't allow undo of first command on a sheet, which is always the command that created the sheet.
40
- for cmdlogrow in sheet.cmdlog_sheet.rows[:0:-1]:
41
+ for i, cmdlogrow in enumerate(sheet.cmdlog_sheet.rows[:0:-1]):
41
42
  if cmdlogrow.undofuncs:
42
43
  for undofunc, args, kwargs, in cmdlogrow.undofuncs[::-1]:
43
44
  undofunc(*args, **kwargs)
44
45
  sheet.undone.append(cmdlogrow)
45
- sheet.cmdlog_sheet.rows.remove(cmdlogrow)
46
+ row_idx = len(sheet.cmdlog_sheet.rows)-1 - i
47
+ del sheet.cmdlog_sheet.rows[row_idx]
46
48
 
47
49
  vd.clearCaches() # undofunc can invalidate the drawcache
48
50
 
@@ -113,3 +115,13 @@ def addUndoColNames(vd, cols):
113
115
 
114
116
  BaseSheet.addCommand('U', 'undo-last', 'vd.undo(sheet)', 'Undo the most recent change (options.undo must be enabled)')
115
117
  BaseSheet.addCommand('R', 'redo-last', 'vd.redo(sheet)', 'Redo the most recent undo (options.undo must be enabled)')
118
+
119
+ vd.addGlobals(
120
+ undoAttrFunc=undoAttrFunc,
121
+ Fanout=Fanout,
122
+ undoAttrCopyFunc=undoAttrCopyFunc)
123
+
124
+ vd.addMenuItems('''
125
+ Edit > Undo > undo-last
126
+ Edit > Redo > redo-last
127
+ ''')
visidata/utils.py CHANGED
@@ -1,10 +1,11 @@
1
+ from contextlib import contextmanager
1
2
  import operator
2
3
  import string
3
4
  import re
4
5
 
5
6
  'Various helper classes and functions.'
6
7
 
7
- __all__ = ['AlwaysDict', 'AttrDict', 'moveListItem', 'namedlist', 'classproperty', 'cleanName', 'MissingAttrFormatter']
8
+ __all__ = ['AlwaysDict', 'AttrDict', 'DefaultAttrDict', 'moveListItem', 'namedlist', 'classproperty', 'MissingAttrFormatter', 'getitem', 'setitem', 'getitemdef', 'getitemdeep', 'setitemdeep', 'getattrdeep', 'setattrdeep', 'ExplodingMock', 'ScopedSetattr']
8
9
 
9
10
 
10
11
  class AlwaysDict(dict):
@@ -36,6 +37,24 @@ class AttrDict(dict):
36
37
  return self.keys()
37
38
 
38
39
 
40
+ class DefaultAttrDict(dict):
41
+ 'Augment a dict with more convenient .attr syntax. not-present keys store new DefaultAttrDict. like a recursive defaultdict.'
42
+ def __getattr__(self, k):
43
+ if k not in self:
44
+ if k.startswith("__"):
45
+ raise AttributeError from e
46
+ self[k] = DefaultAttrDict()
47
+ return self[k]
48
+
49
+ def __setattr__(self, k, v):
50
+ self[k] = v
51
+
52
+ def __dir__(self):
53
+ return self.keys()
54
+
55
+
56
+
57
+
39
58
  class classproperty(property):
40
59
  def __get__(self, cls, obj):
41
60
  return classmethod(self.fget).__get__(None, obj or cls)()
@@ -50,34 +69,75 @@ def moveListItem(L, fromidx, toidx):
50
69
  return toidx
51
70
 
52
71
 
53
- def cleanName(s):
54
- s = re.sub(r'[^\w\d_]', '_', s) # replace non-alphanum chars with _
55
- s = re.sub(r'_+', '_', s) # replace runs of _ with a single _
56
- s = s.strip('_')
57
- return s
72
+ def setitem(r, i, v): # function needed for use in lambda
73
+ r[i] = v
74
+ return True
75
+
76
+ def getitem(o, k, default=None):
77
+ return default if o is None else o[k]
78
+
79
+ def getitemdef(o, k, default=None):
80
+ try:
81
+ return default if o is None else o[k]
82
+ except Exception:
83
+ return default
84
+
58
85
 
86
+ def getattrdeep(obj, attr, *default, getter=getattr):
87
+ try:
88
+ 'Return dotted attr (like "a.b.c") from obj, or default if any of the components are missing.'
89
+ if not isinstance(attr, str):
90
+ return getter(obj, attr, *default)
91
+
92
+ try: # if attribute exists, return toplevel value, even if dotted
93
+ if attr in obj:
94
+ return getter(obj, attr)
95
+ except RecursionError: #1696
96
+ raise
97
+ except Exception as e:
98
+ pass
99
+
100
+ attrs = attr.split('.')
101
+ for a in attrs[:-1]:
102
+ obj = getter(obj, a)
103
+
104
+ return getter(obj, attrs[-1])
105
+ except Exception as e:
106
+ if not default: raise
107
+ return default[0]
59
108
 
60
- class OnExit:
61
- '"with OnExit(func, ...):" calls func(...) when the context is exited'
62
- def __init__(self, func, *args, **kwargs):
63
- self.func = func
64
- self.args = args
65
- self.kwargs = kwargs
66
109
 
67
- def __enter__(self):
68
- return self
110
+ def setattrdeep(obj, attr, val, getter=getattr, setter=setattr):
111
+ 'Set dotted attr (like "a.b.c") on obj to val.'
112
+ if not isinstance(attr, str):
113
+ return setter(obj, attr, val)
69
114
 
70
- def __exit__(self, exc_type, exc_value, exc_traceback):
115
+ try: # if attribute exists, overwrite toplevel value, even if dotted
116
+ getter(obj, attr)
117
+ return setter(obj, attr, val)
118
+ except Exception as e:
119
+ pass
120
+
121
+ attrs = attr.split('.')
122
+ for a in attrs[:-1]:
71
123
  try:
72
- self.func(*self.args, **self.kwargs)
124
+ obj = getter(obj, a)
73
125
  except Exception as e:
74
- vd.exceptionCaught(e)
126
+ obj = obj[a] = type(obj)() # assume homogeneous nesting
127
+
128
+ setter(obj, attrs[-1], val)
75
129
 
76
130
 
77
- def itemsetter(i):
78
- def g(obj, v):
79
- obj[i] = v
80
- return g
131
+ def getitemdeep(obj, k, *default):
132
+ if not isinstance(k, str):
133
+ try:
134
+ return obj[k]
135
+ except IndexError:
136
+ pass
137
+ return getattrdeep(obj, k, *default, getter=getitem)
138
+
139
+ def setitemdeep(obj, k, val):
140
+ return setattrdeep(obj, k, val, getter=getitemdef, setter=setitem)
81
141
 
82
142
 
83
143
  def namedlist(objname, fieldnames):
@@ -111,11 +171,24 @@ def namedlist(objname, fieldnames):
111
171
 
112
172
  return NamedListTemplate
113
173
 
174
+
175
+ class ExplodingMock:
176
+ 'A mock object that raises an exception for everything except conversion to True/False.'
177
+ def __init__(self, msg):
178
+ self.__msg = msg
179
+
180
+ def __getattr__(self, k):
181
+ raise Exception(self.__msg)
182
+
183
+ def __bool__(self):
184
+ return False
185
+
186
+
114
187
  class MissingAttrFormatter(string.Formatter):
115
188
  "formats {} fields with `''`, that would normally result in a raised KeyError or AttributeError; intended for user customisable format strings."
116
- def get_field(self, field_name, *args, **kwargs):
189
+ def get_field(self, field_name, args, kwargs):
117
190
  try:
118
- return super().get_field(field_name, *args, **kwargs)
191
+ return super().get_field(field_name, args, kwargs)
119
192
  except (KeyError, AttributeError):
120
193
  return (None, field_name)
121
194
 
@@ -126,3 +199,13 @@ class MissingAttrFormatter(string.Formatter):
126
199
  elif not value:
127
200
  return str(value)
128
201
  return super().format_field(value, format_spec)
202
+
203
+
204
+ @contextmanager
205
+ def ScopedSetattr(obj, attrname, val):
206
+ oldval = getattr(obj, attrname)
207
+ try:
208
+ setattr(obj, attrname, val)
209
+ yield
210
+ finally:
211
+ setattr(obj, attrname, oldval)