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.
- visidata/__init__.py +72 -91
- visidata/_input.py +263 -44
- visidata/_open.py +84 -29
- visidata/_types.py +22 -4
- visidata/_urlcache.py +17 -4
- visidata/aggregators.py +65 -25
- visidata/apps/__init__.py +0 -0
- visidata/apps/vdsql/__about__.py +8 -0
- visidata/apps/vdsql/__init__.py +5 -0
- visidata/apps/vdsql/__main__.py +27 -0
- visidata/apps/vdsql/_ibis.py +748 -0
- visidata/apps/vdsql/bigquery.py +61 -0
- visidata/apps/vdsql/clickhouse.py +53 -0
- visidata/apps/vdsql/setup.py +40 -0
- visidata/apps/vdsql/snowflake.py +67 -0
- visidata/apps/vgit/__init__.py +13 -0
- visidata/apps/vgit/__main__.py +3 -0
- visidata/apps/vgit/abort.py +23 -0
- visidata/apps/vgit/blame.py +76 -0
- visidata/apps/vgit/branch.py +153 -0
- visidata/apps/vgit/config.py +95 -0
- visidata/apps/vgit/diff.py +169 -0
- visidata/apps/vgit/gitsheet.py +161 -0
- visidata/apps/vgit/grep.py +37 -0
- visidata/apps/vgit/log.py +81 -0
- visidata/apps/vgit/main.py +55 -0
- visidata/apps/vgit/remote.py +57 -0
- visidata/apps/vgit/repos.py +71 -0
- visidata/apps/vgit/setup.py +37 -0
- visidata/apps/vgit/stash.py +69 -0
- visidata/apps/vgit/status.py +204 -0
- visidata/apps/vgit/statusbar.py +34 -0
- visidata/basesheet.py +59 -50
- visidata/canvas.py +251 -99
- visidata/choose.py +15 -11
- visidata/clean_names.py +29 -0
- visidata/clipboard.py +84 -18
- visidata/cliptext.py +220 -46
- visidata/cmdlog.py +89 -114
- visidata/color.py +142 -56
- visidata/column.py +134 -131
- visidata/ddw/input.ddw +74 -79
- visidata/ddw/regex.ddw +57 -0
- visidata/ddwplay.py +33 -14
- visidata/deprecated.py +77 -3
- visidata/desktop/visidata.desktop +7 -0
- visidata/editor.py +12 -6
- visidata/errors.py +5 -1
- visidata/experimental/__init__.py +0 -0
- visidata/experimental/diff_sheet.py +29 -0
- visidata/experimental/digit_autoedit.py +6 -0
- visidata/experimental/gdrive.py +89 -0
- visidata/experimental/google.py +37 -0
- visidata/experimental/gsheets.py +79 -0
- visidata/experimental/live_search.py +37 -0
- visidata/experimental/liveupdate.py +45 -0
- visidata/experimental/mark.py +133 -0
- visidata/experimental/noahs_tapestry/__init__.py +1 -0
- visidata/experimental/noahs_tapestry/tapestry.py +147 -0
- visidata/experimental/rownum.py +73 -0
- visidata/experimental/slide_cells.py +26 -0
- visidata/expr.py +8 -4
- visidata/extensible.py +32 -6
- visidata/features/__init__.py +0 -0
- visidata/features/addcol_audiometadata.py +42 -0
- visidata/features/addcol_histogram.py +34 -0
- visidata/features/canvas_save_svg.py +69 -0
- visidata/features/change_precision.py +46 -0
- visidata/features/cmdpalette.py +163 -0
- visidata/features/colorbrewer.py +363 -0
- visidata/{colorsheet.py → features/colorsheet.py} +17 -16
- visidata/features/command_server.py +105 -0
- visidata/features/currency_to_usd.py +70 -0
- visidata/{customdate.py → features/customdate.py} +2 -0
- visidata/features/dedupe.py +132 -0
- visidata/{describe.py → features/describe.py} +17 -15
- visidata/features/errors_guide.py +26 -0
- visidata/features/expand_cols.py +202 -0
- visidata/{fill.py → features/fill.py} +4 -2
- visidata/{freeze.py → features/freeze.py} +11 -6
- visidata/features/graph_seaborn.py +79 -0
- visidata/features/helloworld.py +10 -0
- visidata/features/hint_types.py +17 -0
- visidata/{incr.py → features/incr.py} +5 -0
- visidata/{join.py → features/join.py} +107 -53
- visidata/features/known_cols.py +21 -0
- visidata/features/layout.py +62 -0
- visidata/{melt.py → features/melt.py} +33 -21
- visidata/features/normcol.py +118 -0
- visidata/features/open_config.py +7 -0
- visidata/features/open_syspaste.py +18 -0
- visidata/features/ping.py +157 -0
- visidata/features/procmgr.py +208 -0
- visidata/features/random_sample.py +6 -0
- visidata/{regex.py → features/regex.py} +47 -31
- visidata/features/reload_every.py +55 -0
- visidata/features/rename_col_cascade.py +30 -0
- visidata/features/scroll_context.py +60 -0
- visidata/features/select_equal_selected.py +11 -0
- visidata/features/setcol_fake.py +65 -0
- visidata/{slide.py → features/slide.py} +75 -21
- visidata/features/sparkline.py +48 -0
- visidata/features/status_source.py +20 -0
- visidata/{sysedit.py → features/sysedit.py} +2 -1
- visidata/features/sysopen_mailcap.py +46 -0
- visidata/features/term_extras.py +13 -0
- visidata/{transpose.py → features/transpose.py} +5 -4
- visidata/features/type_ipaddr.py +73 -0
- visidata/features/type_url.py +11 -0
- visidata/{unfurl.py → features/unfurl.py} +9 -9
- visidata/{window.py → features/window.py} +2 -2
- visidata/form.py +50 -21
- visidata/freqtbl.py +81 -33
- visidata/fuzzymatch.py +414 -0
- visidata/graph.py +105 -33
- visidata/guide.py +180 -0
- visidata/help.py +75 -44
- visidata/hint.py +39 -0
- visidata/indexsheet.py +109 -0
- visidata/input_history.py +55 -0
- visidata/interface.py +58 -0
- visidata/keys.py +17 -16
- visidata/loaders/__init__.py +9 -0
- visidata/loaders/_pandas.py +61 -21
- visidata/loaders/api_airtable.py +70 -0
- visidata/loaders/api_bitio.py +102 -0
- visidata/loaders/api_matrix.py +148 -0
- visidata/loaders/api_reddit.py +306 -0
- visidata/loaders/api_zulip.py +249 -0
- visidata/loaders/archive.py +41 -7
- visidata/loaders/arrow.py +7 -7
- visidata/loaders/conll.py +49 -0
- visidata/loaders/csv.py +25 -7
- visidata/loaders/eml.py +3 -4
- visidata/loaders/f5log.py +1204 -0
- visidata/loaders/fec.py +325 -0
- visidata/loaders/fixed_width.py +3 -5
- visidata/loaders/frictionless.py +3 -3
- visidata/loaders/geojson.py +8 -5
- visidata/loaders/google.py +48 -0
- visidata/loaders/graphviz.py +4 -4
- visidata/loaders/hdf5.py +4 -4
- visidata/loaders/html.py +48 -10
- visidata/loaders/http.py +84 -30
- visidata/loaders/imap.py +20 -10
- visidata/loaders/jrnl.py +52 -0
- visidata/loaders/json.py +83 -29
- visidata/loaders/jsonla.py +74 -0
- visidata/loaders/lsv.py +15 -11
- visidata/loaders/mailbox.py +40 -0
- visidata/loaders/markdown.py +1 -3
- visidata/loaders/mbtiles.py +4 -5
- visidata/loaders/mysql.py +11 -13
- visidata/loaders/npy.py +7 -7
- visidata/loaders/odf.py +4 -1
- visidata/loaders/orgmode.py +428 -0
- visidata/loaders/pandas_freqtbl.py +14 -20
- visidata/loaders/parquet.py +62 -6
- visidata/loaders/pcap.py +3 -3
- visidata/loaders/pdf.py +4 -3
- visidata/loaders/png.py +19 -13
- visidata/loaders/postgres.py +9 -8
- visidata/loaders/rec.py +7 -3
- visidata/loaders/s3.py +342 -0
- visidata/loaders/sas.py +5 -5
- visidata/loaders/scrape.py +186 -0
- visidata/loaders/shp.py +6 -5
- visidata/loaders/spss.py +5 -6
- visidata/loaders/sqlite.py +68 -28
- visidata/loaders/texttables.py +1 -1
- visidata/loaders/toml.py +60 -0
- visidata/loaders/tsv.py +61 -19
- visidata/loaders/ttf.py +19 -7
- visidata/loaders/unzip_http.py +6 -5
- visidata/loaders/usv.py +1 -1
- visidata/loaders/vcf.py +16 -16
- visidata/loaders/vds.py +10 -7
- visidata/loaders/vdx.py +30 -5
- visidata/loaders/xlsb.py +8 -1
- visidata/loaders/xlsx.py +145 -25
- visidata/loaders/xml.py +6 -3
- visidata/loaders/xword.py +4 -4
- visidata/loaders/yaml.py +15 -5
- visidata/macos.py +1 -1
- visidata/macros.py +130 -41
- visidata/main.py +119 -94
- visidata/mainloop.py +101 -154
- visidata/man/parse_options.py +2 -2
- visidata/man/vd.1 +302 -147
- visidata/man/vd.txt +291 -151
- visidata/memory.py +3 -3
- visidata/menu.py +104 -423
- visidata/metasheets.py +59 -141
- visidata/modify.py +79 -23
- visidata/motd.py +3 -3
- visidata/mouse.py +137 -0
- visidata/movement.py +43 -35
- visidata/optionssheet.py +99 -0
- visidata/path.py +131 -43
- visidata/pivot.py +74 -47
- visidata/plugins.py +65 -192
- visidata/pyobj.py +50 -201
- visidata/rename_col.py +20 -0
- visidata/save.py +42 -20
- visidata/search.py +54 -10
- visidata/selection.py +84 -5
- visidata/settings.py +162 -24
- visidata/sheets.py +229 -257
- visidata/shell.py +51 -21
- visidata/sidebar.py +162 -0
- visidata/sort.py +11 -4
- visidata/statusbar.py +113 -104
- visidata/stored_list.py +43 -0
- visidata/stored_prop.py +38 -0
- visidata/tests/conftest.py +3 -3
- visidata/tests/test_cliptext.py +39 -0
- visidata/tests/test_commands.py +62 -7
- visidata/tests/test_edittext.py +2 -2
- visidata/tests/test_features.py +17 -0
- visidata/tests/test_menu.py +14 -0
- visidata/tests/test_path.py +13 -4
- visidata/text_source.py +53 -0
- visidata/textsheet.py +10 -3
- visidata/theme.py +44 -0
- visidata/themes/__init__.py +0 -0
- visidata/themes/ascii8.py +84 -0
- visidata/themes/asciimono.py +84 -0
- visidata/themes/light.py +17 -0
- visidata/threads.py +87 -39
- visidata/tuiwin.py +22 -0
- visidata/type_currency.py +22 -3
- visidata/type_date.py +31 -9
- visidata/type_floatsi.py +5 -1
- visidata/undo.py +18 -6
- visidata/utils.py +106 -23
- visidata/vdobj.py +28 -17
- visidata/windows.py +10 -0
- visidata/wrappers.py +9 -3
- visidata-3.0.data/data/share/applications/visidata.desktop +7 -0
- {visidata-2.11.dev0.data → visidata-3.0.data}/data/share/man/man1/vd.1 +302 -147
- {visidata-2.11.dev0.data → visidata-3.0.data}/data/share/man/man1/visidata.1 +302 -147
- visidata-3.0.data/scripts/vd2to3.vdx +9 -0
- {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/METADATA +13 -11
- visidata-3.0.dist-info/RECORD +257 -0
- {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/WHEEL +1 -1
- {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/entry_points.txt +0 -1
- visidata/layout.py +0 -44
- visidata/misc.py +0 -5
- visidata-2.11.dev0.dist-info/RECORD +0 -142
- /visidata/{repeat.py → features/repeat.py} +0 -0
- {visidata-2.11.dev0.data → visidata-3.0.data}/scripts/vd +0 -0
- {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/LICENSE.gpl3 +0 -0
- {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
|
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
|
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
|
-
|
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
|
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
|
-
|
65
|
-
|
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
|
71
|
-
|
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
|
106
|
-
self.initCols()
|
107
|
-
|
117
|
+
def loader(self):
|
108
118
|
# two different threads for better interactive display
|
109
|
-
self.addAggregateCols()
|
110
|
-
|
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 =
|
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
|
-
|
199
|
-
|
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
|
-
|
230
|
-
|
231
|
-
|
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
|
-
|
238
|
-
|
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
|
-
|
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
|
-
|
246
|
-
|
247
|
-
|
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
|
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.
|
273
|
-
sheet.addColumnAtCursor(
|
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(
|
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
|
-
|
283
|
-
|
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,
|
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.
|
18
|
-
def
|
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
|
-
|
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
|
-
|
45
|
-
|
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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
55
|
-
|
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
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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.
|
68
|
-
def
|
69
|
-
'
|
70
|
-
|
71
|
-
|
72
|
-
|
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
|
-
|
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
|
-
|
91
|
-
|
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
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
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', '
|
229
|
-
PluginsSheet.addCommand('d', 'delete-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
|
+
''')
|