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/loaders/ttf.py CHANGED
@@ -3,7 +3,7 @@ from visidata import VisiData, vd, Sheet, Column, Progress, ColumnAttr, ColumnIt
3
3
 
4
4
  @VisiData.api
5
5
  def open_ttf(vd, p):
6
- return TTFTablesSheet(p.name, source=p)
6
+ return TTFTablesSheet(p.base_stem, source=p)
7
7
 
8
8
  vd.open_otf = vd.open_ttf
9
9
 
@@ -24,9 +24,9 @@ class TTFTablesSheet(Sheet):
24
24
  return TTFGlyphsSheet(self.name+'_glyphs', source=self, sourceRows=[row], ttf=self.ttf)
25
25
 
26
26
  def iterload(self):
27
- import fontTools.ttLib
27
+ fontTools = vd.importExternal('fontTools.ttLib', 'fonttools')
28
28
 
29
- self.ttf = fontTools.ttLib.TTFont(str(self.source), 0, allowVID=0, ignoreDecompileErrors=True, fontNumber=-1)
29
+ self.ttf = fontTools.TTFont(str(self.source), 0, allowVID=0, ignoreDecompileErrors=True, fontNumber=-1)
30
30
  for cmap in self.ttf["cmap"].tables:
31
31
  yield cmap
32
32
 
@@ -53,20 +53,21 @@ class TTFGlyphsSheet(Sheet):
53
53
 
54
54
 
55
55
  def makePen(*args, **kwargs):
56
- try:
57
- from fontTools.pens.basePen import BasePen
58
- except ImportError as e:
59
- vd.error('fonttools not installed')
56
+ fontTools = vd.importExternal('fontTools', 'fonttools')
57
+ from fontTools.pens.basePen import BasePen
60
58
 
61
59
  class GlyphPen(InvertedCanvas, BasePen):
62
60
  aspectRatio = 1.0
63
61
  def __init__(self, name, **kwargs):
64
62
  super().__init__(name, **kwargs)
63
+ self.path_firstxy = None
65
64
  self.lastxy = None
66
65
  self.attr = self.plotColor(('glyph',))
67
66
 
68
67
  def _moveTo(self, xy):
69
68
  self.lastxy = xy
69
+ if self.path_firstxy is None:
70
+ self.path_firstxy = xy
70
71
 
71
72
  def _lineTo(self, xy):
72
73
  x1, y1 = self.lastxy
@@ -74,6 +75,17 @@ def makePen(*args, **kwargs):
74
75
  self.line(x1, y1, x2, y2, self.attr)
75
76
  self._moveTo(xy)
76
77
 
78
+ def _closePath(self):
79
+ if self.path_firstxy:
80
+ if (self.path_firstxy != self.lastxy):
81
+ self._lineTo(self.path_firstxy)
82
+ self.path_firstxy = None
83
+ self.lastxy = None
84
+
85
+ def _endPath(self):
86
+ self.path_firstxy = None
87
+ self.lastxy = None
88
+
77
89
  def _curveToOne(self, xy1, xy2, xy3):
78
90
  vd.error('NotImplemented')
79
91
 
@@ -26,6 +26,7 @@ import struct
26
26
  import fnmatch
27
27
  import pathlib
28
28
  import urllib.parse
29
+ from visidata import vd
29
30
 
30
31
 
31
32
  __version__ = '0.5.1'
@@ -98,7 +99,7 @@ class RemoteZipFile:
98
99
  magic_eocd = b'\x50\x4b\x05\x06'
99
100
 
100
101
  def __init__(self, url):
101
- import urllib3
102
+ urllib3 = vd.importExternal('urllib3')
102
103
  self.url = url
103
104
  self.http = urllib3.PoolManager()
104
105
  self.zip_size = 0
@@ -185,7 +186,7 @@ class RemoteZipFile:
185
186
 
186
187
  outpath = path/member
187
188
  os.makedirs(outpath.parent, exist_ok=True)
188
- with self.open(member) as fpin:
189
+ with self._open(member) as fpin:
189
190
  with open(path/member, mode='wb') as fpout:
190
191
  while True:
191
192
  r = fpin.read(65536)
@@ -205,7 +206,7 @@ class RemoteZipFile:
205
206
  if any(fnmatch.fnmatch(f.filename, g) for g in globs):
206
207
  yield f
207
208
 
208
- def open(self, fn):
209
+ def _open(self, fn):
209
210
  if isinstance(fn, str):
210
211
  f = list(self.matching_files(fn))
211
212
  if not f:
@@ -226,8 +227,8 @@ class RemoteZipFile:
226
227
  else:
227
228
  error(f'unknown compression method {method}')
228
229
 
229
- def open_text(self, fn):
230
- return io.TextIOWrapper(self.open(fn))
230
+ def open(self, fn):
231
+ return io.TextIOWrapper(self._open(fn))
231
232
 
232
233
 
233
234
  class RemoteZipStream(io.RawIOBase):
visidata/loaders/usv.py CHANGED
@@ -4,7 +4,7 @@ from visidata import Sheet, TsvSheet, options, vd, VisiData
4
4
 
5
5
  @VisiData.api
6
6
  def open_usv(vd, p):
7
- return TsvSheet(p.name, source=p, delimiter='\u241f', row_delimiter='\u241e')
7
+ return TsvSheet(p.base_stem, source=p, delimiter='\u241f', row_delimiter='\u241e')
8
8
 
9
9
  @VisiData.api
10
10
  def save_usv(vd, p, vs):
visidata/loaders/vcf.py CHANGED
@@ -1,11 +1,11 @@
1
- from visidata import VisiData, Column, getitemdef, PythonSheet, asyncthread
1
+ from visidata import VisiData, Column, getitemdef, PythonSheet, asyncthread, vd
2
2
 
3
3
 
4
4
  # requires (deb): libbz2-dev libcurl4-openssl-dev liblzma-dev
5
5
 
6
6
  @VisiData.api
7
7
  def open_vcf(vd, p):
8
- return VcfSheet(p.name, source=p)
8
+ return VcfSheet(p.base_stem, source=p)
9
9
 
10
10
  def unbox(col, row):
11
11
  v = getitemdef(row, col.expr)
@@ -20,22 +20,22 @@ class VcfSheet(PythonSheet):
20
20
  rowtype = 'cards'
21
21
  @asyncthread
22
22
  def reload(self):
23
- import vobject
23
+ vobject = vd.importExternal('vobject')
24
24
  self.rows = []
25
25
  self.columns = []
26
26
 
27
27
  addedCols = set()
28
28
  lines = []
29
- for line in self.source.open_text(encoding=self.options.encoding):
30
- lines.append(line)
31
- if line.startswith('END:'):
32
- row = vobject.readOne('\n'.join(lines))
33
- for k, v in row.contents.items():
34
- if v and str(v[0].value).startswith('(None)'):
35
- continue
36
- if not k in addedCols:
37
- addedCols.add(k)
38
- self.addColumn(Column(k, expr=k, getter=unbox))
39
- self.addRow(row.contents)
40
- lines = []
41
-
29
+ with self.open_text_source() as fp:
30
+ for line in fp:
31
+ lines.append(line)
32
+ if line.startswith('END:'):
33
+ row = vobject.readOne('\n'.join(lines))
34
+ for k, v in row.contents.items():
35
+ if v and str(v[0].value).startswith('(None)'):
36
+ continue
37
+ if not k in addedCols:
38
+ addedCols.add(k)
39
+ self.addColumn(Column(k, expr=k, getter=unbox))
40
+ self.addRow(row.contents)
41
+ lines = []
visidata/loaders/vds.py CHANGED
@@ -2,37 +2,40 @@
2
2
 
3
3
  import json
4
4
 
5
- from visidata import VisiData, JsonSheet, Progress, IndexSheet, SettableColumn, ItemColumn
5
+ from visidata import VisiData, JsonSheet, Progress, IndexSheet, SettableColumn, ItemColumn, ExprColumn
6
6
 
7
7
 
8
8
  NL='\n'
9
9
 
10
10
  @VisiData.api
