visidata 2.11.dev0__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 (253) hide show
  1. visidata/__init__.py +72 -91
  2. visidata/_input.py +263 -44
  3. visidata/_open.py +84 -29
  4. visidata/_types.py +22 -4
  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. visidata/apps/vgit/__main__.py +3 -0
  18. visidata/apps/vgit/abort.py +23 -0
  19. visidata/apps/vgit/blame.py +76 -0
  20. visidata/apps/vgit/branch.py +153 -0
  21. visidata/apps/vgit/config.py +95 -0
  22. visidata/apps/vgit/diff.py +169 -0
  23. visidata/apps/vgit/gitsheet.py +161 -0
  24. visidata/apps/vgit/grep.py +37 -0
  25. visidata/apps/vgit/log.py +81 -0
  26. visidata/apps/vgit/main.py +55 -0
  27. visidata/apps/vgit/remote.py +57 -0
  28. visidata/apps/vgit/repos.py +71 -0
  29. visidata/apps/vgit/setup.py +37 -0
  30. visidata/apps/vgit/stash.py +69 -0
  31. visidata/apps/vgit/status.py +204 -0
  32. visidata/apps/vgit/statusbar.py +34 -0
  33. visidata/basesheet.py +59 -50
  34. visidata/canvas.py +251 -99
  35. visidata/choose.py +15 -11
  36. visidata/clean_names.py +29 -0
  37. visidata/clipboard.py +84 -18
  38. visidata/cliptext.py +220 -46
  39. visidata/cmdlog.py +89 -114
  40. visidata/color.py +142 -56
  41. visidata/column.py +134 -131
  42. visidata/ddw/input.ddw +74 -79
  43. visidata/ddw/regex.ddw +57 -0
  44. visidata/ddwplay.py +33 -14
  45. visidata/deprecated.py +77 -3
  46. visidata/desktop/visidata.desktop +7 -0
  47. visidata/editor.py +12 -6
  48. visidata/errors.py +5 -1
  49. visidata/experimental/__init__.py +0 -0
  50. visidata/experimental/diff_sheet.py +29 -0
  51. visidata/experimental/digit_autoedit.py +6 -0
  52. visidata/experimental/gdrive.py +89 -0
  53. visidata/experimental/google.py +37 -0
  54. visidata/experimental/gsheets.py +79 -0
  55. visidata/experimental/live_search.py +37 -0
  56. visidata/experimental/liveupdate.py +45 -0
  57. visidata/experimental/mark.py +133 -0
  58. visidata/experimental/noahs_tapestry/__init__.py +1 -0
  59. visidata/experimental/noahs_tapestry/tapestry.py +147 -0
  60. visidata/experimental/rownum.py +73 -0
  61. visidata/experimental/slide_cells.py +26 -0
  62. visidata/expr.py +8 -4
  63. visidata/extensible.py +32 -6
  64. visidata/features/__init__.py +0 -0
  65. visidata/features/addcol_audiometadata.py +42 -0
  66. visidata/features/addcol_histogram.py +34 -0
  67. visidata/features/canvas_save_svg.py +69 -0
  68. visidata/features/change_precision.py +46 -0
  69. visidata/features/cmdpalette.py +163 -0
  70. visidata/features/colorbrewer.py +363 -0
  71. visidata/{colorsheet.py → features/colorsheet.py} +17 -16
  72. visidata/features/command_server.py +105 -0
  73. visidata/features/currency_to_usd.py +70 -0
  74. visidata/{customdate.py → features/customdate.py} +2 -0
  75. visidata/features/dedupe.py +132 -0
  76. visidata/{describe.py → features/describe.py} +17 -15
  77. visidata/features/errors_guide.py +26 -0
  78. visidata/features/expand_cols.py +202 -0
  79. visidata/{fill.py → features/fill.py} +4 -2
  80. visidata/{freeze.py → features/freeze.py} +11 -6
  81. visidata/features/graph_seaborn.py +79 -0
  82. visidata/features/helloworld.py +10 -0
  83. visidata/features/hint_types.py +17 -0
  84. visidata/{incr.py → features/incr.py} +5 -0
  85. visidata/{join.py → features/join.py} +107 -53
  86. visidata/features/known_cols.py +21 -0
  87. visidata/features/layout.py +62 -0
  88. visidata/{melt.py → features/melt.py} +33 -21
  89. visidata/features/normcol.py +118 -0
  90. visidata/features/open_config.py +7 -0
  91. visidata/features/open_syspaste.py +18 -0
  92. visidata/features/ping.py +157 -0
  93. visidata/features/procmgr.py +208 -0
  94. visidata/features/random_sample.py +6 -0
  95. visidata/{regex.py → features/regex.py} +47 -31
  96. visidata/features/reload_every.py +55 -0
  97. visidata/features/rename_col_cascade.py +30 -0
  98. visidata/features/scroll_context.py +60 -0
  99. visidata/features/select_equal_selected.py +11 -0
  100. visidata/features/setcol_fake.py +65 -0
  101. visidata/{slide.py → features/slide.py} +75 -21
  102. visidata/features/sparkline.py +48 -0
  103. visidata/features/status_source.py +20 -0
  104. visidata/{sysedit.py → features/sysedit.py} +2 -1
  105. visidata/features/sysopen_mailcap.py +46 -0
  106. visidata/features/term_extras.py +13 -0
  107. visidata/{transpose.py → features/transpose.py} +5 -4
  108. visidata/features/type_ipaddr.py +73 -0
  109. visidata/features/type_url.py +11 -0
  110. visidata/{unfurl.py → features/unfurl.py} +9 -9
  111. visidata/{window.py → features/window.py} +2 -2
  112. visidata/form.py +50 -21
  113. visidata/freqtbl.py +81 -33
  114. visidata/fuzzymatch.py +414 -0
  115. visidata/graph.py +105 -33
  116. visidata/guide.py +180 -0
  117. visidata/help.py +75 -44
  118. visidata/hint.py +39 -0
  119. visidata/indexsheet.py +109 -0
  120. visidata/input_history.py +55 -0
  121. visidata/interface.py +58 -0
  122. visidata/keys.py +17 -16
  123. visidata/loaders/__init__.py +9 -0
  124. visidata/loaders/_pandas.py +61 -21
  125. visidata/loaders/api_airtable.py +70 -0
  126. visidata/loaders/api_bitio.py +102 -0
  127. visidata/loaders/api_matrix.py +148 -0
  128. visidata/loaders/api_reddit.py +306 -0
  129. visidata/loaders/api_zulip.py +249 -0
  130. visidata/loaders/archive.py +41 -7
  131. visidata/loaders/arrow.py +7 -7
  132. visidata/loaders/conll.py +49 -0
  133. visidata/loaders/csv.py +25 -7
  134. visidata/loaders/eml.py +3 -4
  135. visidata/loaders/f5log.py +1204 -0
  136. visidata/loaders/fec.py +325 -0
  137. visidata/loaders/fixed_width.py +3 -5
  138. visidata/loaders/frictionless.py +3 -3
  139. visidata/loaders/geojson.py +8 -5
  140. visidata/loaders/google.py +48 -0
  141. visidata/loaders/graphviz.py +4 -4
  142. visidata/loaders/hdf5.py +4 -4
  143. visidata/loaders/html.py +48 -10
  144. visidata/loaders/http.py +84 -30
  145. visidata/loaders/imap.py +20 -10
  146. visidata/loaders/jrnl.py +52 -0
  147. visidata/loaders/json.py +83 -29
  148. visidata/loaders/jsonla.py +74 -0
  149. visidata/loaders/lsv.py +15 -11
  150. visidata/loaders/mailbox.py +40 -0
  151. visidata/loaders/markdown.py +1 -3
  152. visidata/loaders/mbtiles.py +4 -5
  153. visidata/loaders/mysql.py +11 -13
  154. visidata/loaders/npy.py +7 -7
  155. visidata/loaders/odf.py +4 -1
  156. visidata/loaders/orgmode.py +428 -0
  157. visidata/loaders/pandas_freqtbl.py +14 -20
  158. visidata/loaders/parquet.py +62 -6
  159. visidata/loaders/pcap.py +3 -3
  160. visidata/loaders/pdf.py +4 -3
  161. visidata/loaders/png.py +19 -13
  162. visidata/loaders/postgres.py +9 -8
  163. visidata/loaders/rec.py +7 -3
  164. visidata/loaders/s3.py +342 -0
  165. visidata/loaders/sas.py +5 -5
  166. visidata/loaders/scrape.py +186 -0
  167. visidata/loaders/shp.py +6 -5
  168. visidata/loaders/spss.py +5 -6
  169. visidata/loaders/sqlite.py +68 -28
  170. visidata/loaders/texttables.py +1 -1
  171. visidata/loaders/toml.py +60 -0
  172. visidata/loaders/tsv.py +61 -19
  173. visidata/loaders/ttf.py +19 -7
  174. visidata/loaders/unzip_http.py +6 -5
  175. visidata/loaders/usv.py +1 -1
  176. visidata/loaders/vcf.py +16 -16
  177. visidata/loaders/vds.py +10 -7
  178. visidata/loaders/vdx.py +30 -5
  179. visidata/loaders/xlsb.py +8 -1
  180. visidata/loaders/xlsx.py +145 -25
  181. visidata/loaders/xml.py +6 -3
  182. visidata/loaders/xword.py +4 -4
  183. visidata/loaders/yaml.py +15 -5
  184. visidata/macos.py +1 -1
  185. visidata/macros.py +130 -41
  186. visidata/main.py +119 -94
  187. visidata/mainloop.py +101 -154
  188. visidata/man/parse_options.py +2 -2
  189. visidata/man/vd.1 +302 -147
  190. visidata/man/vd.txt +291 -151
  191. visidata/memory.py +3 -3
  192. visidata/menu.py +104 -423
  193. visidata/metasheets.py +59 -141
  194. visidata/modify.py +79 -23
  195. visidata/motd.py +3 -3
  196. visidata/mouse.py +137 -0
  197. visidata/movement.py +43 -35
  198. visidata/optionssheet.py +99 -0
  199. visidata/path.py +131 -43
  200. visidata/pivot.py +74 -47
  201. visidata/plugins.py +65 -192
  202. visidata/pyobj.py +50 -201
  203. visidata/rename_col.py +20 -0
  204. visidata/save.py +42 -20
  205. visidata/search.py +54 -10
  206. visidata/selection.py +84 -5
  207. visidata/settings.py +162 -24
  208. visidata/sheets.py +229 -257
  209. visidata/shell.py +51 -21
  210. visidata/sidebar.py +162 -0
  211. visidata/sort.py +11 -4
  212. visidata/statusbar.py +113 -104
  213. visidata/stored_list.py +43 -0
  214. visidata/stored_prop.py +38 -0
  215. visidata/tests/conftest.py +3 -3
  216. visidata/tests/test_cliptext.py +39 -0
  217. visidata/tests/test_commands.py +62 -7
  218. visidata/tests/test_edittext.py +2 -2
  219. visidata/tests/test_features.py +17 -0
  220. visidata/tests/test_menu.py +14 -0
  221. visidata/tests/test_path.py +13 -4
  222. visidata/text_source.py +53 -0
  223. visidata/textsheet.py +10 -3
  224. visidata/theme.py +44 -0
  225. visidata/themes/__init__.py +0 -0
  226. visidata/themes/ascii8.py +84 -0
  227. visidata/themes/asciimono.py +84 -0
  228. visidata/themes/light.py +17 -0
  229. visidata/threads.py +87 -39
  230. visidata/tuiwin.py +22 -0
  231. visidata/type_currency.py +22 -3
  232. visidata/type_date.py +31 -9
  233. visidata/type_floatsi.py +5 -1
  234. visidata/undo.py +18 -6
  235. visidata/utils.py +106 -23
  236. visidata/vdobj.py +28 -17
  237. visidata/windows.py +10 -0
  238. visidata/wrappers.py +9 -3
  239. visidata-3.0.data/data/share/applications/visidata.desktop +7 -0
  240. {visidata-2.11.dev0.data → visidata-3.0.data}/data/share/man/man1/vd.1 +302 -147
  241. {visidata-2.11.dev0.data → visidata-3.0.data}/data/share/man/man1/visidata.1 +302 -147
  242. visidata-3.0.data/scripts/vd2to3.vdx +9 -0
  243. {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/METADATA +13 -11
  244. visidata-3.0.dist-info/RECORD +257 -0
  245. {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/WHEEL +1 -1
  246. {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/entry_points.txt +0 -1
  247. visidata/layout.py +0 -44
  248. visidata/misc.py +0 -5
  249. visidata-2.11.dev0.dist-info/RECORD +0 -142
  250. /visidata/{repeat.py → features/repeat.py} +0 -0
  251. {visidata-2.11.dev0.data → visidata-3.0.data}/scripts/vd +0 -0
  252. {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/LICENSE.gpl3 +0 -0
  253. {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/top_level.txt +0 -0
visidata/pivot.py CHANGED
@@ -1,5 +1,8 @@
1
1
  import collections
2
- from visidata import *
2
+ from copy import copy
3
+ from visidata import ScopedSetattr, Column, Sheet, asyncthread, Progress, forward, wrapply, INPROGRESS
4
+ from visidata import vlen, vd, date, setitem, anytype
5
+ import visidata
3
6
 
4
7
 
5
8
  # discrete_keys = tuple of formatted discrete keys that group the row
@@ -8,10 +11,10 @@ from visidata import *
8
11
  # pivotrows is { pivot_values: list(source.rows in group with pivot_values) }
9
12
  PivotGroupRow = collections.namedtuple('PivotGroupRow', 'discrete_keys numeric_key sourcerows pivotrows'.split())
10
13
 
11
- def Pivot(source, groupByCols, pivotCols):
14
+ def makePivot(source, groupByCols, pivotCols):
12
15
  return PivotSheet('',
13
- groupByCols,
14
- pivotCols,
16
+ groupByCols=groupByCols,
17
+ pivotCols=pivotCols,
15
18
  source=source)
16
19
 
17
20
  def makeErrorKey(col):
@@ -23,6 +26,8 @@ def makeErrorKey(col):
23
26
  def formatRange(col, numeric_key):
24
27
  a, b = numeric_key
25
28
  nankey = makeErrorKey(col)
29
+ if b is None:
30
+ return a
26
31
  if a is nankey and b is nankey:
27
32
  return '#ERR'
28
33
  elif a == b:
@@ -44,31 +49,38 @@ class RangeColumn(Column):
44
49
  return formatRange(self.origcol, typedval)
45
50
 
46
51
 
47
- 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):
48
60
  aggname = '%s_%s' % (aggcol.name, aggregator.name)
49
61
 
50
- return Column(aggname,
62
+ return AggrColumn(aggname,
51
63
  type=aggregator.type or aggcol.type,
52
64
  fmtstr=aggcol.fmtstr,
53
- getter=lambda col,row,agg=aggregator: agg(col.origCol, row.sourcerows),
54
65
  origCol=aggcol,
55
- )
66
+ aggregator=aggregator)
56
67
 
57
68
 
58
69
  class PivotSheet(Sheet):
59
70
  'Summarize key columns in pivot table and display as new sheet.'
60
71
  rowtype = 'grouped rows' # rowdef: PivotGroupRow
61
- def __init__(self, name, groupByCols, pivotCols, **kwargs):
62
- super().__init__(name, **kwargs)
63
72
 
64
- self.pivotCols = pivotCols # whose values become columns
65
- 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)
66
78
 
67
79
  def isNumericRange(self, col):
68
80
  return vd.isNumeric(col) and self.source.options.numeric_binning
69
81
 
70
- def initCols(self):
71
- self.columns = []
82
+ def resetCols(self):
83
+ super().resetCols()
72
84
 
73
85
  # add key columns (grouped by)
74
86
  for colnum, c in enumerate(self.groupByCols):
@@ -102,12 +114,10 @@ class PivotSheet(Sheet):
102
114
  vs.rows = row.pivotrows.get(col.aggvalue, [])
103
115
  return vs
104
116
 
105
- def reload(self):
106
- self.initCols()
107
-
117
+ def loader(self):
108
118
  # two different threads for better interactive display
109
- self.addAggregateCols()
110
- self.groupRows()
119
+ vd.sync(self.addAggregateCols(),
120
+ self.groupRows())
111
121
 
112
122
  @asyncthread
113
123
  def addAggregateCols(self):
@@ -129,7 +139,7 @@ class PivotSheet(Sheet):
129
139
  if not self.pivotCols:
130
140
  for aggcol, aggregatorlist in aggcols.items():
131
141
  for aggregator in aggregatorlist:
132
- c = AggrColumn(aggcol, aggregator)
142
+ c = makeAggrColumn(aggcol, aggregator)
133
143
  self.addColumn(c)
134
144
 
135
145
  # add pivoted columns
@@ -176,6 +186,7 @@ class PivotSheet(Sheet):
176
186
 
177
187
  @asyncthread
178
188
  def groupRows(self, rowfunc=None):
189
+ with ScopedSetattr(self, 'loading', True):
179
190
  self.rows = []
180
191
 
181
192
  discreteCols = [c for c in self.groupByCols if not self.isNumericRange(c)]
@@ -190,13 +201,16 @@ class PivotSheet(Sheet):
190
201
  if numericCols:
191
202
  nbins = self.source.options.histogram_bins or int(len(self.source.rows) ** (1./2))
192
203
  vals = tuple(numericCols[0].getValues(self.source.rows))
193
- minval = min(vals)
194
- maxval = max(vals)
204
+ minval = min(vals) if vals else 0
205
+ maxval = max(vals) if vals else 0
195
206
  width = (maxval - minval)/nbins
196
207
 
197
208
  if width == 0:
198
- # only one value (and maybe errors)
199
- numericBins = [(minval, maxval)]
209
+ if vals:
210
+ # only one value
211
+ numericBins = [(minval, maxval)]
212
+ else:
213
+ numericBins = []
200
214
  elif (numericCols[0].type in (int, vlen) and nbins > (maxval - minval)) or (width == 1):
201
215
  # (more bins than int vals) or (if bins are of width 1), just use the vals as bins
202
216
  degenerateBinning = True
@@ -226,25 +240,29 @@ class PivotSheet(Sheet):
226
240
  if numericCols:
227
241
  try:
228
242
  val = numericCols[0].getValue(sourcerow)
229
- if val is not None:
230
- val = numericCols[0].type(val)
231
- if not width:
232
- binidx = 0
233
- elif degenerateBinning:
234
- # in degenerate binning, each val has its own bin
235
- binidx = numericBins.index((val, val))
243
+ val = wrapply(numericCols[0].type, val)
244
+ if not val:
245
+ groupRow = numericGroupRows.get(str(val), None)
236
246
  else:
237
- binidx = int((val-minval)//width)
238
- 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)])]
239
255
  except Exception as e:
