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
visidata/pivot.py CHANGED
@@ -1,6 +1,8 @@
1
1
  import collections
2
2
  from copy import copy
3
- from visidata import *
3
+ from visidata import ScopedSetattr, Column, Sheet, asyncthread, Progress, forward, wrapply, INPROGRESS
4
+ from visidata import vlen, vd, date, setitem, anytype
5
+ import visidata
4
6
 
5
7
 
6
8
  # discrete_keys = tuple of formatted discrete keys that group the row
@@ -9,10 +11,10 @@ from visidata import *
9
11
  # pivotrows is { pivot_values: list(source.rows in group with pivot_values) }
10
12
  PivotGroupRow = collections.namedtuple('PivotGroupRow', 'discrete_keys numeric_key sourcerows pivotrows'.split())
11
13
 
12
- def Pivot(source, groupByCols, pivotCols):
14
+ def makePivot(source, groupByCols, pivotCols):
13
15
  return PivotSheet('',
14
- groupByCols,
15
- pivotCols,
16
+ groupByCols=groupByCols,
17
+ pivotCols=pivotCols,
16
18
  source=source)
17
19
 
18
20
  def makeErrorKey(col):
@@ -24,6 +26,8 @@ def makeErrorKey(col):
24
26
  def formatRange(col, numeric_key):
25
27
  a, b = numeric_key
26
28
  nankey = makeErrorKey(col)
29
+ if b is None:
30
+ return a
27
31
  if a is nankey and b is nankey:
28
32
  return '#ERR'
29
33
  elif a == b:
@@ -45,31 +49,38 @@ class RangeColumn(Column):
45
49
  return formatRange(self.origcol, typedval)
46
50
 
47
51
 
48
- def AggrColumn(aggcol, aggregator):
52
+ class AggrColumn(Column):
53
+ def calcValue(col, row):
54
+ if col.sheet.loading:
55
+ return visidata.INPROGRESS
56
+ return col.aggregator(col.origCol, row.sourcerows)
57
+
58
+
59
+ def makeAggrColumn(aggcol, aggregator):
49
60
  aggname = '%s_%s' % (aggcol.name, aggregator.name)
50
61
 
51
- return Column(aggname,
62
+ return AggrColumn(aggname,
52
63
  type=aggregator.type or aggcol.type,
53
64
  fmtstr=aggcol.fmtstr,
54
- getter=lambda col,row,agg=aggregator: agg(col.origCol, row.sourcerows),
55
65
  origCol=aggcol,
56
- )
66
+ aggregator=aggregator)
57
67
 
58
68
 
59
69
  class PivotSheet(Sheet):
60
70
  'Summarize key columns in pivot table and display as new sheet.'
61
71
  rowtype = 'grouped rows' # rowdef: PivotGroupRow
62
- def __init__(self, name, groupByCols, pivotCols, **kwargs):
63
- super().__init__(name, **kwargs)
64
72
 
65
- self.pivotCols = pivotCols # whose values become columns
66
- self.groupByCols = groupByCols # whose values become rows
73
+ def __init__(self, *names, groupByCols=[], pivotCols=[], **kwargs):
74
+ super().__init__(*names,
75
+ pivotCols=pivotCols, # whose values become columns
76
+ groupByCols=groupByCols, # whose values become rows
77
+ **kwargs)
67
78
 
68
79
  def isNumericRange(self, col):
69
80
  return vd.isNumeric(col) and self.source.options.numeric_binning
70
81
 
71
- def initCols(self):
72
- self.columns = []
82
+ def resetCols(self):
83
+ super().resetCols()
73
84
 
74
85
  # add key columns (grouped by)
75
86
  for colnum, c in enumerate(self.groupByCols):
@@ -103,12 +114,10 @@ class PivotSheet(Sheet):
103
114
  vs.rows = row.pivotrows.get(col.aggvalue, [])
104
115
  return vs
105
116
 
106
- def reload(self):
107
- self.initCols()
108
-
117
+ def loader(self):
109
118
  # two different threads for better interactive display