11
11
  def open_vds(vd, p):
12
- return VdsIndexSheet(p.name, source=p)
12
+ return VdsIndexSheet(p.base_stem, source=p)
13
13
 
14
14
 
15
15
  @VisiData.api
16
16
  def save_vds(vd, p, *sheets):
17
17
  'Save in custom VisiData format, preserving columns and their attributes.'
18
18
 
19
- with p.open_text(mode='w', encoding='utf-8') as fp:
19
+ with p.open(mode='w', encoding='utf-8') as fp:
20
20
  for vs in sheets:
21
21
  # class and attrs for vs
22
22
  d = { 'name': vs.name, }
23
23
  fp.write('#'+json.dumps(d)+NL)
24
24
 
25
25
  # class and attrs for each column in vs
26
- for col in vs.visibleCols:
26
+ for col in vs.columns:
27
27
  d = col.__getstate__()
28
28
  if isinstance(col, SettableColumn):
29
29
  d['col'] = 'Column'
30
+ elif isinstance(col, ItemColumn):
31
+ d['col'] = 'Column'
32
+ d['expr'] = col.name #2037 override expr
30
33
  else:
31
34
  d['col'] = type(col).__name__
32
35
  fp.write('#'+json.dumps(d)+NL)
33
36
 
34
37
  with Progress(gerund='saving'):
35
- for row in vs.iterdispvals(*vs.visibleCols, format=False):
38
+ for row in vs.iterdispvals(*vs.columns, format=False):
36
39
  d = {col.name:val for col, val in row.items()}
37
40
  fp.write(json.dumps(d, default=str)+NL)
38
41
 
@@ -40,7 +43,7 @@ def save_vds(vd, p, *sheets):
40
43
  class VdsIndexSheet(IndexSheet):
41
44
  def iterload(self):
42
45
  vs = None
43
- with self.source.open_text(encoding='utf-8') as fp:
46
+ with self.source.open(encoding='utf-8') as fp:
44
47
  line = fp.readline()
45
48
  while line:
46
49
  if line.startswith('#{'):
@@ -59,7 +62,7 @@ class VdsSheet(JsonSheet):
59
62
  self.colnames = {}
60
63
  self.columns = []
61
64
 
62
- with self.source.open_text(encoding='utf-8') as fp:
65
+ with self.source.open(encoding='utf-8') as fp:
63
66
  fp.seek(self.source_fpos)
64
67
 
65
68
  # consume all metadata, create columns
visidata/loaders/vdx.py CHANGED
@@ -1,10 +1,12 @@
1
+ import re
2
+
1
3
  import visidata
2
- from visidata import VisiData, CommandLogBase, BaseSheet, Sheet, AttrDict
4
+ from visidata import VisiData, CommandLogBase, BaseSheet, Sheet, AttrDict, Progress
3
5
 
4
6
 
5
7
  @VisiData.api
6
8
  def open_vdx(vd, p):
7
- return CommandLogSimple(p.name, source=p, precious=True)
9
+ return CommandLogSimple(p.base_stem, source=p, precious=True)
8
10
 
9
11
 
10
12
  class CommandLogSimple(CommandLogBase, Sheet):
@@ -20,16 +22,16 @@ class CommandLogSimple(CommandLogBase, Sheet):
20
22
 
21
23
  @VisiData.api
22
24
  def save_vdx(vd, p, *vsheets):
23
- with p.open_text(mode='w', encoding=vsheets[0].options.encoding) as fp:
25
+ with p.open(mode='w', encoding=vsheets[0].options.save_encoding) as fp:
24
26
  fp.write(f"# {visidata.__version_info__}\n")
25
27
  for vs in vsheets:
26
28
  prevrow = None
27
29
  for r in vs.rows:
28
30
  if prevrow is not None and r.sheet and prevrow.sheet != r.sheet:
29
31
  fp.write(f'sheet {r.sheet}\n')
30
- if r.col and (prevrow.col != r.col or prevrow is None):
32
+ if r.col and (prevrow is None or prevrow.col != r.col):
31
33
  fp.write(f'col {r.col}\n')