240
- # leave in main/error bin
241
- pass
256
+ vd.exceptionCaught(e)
242
257
 
243
258
  # add the main bin if no numeric bin (error, or no numeric cols)
244
259
  if groupRow is None:
245
- nankey = makeErrorKey(numericCols[0]) if numericCols else 0
246
- groupRow = PivotGroupRow(discreteKeys, (nankey, nankey), [], {})
247
- 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)
248
266
  self.addRow(groupRow)
249
267
 
250
268
  # add the sourcerow to its all bin
@@ -261,24 +279,33 @@ class PivotSheet(Sheet):
261
279
  if rowfunc:
262
280
  rowfunc(groupRow)
263
281
 
282
+ def afterLoad(self):
283
+ super().afterLoad()
284
+
264
285
  # automatically add cache to all columns now that everything is binned
265
286
  for c in self.nonKeyVisibleCols:
266
- c.setCache(True)
287
+ if isinstance(c, AggrColumn):
288
+ c.setCache(True)
267
289
 
268
290
 
269
291
  @PivotSheet.api
270
292
  def addcol_aggr(sheet, col):
271
293
  hasattr(col, 'origCol') or vd.fail('not an aggregation column')
272
- for agg in vd.chooseMany(vd.aggregator_choices):
273
- sheet.addColumnAtCursor(AggrColumn(col.origCol, vd.aggregators[agg]))
294
+ for agg in vd.chooseAggregators():
295
+ sheet.addColumnAtCursor(makeAggrColumn(col.origCol, vd.aggregators[agg]))
274
296
 
275
297
 
276
- 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')
277
299
 
278
300
  PivotSheet.addCommand('', 'addcol-aggr', 'addcol_aggr(cursorCol)', 'add aggregation column from source of current column')
279
- vd.addMenuItem('Column', 'Add column', 'aggregator', 'addcol-aggr')
280
301
 
281
- vd.addGlobals({
282
- 'Pivot': Pivot,
283
- 'PivotGroupRow': PivotGroupRow,
284
- })
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
+ ''')