110
- self.addAggregateCols()
111
- self.groupRows()
119
+ vd.sync(self.addAggregateCols(),
120
+ self.groupRows())
112
121
 
113
122
  @asyncthread
114
123
  def addAggregateCols(self):
@@ -130,7 +139,7 @@ class PivotSheet(Sheet):
130
139
  if not self.pivotCols:
131
140
  for aggcol, aggregatorlist in aggcols.items():
132
141
  for aggregator in aggregatorlist:
133
- c = AggrColumn(aggcol, aggregator)
142
+ c = makeAggrColumn(aggcol, aggregator)
134
143
  self.addColumn(c)
135
144
 
136
145
  # add pivoted columns
@@ -177,6 +186,7 @@ class PivotSheet(Sheet):
177
186
 
178
187
  @asyncthread
179
188
  def groupRows(self, rowfunc=None):
189
+ with ScopedSetattr(self, 'loading', True):
180
190
  self.rows = []
181
191
 
182
192
  discreteCols = [c for c in self.groupByCols if not self.isNumericRange(c)]
@@ -191,13 +201,16 @@ class PivotSheet(Sheet):
191
201
  if numericCols:
192
202
  nbins = self.source.options.histogram_bins or int(len(self.source.rows) ** (1./2))
193
203
  vals = tuple(numericCols[0].getValues(self.source.rows))
194
- minval = min(vals)
195
- maxval = max(vals)
204
+ minval = min(vals) if vals else 0
205
+ maxval = max(vals) if vals else 0
196
206
  width = (maxval - minval)/nbins
197
207
 
198
208
  if width == 0:
199
- # only one value (and maybe errors)
200
- numericBins = [(minval, maxval)]
209
+ if vals:
210
+ # only one value
211
+ numericBins = [(minval, maxval)]
212
+ else:
213
+ numericBins = []
201
214
  elif (numericCols[0].type in (int, vlen) and nbins > (maxval - minval)) or (width == 1):
202
215
  # (more bins than int vals) or (if bins are of width 1), just use the vals as bins
203
216
  degenerateBinning = True
@@ -227,25 +240,29 @@ class PivotSheet(Sheet):
227
240
  if numericCols:
228
241
  try:
229
242
  val = numericCols[0].getValue(sourcerow)
230
- if val is not None:
231
- val = numericCols[0].type(val)
232
- if not width:
233
- binidx = 0
234
- elif degenerateBinning:
235
- # in degenerate binning, each val has its own bin
236
- binidx = numericBins.index((val, val))
243
+ val = wrapply(numericCols[0].type, val)
244
+ if not val:
245
+ groupRow = numericGroupRows.get(str(val), None)
237
246
  else:
238
- binidx = int((val-minval)//width)
239
- groupRow = numericGroupRows[formatRange(numericCols[0], numericBins[min(binidx, nbins-1)])]
247
+ if not width:
248
+ binidx = 0
249
+ elif degenerateBinning:
250
+ # in degenerate binning, each val has its own bin
251
+ binidx = numericBins.index((val, val))
252
+ else:
253
+ binidx = int((val-minval)//width)
254
+ groupRow = numericGroupRows[formatRange(numericCols[0], numericBins[min(binidx, nbins-1)])]
240
255
  except Exception as e:
241
- # leave in main/error bin
242
- pass
256
+ vd.exceptionCaught(e)
243
257
 
244
258
  # add the main bin if no numeric bin (error, or no numeric cols)
245
259
  if groupRow is None:
246
- nankey = makeErrorKey(numericCols[0]) if numericCols else 0
247
- groupRow = PivotGroupRow(discreteKeys, (nankey, nankey), [], {})
248
- groups[formattedDiscreteKeys] = (numericGroupRows, groupRow)
260
+ if numericCols:
261
+ groupRow = PivotGroupRow(discreteKeys, val, [], {})
262
+ numericGroupRows[str(val)] = groupRow
263
+ else:
264
+ groupRow = PivotGroupRow(discreteKeys, (0, 0), [], {})
265
+ groups[formattedDiscreteKeys] = (numericGroupRows, groupRow)
249
266
  self.addRow(groupRow)
250
267
 
251
268
  # add the sourcerow to its all bin
@@ -262,24 +279,33 @@ class PivotSheet(Sheet):
262
279
  if rowfunc:
263
280
  rowfunc(groupRow)
264
281
 
282
+ def afterLoad(self):
283
+ super().afterLoad()
284
+
265
285
  # automatically add cache to all columns now that everything is binned
266
286
  for c in self.nonKeyVisibleCols:
267
- c.setCache(True)
287
+ if isinstance(c, AggrColumn):
288
+ c.setCache(True)
268
289
 
269
290
 
270
291
  @PivotSheet.api
271
292
  def addcol_aggr(sheet, col):
272
293
  hasattr(col, 'origCol') or vd.fail('not an aggregation column')
273
- for agg in vd.chooseMany(vd.aggregator_choices):
274
- sheet.addColumnAtCursor(AggrColumn(col.origCol, vd.aggregators[agg]))
294
+ for agg in vd.chooseAggregators():
295
+ sheet.addColumnAtCursor(makeAggrColumn(col.origCol, vd.aggregators[agg]))
275
296
 
276
297
 
277
- Sheet.addCommand('W', 'pivot', 'vd.push(Pivot(sheet, keyCols, [cursorCol]))', 'open Pivot Table: group rows by key column and summarize current column')
298
+ Sheet.addCommand('W', 'pivot', 'vd.push(makePivot(sheet, keyCols, [cursorCol]))', 'open Pivot Table: group rows by key column and summarize current column')
278
299
 
279
300
  PivotSheet.addCommand('', 'addcol-aggr', 'addcol_aggr(cursorCol)', 'add aggregation column from source of current column')
280
- vd.addMenuItem('Column', 'Add column', 'aggregator', 'addcol-aggr')
281
301
 
282
- vd.addGlobals({
283
- 'Pivot': Pivot,
284
- 'PivotGroupRow': PivotGroupRow,
285
- })
302
+ vd.addGlobals(
303
+ makePivot=makePivot,
304
+ PivotSheet=PivotSheet,
305
+ PivotGroupRow=PivotGroupRow,
306
+ )
307
+
308
+ vd.addMenuItems('''
309
+ Column > Add column > aggregator > addcol-aggr
310
+ Data > Pivot > pivot
311
+ ''')
visidata/plugins.py CHANGED
@@ -5,225 +5,98 @@ import re
5
5
  import shutil
6
6
  import importlib
7
7
  import subprocess
8
- import urllib
8
+ import urllib.error
9
9
 
10
- from visidata import VisiData, vd, Path, CellColorizer, JsonLinesSheet, AttrDict, Column, Progress, ExpectedException, BaseSheet, asyncsingle, asyncthread
10
+ from visidata import VisiData, vd, Path, CellColorizer, Sheet, AttrDict, ItemColumn, Column, Progress, ExpectedException, BaseSheet, asyncthread
11
11
 
12
12
 
13
- vd.option('plugins_url', 'https://visidata.org/plugins/plugins.jsonl', 'source of plugins sheet')
14
13
  vd.option('plugins_autoload', True, 'do not autoload plugins if False')
15
14
 
16
15
 
17
- @VisiData.lazy_property
18
- def pluginsSheet(p):
19
- return PluginsSheet('plugins_global')
20
-
21
- def _plugin_path(plugin):
22
- return Path(os.path.join(vd.options.visidata_dir, "plugins", plugin.name+".py"))
23
-
24
- def _plugin_init():
16
+ @VisiData.property
17
+ def pluginConfig(self):
25
18
  return Path(os.path.join(vd.options.visidata_dir, "plugins", "__init__.py"))
26
19
 
27
- def _plugin_import(plugin):
28
- return "import " + _plugin_import_name(plugin)
29
20
 
30
- def _plugin_import_name(plugin):
21
+ @VisiData.property
22
+ def pluginConfigLines(self):
23
+ return Path(self.pluginConfig).open(mode='r', encoding='utf-8').readlines()
24
+
25
+ def _plugin_import_name(self, plugin):
31
26
  if not plugin.url:
32
27
  return 'visidata.plugins.'+plugin.name
33
28
  if 'git+' in plugin.url:
34
29
  return plugin.name
35
30
  return "plugins."+plugin.name
36
31
 
37
- def _plugin_in_import_list(plugin):
38
- with Path(_plugin_init()).open_text(mode='r', encoding='utf-8') as fprc:
39
- r = re.compile(r'^{}\W'.format(_plugin_import(plugin)))
40
- for line in fprc.readlines():
41
- if r.match(line):
42
- return True
43
32
 
44
- def _installedStatus(col, plugin):
45
- return '*' if importlib.util.find_spec(_plugin_import_name(plugin)) else ''
33
+ @VisiData.api
34
+ def enablePlugin(vd, plugin:str):
35
+ with vd.pluginConfig.open(mode='a', encoding='utf-8') as fprc:
36
+ print(f'import {plugin}', file=fprc)
37
+ importlib.import_module(plugin)
38
+ vd.status(f'{plugin} plugin enabled')
46
39
 
47
- def _loadedVersion(plugin):
48
- name = _plugin_import_name(plugin)
49
- if name not in sys.modules:
50
- return ''
51
- mod = sys.modules[name]
52
- return getattr(mod, '__version__', 'unknown version installed')
40
+ @VisiData.api
41
+ def removePlugin(vd, plugin:str):
42
+ path = vd.pluginConfig
43
+ pathbackup = path.with_suffix(path.suffix + '.bak')
44
+ try:
45
+ shutil.copyfile(path, pathbackup)
46
+
47
+ # Copy lines from the backup init file into its replacement, skipping lines that import the removed plugin.
48
+ #
49
+ # By matching from the start of a line through a word boundary, we avoid removing commented lines or inadvertently removing
50
+ # plugins with similar names.
51
+
52
+ r = re.compile(f'^import {plugin}\\W')
53
+ nonimports = [line for line in vd.pluginConfigLines if not r.match(line)]
54
+ if len(nonimports) == len(vd.pluginConfigLines):
55
+ vd.fail("plugin not in import list")
53
56
 
54
- def _checkHash(data, sha):
55
- import hashlib
56
- return hashlib.sha256(data.strip().encode('utf-8')).hexdigest() == sha
57
+ with path.open(mode='w', encoding='utf-8') as new:
58
+ new.writelines(nonimports)
57
59
 
58
- def _pluginColorizer(s,c,r,v):
59
- if not r: return None
60
- ver = _loadedVersion(r)
61
- if not ver: return None
62
- if not r.latest_ver: return None
63
- if ver != r.latest_ver: return 'color_warning'
64
- return 'color_working'
60
+ sys.modules.pop(plugin)
61
+ importlib.invalidate_caches()
62
+ vd.warning(f'"{plugin}" plugin removed')
63
+ except FileNotFoundError:
64
+ vd.debug("no {vd.pluginConfig} found")
65
65
 
66
66
 
67
- @VisiData.api
68
- def pipinstall(vd, deps):
69
- 'Install *deps*, a list of pypi modules to install via pip into the plugins-deps directory. Return True if successful (no error).'
70
- p = subprocess.Popen([sys.executable, '-m', 'pip', 'install',
71
- '--target', str(Path(vd.options.visidata_dir)/"plugins-deps"),
72
- ] + deps,
73
- stdout=subprocess.PIPE,
74
- stderr=subprocess.PIPE)
75
- out, err = p.communicate()
76
- vd.status(out.decode())
77
- if err:
78
- vd.warning(err.decode())
79
- return False
80
- return True
81
-
82
-
83
- class PluginsSheet(JsonLinesSheet):
67
+ @VisiData.lazy_property
68
+ def pluginsSheet(p):
69
+ return PluginsSheet('plugins')
70
+
71
+
72
+ class PluginsSheet(Sheet):
84
73
  rowtype = "plugins" # rowdef: AttrDict of json dict
85
74
  colorizers = [
86
- CellColorizer(3, None, _pluginColorizer)
75
+ CellColorizer(3, 'color_working', lambda s,c,r,v: r and r.installed)
87
76
  ]
88
-
77
+ columns = [
78
+ ItemColumn('name'),
79
+ ItemColumn('installed', width=8),
80
+ ItemColumn('description', width=60),
81
+ ]
82
+ nKeys = 1
89
83
  def iterload(self):
90
- for r in JsonLinesSheet.iterload(self):
91
- yield AttrDict(r)
92
-
93
- @asyncsingle
94
- def reload(self):
95
- try:
96
- self.source = vd.urlcache(vd.options.plugins_url or vd.fail(), days=1) # for VisiDataMetaSheet.reload()
97
- except urllib.error.URLError as e:
98
- vd.debug(e)
99
- return
100
-
101
- super().reload.__wrapped__(self)
102
- self.addColumn(Column('available', width=0, getter=_installedStatus), index=1)
103
- self.addColumn(Column('installed', width=8, getter=lambda c,r: _loadedVersion(r)), index=2)
104
- self.column('description').width = 40
105
- self.setKeys([self.column("name")])
106
-
84
+ import pkgutil
85
+ import ast
86
+ # enumerate installed plugins
107
87
  for name, mod in sys.modules.items():
108
- if name.startswith('visidata.plugins.'):
109
- self.addRow(AttrDict(name='.'.join(name.split('.')[2:]),
110
- description=getattr(mod, '__description__', mod.__doc__),
111
- maintainer=getattr(mod, '__author__', None),
112
- latest_release='',
113
- latest_ver='',
114
- url=''
115
- ))
116
-
117
- for r in Progress(self.rows):
118
- for funcname in (r.provides or '').split():
119
- func = lambda *args, **kwargs: vd.fail('this requires the %s plugin' % r.name)
120
- vd.addGlobals({funcname: func})
121
- setattr(vd, funcname, func)
122
-
123
- # check for plugins with newer versions
124
- def is_stale(r):
125
- v = _loadedVersion(r)
126
- return v and r.latest_ver and v != r.latest_ver
127
-
128
- stale_plugins = list(filter(is_stale, self.rows))
129
- if len(stale_plugins) > 0:
130
- vd.warning(f'update available for {len(stale_plugins)} plugins')
131
-
132
- def installPlugin(self, plugin):
133
- # pip3 install requirements
134
- initpath = _plugin_init()
135
- os.makedirs(initpath.parent, exist_ok=True)
136
- if not initpath.exists():
137
- initpath.touch()
138
-
139
- outpath = _plugin_path(plugin)
140
- overwrite = True
141
- if outpath.exists():
142
- try:
143
- vd.confirm("plugin path already exists, overwrite? ")
144
- except ExpectedException:
145
- overwrite = False
146
- if _plugin_in_import_list(plugin):
147
- vd.fail("plugin already loaded")
148
- else:
149
- self._loadPlugin(plugin)
150
- if overwrite:
151
- self._install(plugin)
152
-
153
- @asyncthread
154
- def _install(self, plugin):
155
- outpath = _plugin_path(plugin)
156
-
157
- if "git+" in plugin.url:
158
- p = subprocess.Popen([sys.executable, '-m', 'pip', 'install',
159
- '--upgrade', plugin.url],
160
- stdout=subprocess.PIPE,
161
- stderr=subprocess.PIPE)
162
- out, err = p.communicate()
163
- vd.status(out.decode())
164
- if err:
165
- vd.warning(err.decode())
166
- if p.returncode != 0:
167
- vd.fail('pip install failed')
168
- else:
169
- with vd.urlcache(plugin.url, days=0).open_text(encoding='utf-8') as pyfp:
170
- contents = pyfp.read()
171
- if plugin.sha256:
172
- if not _checkHash(contents, plugin.sha256):
173
- vd.error('%s plugin SHA256 does not match!' % plugin.name)
174
- else:
175
- vd.warning('no SHA256 provided for %s plugin, not validating' % plugin.name)
176
- with outpath.open_text(mode='w', encoding='utf-8') as outfp:
177
- outfp.write(contents)
178
-
179
- if plugin.pydeps:
180
- vd.pipinstall(plugin.pydeps.split())
181
-
182
- vd.status('%s plugin installed' % plugin.name)
183
-
184
- if _plugin_in_import_list(plugin):
185
- vd.warning("plugin already loaded")
186
- else:
187
- self._loadPlugin(plugin)
188
-
189
-
190
- def _loadPlugin(self, plugin):
191
- with Path(_plugin_init()).open_text(mode='a', encoding='utf-8') as fprc:
192
- print(_plugin_import(plugin), file=fprc)
193
- importlib.import_module(_plugin_import_name(plugin))
194
- vd.status('%s plugin loaded' % plugin.name)
195
-
196
-
197
- def removePluginIfExists(self, plugin):
198
- self.removePlugin(plugin)
199
-
200
- def removePlugin(self, plugin):
201
- if not _plugin_in_import_list(plugin):
202
- vd.fail("plugin not in import list")
203
-
204
- initpath = Path(_plugin_init())
205
- oldinitpath = Path(initpath.with_suffix(initpath.suffix + '.bak'))
206
- try:
207
- shutil.copyfile(initpath, oldinitpath)
208
-
209
- # Copy lines from the backup init file into its replacement, skipping lines that import the removed plugin.
210
- #
211
- # By matching from the start of a line through a word boundary, we avoid removing commented lines or inadvertently removing
212
- # plugins with similar names.
213
- with oldinitpath.open_text(encoding='utf-8') as old, initpath.open_text(mode='w', encoding='utf-8') as new:
214
- r = re.compile(r'^{}\W'.format(_plugin_import(plugin)))
215
- new.writelines(line for line in old.readlines() if not r.match(line))
216
-
217
- if os.path.exists(_plugin_path(plugin)):
218
- os.unlink(_plugin_path(plugin))
219
- sys.modules.pop(_plugin_import_name(plugin))
220
- importlib.invalidate_caches()
221
- vd.warning('{0} plugin uninstalled'.format(plugin['name']))
222
- except FileNotFoundError:
223
- vd.warning("no plugins/__init__.py found")
88
+ if name.startswith(('visidata.plugins.', 'visidata.experimental.')):
89
+ yield AttrDict(name=name, # '.'.join(name.split('.')[2:]),
90
+ description=getattr(mod, '__description__', mod.__doc__),
91
+ installed=getattr(mod, '__version__', 'yes'),
92
+ maintainer=getattr(mod, '__author__', None))
224
93
 
225
94
 
226
95
  BaseSheet.addCommand(None, 'open-plugins', 'vd.push(vd.pluginsSheet)', 'Open Plugins Sheet to manage supported plugins')
227
96
 
228
- PluginsSheet.addCommand('a', 'add-plugin', 'installPlugin(cursorRow)', 'Install and enable current plugin')
229
- PluginsSheet.addCommand('d', 'delete-plugin', 'removePluginIfExists(cursorRow)', 'Disable current plugin')
97
+ PluginsSheet.addCommand('a', 'add-plugin', 'enablePlugin(cursorRow.name); reload_rows()', 'Enable current plugin by adding to imports')
98
+ PluginsSheet.addCommand('d', 'delete-plugin', 'removePlugin(cursorRow.name); reload_rows()', 'Disable current plugin by removing from imports')
99
+
100
+ vd.addMenuItems('''
101
+ System > Plugins Sheet > open-plugins
102
+ ''')