32
- if r.row and (prevrow.row != r.row or prevrow is None):
34
+ if r.row and (prevrow is None or prevrow.row != r.row):
33
35
  fp.write(f'row {r.row}\n')
34
36
 
35
37
  line = r.longname
@@ -40,6 +42,29 @@ def save_vdx(vd, p, *vsheets):
40
42
  prevrow = r
41
43
 
42
44
 
45
+ @VisiData.api
46
+ def runvdx(vd, vdx:str):
47
+ for line in Progress(vdx.splitlines()):
48
+ vs = vd.sheet or Sheet()
49
+ vs.ensureLoaded()
50
+ line = line.strip()
51
+ if not line or line[0] == '#':
52
+ continue
53
+
54
+ m = re.match(r'^(\+(\S+) )?(\S+)(.*)$', line)
55
+ if not m:
56
+ print('bad:', line)
57
+ continue
58
+
59
+ _, pos, longname, rest = m.groups()
60
+ vd.currentReplayRow = AttrDict(longname=longname, input=rest)
61
+ if pos:
62
+ vd.moveToPos(vd.sheets, *vd.parsePos(pos))
63
+ print(vs.name, longname)
64
+ vs.execCommand(longname)
65
+ vd.sync()
66
+
67
+
43
68
  BaseSheet.addCommand('', 'sheet', 'n=input("sheet to jump to: "); vd.push(vd.getSheet(n) or fail(f"no such sheet {n}"))', 'jump to named sheet')
44
69
  BaseSheet.addCommand('', 'col', 'n=input("column to go to: "); moveToCol(n) or fail(f"no such column {n}")', 'move to named/numbered col')
45
70
  BaseSheet.addCommand('', 'row', 'n=input("row to go to: "); moveToRow(n) or fail(f"no such row {n}")', 'move to named/numbered row')
visidata/loaders/xlsb.py CHANGED
@@ -2,13 +2,20 @@ from visidata import vd, IndexSheet, VisiData
2
2
 
3
3
  'Requires visidata/deps/pyxlsb fork'
4
4
 
5
+ @VisiData.api
6
+ def guess_xls(vd, p):
7
+ if p.open_bytes().read(16).startswith(b'\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1'):
8
+ return dict(filetype='xlsb', _likelihood=10)
9
+
5
10
 
6
11
  @VisiData.api
7
12
  def open_xlsb(vd, p):
8
- return XlsbIndex(p.name, source=p)
13
+ return XlsbIndex(p.base_stem, source=p)
14
+
9
15
 
10
16
  class XlsbIndex(IndexSheet):
11
17
  def iterload(self):
18
+ vd.importExternal('pyxlsb', '-e git+https://github.com/saulpw/pyxlsb.git@visidata#egg=pyxlsb')
12
19
  from pyxlsb import open_workbook
13
20
 
14
21
  wb = open_workbook(str(self.source))
visidata/loaders/xlsx.py CHANGED
@@ -1,8 +1,11 @@
1
1
  import itertools
2
2
  import copy
3
3
  import datetime
4
+ import re
5
+ from colorsys import rgb_to_hls, hls_to_rgb
4
6
 
5
7
  from visidata import VisiData, vd, Sheet, Column, Progress, IndexSheet, ColumnAttr, SequenceSheet, AttrDict, AttrColumn
8
+ from visidata import CellColorizer, getattrdeep, rgb_to_attr
6
9
  from visidata.type_date import date
7
10
 
8
11
 
