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.
- visidata/__init__.py +72 -91
- visidata/_input.py +259 -42
- visidata/_open.py +84 -29
- visidata/_types.py +21 -3
- 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
- {vgit → visidata/apps/vgit}/blame.py +5 -2
- {vgit → visidata/apps/vgit}/branch.py +31 -16
- {vgit → visidata/apps/vgit}/config.py +3 -3
- visidata/apps/vgit/diff.py +169 -0
- visidata/apps/vgit/gitsheet.py +161 -0
- {vgit → visidata/apps/vgit}/grep.py +6 -5
- visidata/apps/vgit/log.py +81 -0
- {vgit → visidata/apps/vgit}/main.py +18 -5
- {vgit → visidata/apps/vgit}/remote.py +8 -4
- visidata/apps/vgit/repos.py +71 -0
- {vgit → visidata/apps/vgit}/setup.py +6 -4
- visidata/apps/vgit/stash.py +69 -0
- visidata/apps/vgit/status.py +204 -0
- {vgit → visidata/apps/vgit}/statusbar.py +2 -0
- visidata/basesheet.py +59 -50
- visidata/canvas.py +208 -93
- visidata/choose.py +6 -6
- visidata/clean_names.py +29 -0
- visidata/clipboard.py +73 -17
- visidata/cliptext.py +220 -46
- visidata/cmdlog.py +88 -114
- visidata/color.py +142 -56
- visidata/column.py +121 -129
- 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 +30 -5
- 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} +3 -1
- 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} +32 -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 +2 -4
- 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/macros.py +129 -42
- visidata/main.py +119 -94
- visidata/mainloop.py +101 -155
- visidata/man/parse_options.py +2 -2
- visidata/man/vd.1 +301 -148
- visidata/man/vd.txt +290 -153
- visidata/memory.py +3 -3
- visidata/menu.py +104 -423
- visidata/metasheets.py +59 -141
- visidata/modify.py +78 -23
- visidata/motd.py +3 -3
- visidata/mouse.py +137 -0
- visidata/movement.py +43 -35
- visidata/optionssheet.py +99 -0
- visidata/path.py +113 -32
- visidata/pivot.py +73 -47
- visidata/plugins.py +65 -192
- visidata/pyobj.py +50 -201
- visidata/rename_col.py +20 -0
- visidata/save.py +37 -20
- visidata/search.py +54 -10
- visidata/selection.py +84 -5
- visidata/settings.py +162 -25
- 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 +17 -5
- 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.1.data → visidata-3.0.data}/data/share/man/man1/vd.1 +301 -148
- {visidata-2.11.1.data → visidata-3.0.data}/data/share/man/man1/visidata.1 +301 -148
- visidata-3.0.data/scripts/vd2to3.vdx +9 -0
- {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/METADATA +12 -8
- visidata-3.0.dist-info/RECORD +257 -0
- {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/WHEEL +1 -1
- vgit/__init__.py +0 -1
- vgit/gitsheet.py +0 -164
- visidata/layout.py +0 -44
- visidata/misc.py +0 -5
- visidata-2.11.1.data/scripts/vgit +0 -9
- visidata-2.11.1.dist-info/RECORD +0 -155
- {vgit → visidata/apps/vgit}/__main__.py +0 -0
- {vgit → visidata/apps/vgit}/abort.py +0 -0
- /visidata/{repeat.py → features/repeat.py} +0 -0
- {visidata-2.11.1.data → visidata-3.0.data}/scripts/vd +0 -0
- {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/LICENSE.gpl3 +0 -0
- {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/entry_points.txt +0 -0
- {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
|
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
|
-
|
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
|
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
|
-
|
66
|
-
|
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
|
72
|
-
|
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
|
107
|
-
self.initCols()
|
108
|
-
|
117
|
+
def loader(self):
|
109
118
|
# two different threads for better interactive display
|
110
|
-
self.addAggregateCols()
|
111
|
-
|
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 =
|
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
|
-
|
200
|
-
|
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
|
-
|
231
|
-
|
232
|
-
|
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
|
-
|
239
|
-
|
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
|
-
|
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
|
-
|
247
|
-
|
248
|
-
|
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
|
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.
|
274
|
-
sheet.addColumnAtCursor(
|
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(
|
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
|
-
|
284
|
-
|
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,
|
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
|
+
''')
|