@@ -10,11 +13,11 @@ vd.option('xlsx_meta_columns', False, 'include columns for cell objects, font co
10
13
 
11
14
  @VisiData.api
12
15
  def open_xls(vd, p):
13
- return XlsIndexSheet(p.name, source=p)
16
+ return XlsIndexSheet(p.base_stem, source=p)
14
17
 
15
18
  @VisiData.api
16
19
  def open_xlsx(vd, p):
17
- return XlsxIndexSheet(p.name, source=p)
20
+ return XlsxIndexSheet(p.base_stem, source=p)
18
21
 
19
22
  class XlsxIndexSheet(IndexSheet):
20
23
  'Load XLSX file (in Excel Open XML format).'
@@ -29,16 +32,20 @@ class XlsxIndexSheet(IndexSheet):
29
32
  nKeys = 1
30
33
 
31
34
  def iterload(self):
32
- import openpyxl
35
+ openpyxl = vd.importExternal('openpyxl')
33
36
  self.workbook = openpyxl.load_workbook(str(self.source), data_only=True, read_only=True)
34
37
  for sheetname in self.workbook.sheetnames:
35
38
  src = self.workbook[sheetname]
36
- yield XlsxSheet(self.name, sheetname, source=src)
39
+ yield XlsxSheet(self.name, sheetname, source=src, workbook=self.workbook)
37
40
 
38
41
 
39
42
  class XlsxSheet(SequenceSheet):
40
43
  # rowdef: AttrDict of column_letter to cell
44
+ colorizers = [
45
+ CellColorizer(5, None, lambda s,c,r,v: c and r and s.colorize_xlsx_cell(c,r))
46
+ ]
41
47
  def setCols(self, headerrows):
48
+ vd.importExternal('openpyxl')
42
49
  from openpyxl.utils.cell import get_column_letter
43
50
  self.columns = []
44
51
  self._rowtype = AttrDict
@@ -55,7 +62,7 @@ class XlsxSheet(SequenceSheet):
55
62
  for i, colnamelines in enumerate(itertools.zip_longest(*headers, fillvalue='')):
56
63
  colnamelines = ['' if c is None else c for c in colnamelines]
57
64
  column_name = ''.join(map(str, colnamelines))
58
- self.addColumn(AttrColumn(column_name, column_letters[i] + '.value'))
65
+ self.addColumn(AttrColumn(column_name, column_letters[i] + '.value', column_letter=column_letters[i]))
59
66
  self.addXlsxMetaColumns(column_letters[i], column_name)
60
67
 
61
68
  def addRow(self, row, index=None):
@@ -65,6 +72,7 @@ class XlsxSheet(SequenceSheet):
65
72
  self.addXlsxMetaColumns(column_letter, column_letter)
66
73
 
67
74
  def iterload(self):
75
+ vd.importExternal('openpyxl')
68
76
  from openpyxl.utils.cell import get_column_letter
69
77
  worksheet = self.source
70
78
  for row in Progress(worksheet.iter_rows(), total=worksheet.max_row or 0):
@@ -73,13 +81,16 @@ class XlsxSheet(SequenceSheet):
73
81
  def addXlsxMetaColumns(self, column_letter, column_name):
74
82
  if self.options.xlsx_meta_columns:
75
83
  self.addColumn(
76
- AttrColumn(column_name + '_cellPyObj', column_letter))
84
+ AttrColumn(column_name + '_cellPyObj', column_letter, column_letter=column_letter))
77
85
  self.addColumn(
78
86
  AttrColumn(column_name + '_fontColor',
79
- column_letter + '.font.color.value'))
87
+ column_letter + '.font.color.value', column_letter=column_letter))
80
88
  self.addColumn(
81
89
  AttrColumn(column_name + '_fillColor', column_letter +
82
- '.fill.start_color.value'))
90
+ '.fill.start_color.value', column_letter=column_letter))
91
+ self.addColumn(Column(column_name + '_colorizer', width=0,
92
+ column_letter=column_letter,
93
+ getter=lambda c,r: c.sheet.colorize_xlsx_cell(c,r)))
83
94
 
84
95
  def paste_after(self, rowidx):
85
96
  to_paste = list(copy.copy(r) for r in reversed(vd.memory.cliprows))
@@ -97,7 +108,7 @@ class XlsIndexSheet(IndexSheet):
97
108
  ]
98
109
  nKeys = 1
99
110
  def iterload(self):
100
- import xlrd
111
+ xlrd = vd.importExternal('xlrd')
101
112
  self.workbook = xlrd.open_workbook(str(self.source))
102
113
  for sheetname in self.workbook.sheet_names():
103
114
  yield XlsSheet(self.name, sheetname, source=self.workbook.sheet_by_name(sheetname))
@@ -123,11 +134,26 @@ def xls_name(vs):
123
134
 
124
135
  @VisiData.api
125
136
  def save_xlsx(vd, p, *sheets):
126
- import openpyxl
137
+ openpyxl = vd.importExternal('openpyxl')
127
138
 
128
139
  wb = openpyxl.Workbook()
129
140
  wb.remove_sheet(wb['Sheet'])
130
141
 
142
+ def _convert_save_row(dispvals:dict, replace_illegal=False) -> list:
143
+ row = []
144
+ for col, v in dispvals.items():
145
+ if v is None:
146
+ v = ""
147
+ elif col.type == date:
148
+ v = datetime.datetime.fromtimestamp(int(v.timestamp()))
149
+ elif not vd.isNumeric(col):
150
+ v = str(v)
151
+ if replace_illegal:
152
+ v = re.sub(openpyxl.cell.cell.ILLEGAL_CHARACTERS_RE, ' ', v)
153
+
154
+ row.append(v)
155
+ return row
156
+
131
157
  for vs in sheets:
132
158
  if vs.xls_name != vs.names[-1]:
133
159
  vd.warning(f'saving {vs.name} as {vs.xls_name}')
@@ -137,28 +163,21 @@ def save_xlsx(vd, p, *sheets):
137
163
  ws.append(headers)
138
164
 
139
165
  for dispvals in vs.iterdispvals(format=False):
140
-
141
- row = []
142
- for col, v in dispvals.items():
143
- if v is None:
144
- v = ""
145
- elif col.type == date:
146
- v = datetime.datetime.fromtimestamp(int(v.timestamp()))
147
- elif not vd.isNumeric(col):
148
- v = str(v)
149
- row.append(v)
150
-
151
- ws.append(row)
166
+ row = _convert_save_row(dispvals)
167
+ try:
168
+ ws.append(row)
169
+ except openpyxl.utils.exceptions.IllegalCharacterError as e:
170
+ row = _convert_save_row(dispvals, replace_illegal=True) #1402
171
+ ws.append(row)
152
172
 
153
173
  wb.active = ws
154
174
 
155
175
  wb.save(filename=p)
156
- vd.status(f'{p} save finished')
157
176
 
158
177
 
159
178
  @VisiData.api
160
179
  def save_xls(vd, p, *sheets):
161
- import xlwt
180
+ xlwt = vd.importExternal('xlwt')
162
181
 
163
182
  wb = xlwt.Workbook()
164
183
 
@@ -175,4 +194,105 @@ def save_xls(vd, p, *sheets):
175
194
  ws1.write(r_i, c_i, v)
176
195
 
177
196
  wb.save(p)
178
- vd.status(f'{p} save finished')
197
+
198
+
199
+ # from https://stackoverflow.com/a/65426130
200
+
201
+ RGBMAX = 255
202
+ HLSMAX = 240
203
+
204
+ @XlsxSheet.api
205
+ def colorize_xlsx_cell(sheet, col, row):
206
+ fg = getattrdeep(row, col.column_letter+'.font.color', None)
207
+ bg = getattrdeep(row, col.column_letter+'.fill.start_color', None)
208
+ fg = sheet.xlsx_color_to_xterm256(fg)
209
+ bg = sheet.xlsx_color_to_xterm256(bg)
210
+
211
+ if bg == '-1' or fg == '-1':
212
+ fg, bg = '-1', '-1'
213
+
214
+ return f'{fg} on {bg}'
215
+
216
+ @XlsxSheet.api
217
+ def xlsx_color_to_xterm256(sheet, color) -> str:
218
+ if not color:
219
+ return ''
220
+
221
+ if color.type == 'rgb':
222
+ s = color.value
223
+ if isinstance(s, int):
224
+ return str(s)
225
+
226
+ a,r,g,b = s[0:2], s[2:4], s[4:6], s[6:8]
227
+ return rgb_to_attr(int(r, 16), int(g, 16), int(b, 16), int(a, 16))
228
+
229
+ if color.type == 'theme':
230
+ return sheet.theme_and_tint_to_rgb(color.value, color.tint)
231
+ else:
232
+ return str(color.value)
233
+
234
+ @XlsxSheet.api
235
+ def theme_and_tint_to_rgb(sheet, theme, tint) -> str:
236
+ """Given a workbook, a theme number and a tint return a xterm256 color number"""
237
+ rgb = sheet.theme_colors[theme]
238
+ h, l, s = rgb_to_ms_hls(rgb)
239
+ r, g, b = ms_hls_to_rgb(h, tint_luminance(tint, l), s)
240
+
241
+ return rgb_to_attr(r*256, g*256, b*256)
242
+
243
+ @XlsxSheet.lazy_property
244
+ def theme_colors(sheet):
245
+ """Gets theme colors from the workbook"""
246
+ # see: https://groups.google.com/forum/#!topic/openpyxl-users/I0k3TfqNLrc
247
+ from openpyxl.xml.functions import QName, fromstring
248
+ xlmns = 'http://schemas.openxmlformats.org/drawingml/2006/main'
249
+ root = fromstring(sheet.workbook.loaded_theme)
250
+ themeEl = root.find(QName(xlmns, 'themeElements').text)
251
+ colorSchemes = themeEl.findall(QName(xlmns, 'clrScheme').text)
252
+ firstColorScheme = colorSchemes[0]
253
+
254
+ theme_colors = []
255
+
256
+ for c in ['lt1', 'dk1', 'lt2', 'dk2', 'accent1', 'accent2', 'accent3', 'accent4', 'accent5', 'accent6']:
257
+ accent = firstColorScheme.find(QName(xlmns, c).text)
258
+ for i in list(accent): # walk all child nodes, rather than assuming [0]
259
+ if 'window' in i.attrib['val']:
260
+ theme_colors.append(i.attrib['lastClr'])
261
+ else:
262
+ theme_colors.append(i.attrib['val'])
263
+
264
+ return theme_colors
265
+
266
+ def rgb_to_ms_hls(red, green=None, blue=None):
267
+ """Converts rgb values in range (0,1) or a hex string of the form '[#aa]rrggbb' to HLSMAX based HLS, (alpha values are ignored)"""
268
+ if green is None:
269
+ if isinstance(red, str):
270
+ if len(red) > 6:
271
+ red = red[-6:] # Ignore preceding '#' and alpha values
272
+ blue = int(red[4:], 16) / RGBMAX
273
+ green = int(red[2:4], 16) / RGBMAX
274
+ red = int(red[0:2], 16) / RGBMAX
275
+ else:
276
+ red, green, blue = red
277
+ h, l, s = rgb_to_hls(red, green, blue)
278
+ return (int(round(h * HLSMAX)), int(round(l * HLSMAX)), int(round(s * HLSMAX)))
279
+
280
+ def ms_hls_to_rgb(hue, lightness=None, saturation=None):
281
+ """Converts HLSMAX based HLS values to rgb values in the range (0,1)"""
282
+ if lightness is None:
283
+ hue, lightness, saturation = hue
284
+ return hls_to_rgb(hue / HLSMAX, lightness / HLSMAX, saturation / HLSMAX)
285
+
286
+ def rgb_to_hex(red, green=None, blue=None):
287
+ """Converts (0,1) based RGB values to a hex string 'rrggbb'"""
288
+ if green is None:
289
+ red, green, blue = red
290
+ return ('%02x%02x%02x' % (int(round(red * RGBMAX)), int(round(green * RGBMAX)), int(round(blue * RGBMAX)))).upper()
291
+
292
+ def tint_luminance(tint, lum):
293
+ """Tints a HLSMAX based luminance"""
294
+ # See: http://ciintelligence.blogspot.co.uk/2012/02/converting-excel-theme-color-and-tint.html
295
+ if tint < 0:
296
+ return int(round(lum * (1.0 + tint)))
297
+ else:
298
+ return int(round(lum * (1.0 - tint) + (HLSMAX - HLSMAX * (1.0 - tint))))
visidata/loaders/xml.py CHANGED
@@ -1,11 +1,12 @@
1
- from visidata import VisiData, vd, Sheet, options, Column, Progress, setitem, ColumnAttr, vlen, RowColorizer, Path, copy
1
+ from copy import copy
2
+ from visidata import VisiData, vd, Sheet, options, Column, Progress, setitem, ColumnAttr, vlen, RowColorizer, Path
2
3
 
3
4
  vd.option('xml_parser_huge_tree', True, 'allow very deep trees and very long text content')
4
5
 
5
6
 
6
7
  @VisiData.api
7
8
  def open_xml(vd, p):
8
- return XmlSheet(p.name, source=p)
9
+ return XmlSheet(p.base_stem, source=p)
9
10
 
10
11
  VisiData.open_svg = VisiData.open_xml
11
12
 
@@ -47,9 +48,11 @@ class XmlSheet(Sheet):
47
48
 
48
49
  def iterload(self):
49
50
  if isinstance(self.source, Path):
51
+ vd.importExternal('lxml')
50
52
  from lxml import etree, objectify
51
53
  p = etree.XMLParser(**self.options.getall('xml_parser_'))
52
- self.root = etree.parse(self.source.open_text(encoding=self.options.encoding), parser=p)
54
+ with self.open_text_source() as fp:
55
+ self.root = etree.parse(fp, parser=p)
53
56
  objectify.deannotate(self.root, cleanup_namespaces=True)
54
57
  else: # elif isinstance(self.source, XmlElement):
55
58
  self.root = self.source
visidata/loaders/xword.py CHANGED
@@ -8,13 +8,13 @@ vd.option('color_xword_active', 'green', 'color of active clue')
8
8
 
9
9
  @VisiData.api
10
10
  def open_puz(vd, p):
11
- return PuzSheet(p.name, source=p)
11
+ return PuzSheet(p.base_stem, source=p)
12
12
 
13
13
  @VisiData.api
14
14
  def open_xd(vd, p):
15
15
  if p.is_dir():
16
- return CrosswordsSheet(p.name, source=p)
17
- return CrosswordSheet(p.name, source=p)
16
+ return CrosswordsSheet(p.base_stem, source=p)
17
+ return CrosswordSheet(p.base_stem, source=p)
18
18
 
19
19
 
20
20
  @VisiData.api
@@ -96,7 +96,7 @@ class PuzSheet(CrosswordSheet):
96
96
 
97
97
  @VisiData.api
98
98
  def save_xd(vd, p, vs):
99
- with p.open_text(mode='w', encoding='utf-8') as fp:
99
+ with p.open(mode='w', encoding='utf-8') as fp:
100
100
  fp.write(vs.xd.to_unicode())
101
101
 
102
102
 
visidata/loaders/yaml.py CHANGED
@@ -1,19 +1,29 @@
1
1
  from itertools import chain
2
2
 
3
- from visidata import VisiData, Progress, JsonSheet
3
+ from visidata import VisiData, Progress, JsonSheet, vd
4
4
 
5
5
 
6
6
  @VisiData.api
7
7
  def open_yml(vd, p):
8
- return YamlSheet(p.name, source=p)
8
+ return YamlSheet(p.base_stem, source=p)
9
9
 
10
10
  VisiData.open_yaml = VisiData.open_yml
11
11
 
12
12
  class YamlSheet(JsonSheet):
13
13
  def iterload(self):
14
- import yaml
15
- with self.source.open_text() as fp:
16
- documents = yaml.safe_load_all(fp)
14
+ yaml = vd.importExternal('yaml', 'PyYAML')
15
+
16
+ class PrettySafeLoader(yaml.SafeLoader):
17
+ def construct_python_tuple(self, node):
18
+ return tuple(self.construct_sequence(node))
19
+
20
+ PrettySafeLoader.add_constructor(
21
+ u'tag:yaml.org,2002:python/tuple',
22
+ PrettySafeLoader.construct_python_tuple
23
+ )
24
+
25
+ with self.source.open() as fp:
26
+ documents = yaml.load_all(fp, PrettySafeLoader)
17
27
 
18
28
  self.columns = []
19
29
  self._knownKeys.clear()