visidata 3.0.2__py3-none-any.whl → 3.1__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 (149) hide show
  1. visidata/__init__.py +12 -10
  2. visidata/_input.py +208 -202
  3. visidata/_open.py +4 -1
  4. visidata/_types.py +4 -3
  5. visidata/aggregators.py +88 -39
  6. visidata/apps/vdsql/_ibis.py +7 -11
  7. visidata/apps/vdsql/clickhouse.py +2 -2
  8. visidata/apps/vdsql/snowflake.py +1 -1
  9. visidata/apps/vgit/status.py +1 -1
  10. visidata/basesheet.py +11 -4
  11. visidata/canvas.py +54 -20
  12. visidata/clipboard.py +13 -6
  13. visidata/cliptext.py +7 -6
  14. visidata/cmdlog.py +40 -27
  15. visidata/column.py +14 -49
  16. visidata/ddw/regex.ddw +3 -2
  17. visidata/deprecated.py +14 -2
  18. visidata/desktop/visidata.desktop +2 -2
  19. visidata/editor.py +1 -0
  20. visidata/errors.py +1 -1
  21. visidata/experimental/sort_selected.py +54 -0
  22. visidata/expr.py +69 -18
  23. visidata/features/change_precision.py +1 -3
  24. visidata/features/cmdpalette.py +17 -2
  25. visidata/features/colorsheet.py +1 -1
  26. visidata/features/dedupe.py +3 -3
  27. visidata/features/go_col.py +71 -0
  28. visidata/features/graph_seaborn.py +1 -1
  29. visidata/features/join.py +20 -10
  30. visidata/features/layout.py +16 -3
  31. visidata/features/ping.py +16 -12
  32. visidata/features/regex.py +5 -5
  33. visidata/features/status_source.py +3 -1
  34. visidata/features/sysedit.py +1 -1
  35. visidata/features/transpose.py +2 -1
  36. visidata/features/type_ipaddr.py +2 -4
  37. visidata/features/unfurl.py +1 -0
  38. visidata/form.py +2 -2
  39. visidata/freqtbl.py +16 -11
  40. visidata/fuzzymatch.py +1 -0
  41. visidata/graph.py +163 -12
  42. visidata/guide.py +57 -24
  43. visidata/guides/ClipboardGuide.md +48 -0
  44. visidata/guides/ColumnsGuide.md +52 -0
  45. visidata/guides/CommandsSheet.md +28 -0
  46. visidata/guides/DirSheet.md +34 -0
  47. visidata/guides/ErrorsSheet.md +17 -0
  48. visidata/guides/FrequencyTable.md +42 -0
  49. visidata/guides/GrepSheet.md +28 -0
  50. visidata/guides/JsonSheet.md +38 -0
  51. visidata/guides/MacrosSheet.md +19 -0
  52. visidata/guides/MeltGuide.md +52 -0
  53. visidata/guides/MemorySheet.md +7 -0
  54. visidata/guides/MenuGuide.md +26 -0
  55. visidata/guides/ModifyGuide.md +38 -0
  56. visidata/guides/PivotGuide.md +71 -0
  57. visidata/guides/RegexGuide.md +107 -0
  58. visidata/guides/SelectionGuide.md +44 -0
  59. visidata/guides/SlideGuide.md +26 -0
  60. visidata/guides/SortGuide.md +0 -0
  61. visidata/guides/SplitpaneGuide.md +15 -0
  62. visidata/guides/TypesSheet.md +43 -0
  63. visidata/guides/XsvGuide.md +36 -0
  64. visidata/help.py +6 -6
  65. visidata/hint.py +2 -1
  66. visidata/indexsheet.py +2 -2
  67. visidata/interface.py +13 -14
  68. visidata/keys.py +4 -1
  69. visidata/loaders/api_airtable.py +1 -1
  70. visidata/loaders/archive.py +1 -1
  71. visidata/loaders/csv.py +9 -5
  72. visidata/loaders/eml.py +11 -6
  73. visidata/loaders/f5log.py +1 -0
  74. visidata/loaders/fec.py +18 -42
  75. visidata/loaders/fixed_width.py +19 -3
  76. visidata/loaders/grep.py +121 -0
  77. visidata/loaders/html.py +1 -0
  78. visidata/loaders/http.py +6 -1
  79. visidata/loaders/json.py +22 -4
  80. visidata/loaders/jsonla.py +8 -2
  81. visidata/loaders/mailbox.py +1 -0
  82. visidata/loaders/markdown.py +25 -6
  83. visidata/loaders/msgpack.py +19 -0
  84. visidata/loaders/npy.py +0 -1
  85. visidata/loaders/odf.py +18 -4
  86. visidata/loaders/orgmode.py +1 -1
  87. visidata/loaders/rec.py +6 -4
  88. visidata/loaders/sas.py +11 -4
  89. visidata/loaders/scrape.py +0 -1
  90. visidata/loaders/texttables.py +2 -0
  91. visidata/loaders/tsv.py +24 -7
  92. visidata/loaders/unzip_http.py +127 -3
  93. visidata/loaders/vds.py +4 -0
  94. visidata/loaders/vdx.py +1 -1
  95. visidata/loaders/xlsx.py +5 -0
  96. visidata/loaders/xml.py +2 -1
  97. visidata/macros.py +14 -31
  98. visidata/main.py +14 -13
  99. visidata/mainloop.py +14 -6
  100. visidata/man/vd.1 +72 -39
  101. visidata/man/vd.txt +72 -41
  102. visidata/memory.py +15 -4
  103. visidata/menu.py +14 -3
  104. visidata/metasheets.py +5 -6
  105. visidata/modify.py +4 -4
  106. visidata/mouse.py +2 -0
  107. visidata/movement.py +14 -28
  108. visidata/optionssheet.py +3 -5
  109. visidata/path.py +59 -37
  110. visidata/pivot.py +8 -5
  111. visidata/pyobj.py +63 -9
  112. visidata/save.py +16 -9
  113. visidata/search.py +4 -4
  114. visidata/selection.py +10 -56
  115. visidata/settings.py +37 -35
  116. visidata/sheets.py +186 -108
  117. visidata/shell.py +22 -12
  118. visidata/sidebar.py +71 -16
  119. visidata/sort.py +21 -6
  120. visidata/statusbar.py +42 -5
  121. visidata/stored_list.py +5 -2
  122. visidata/tests/conftest.py +1 -0
  123. visidata/tests/test_commands.py +9 -1
  124. visidata/tests/test_completer.py +18 -0
  125. visidata/tests/test_edittext.py +3 -2
  126. visidata/text_source.py +7 -4
  127. visidata/textsheet.py +20 -6
  128. visidata/themes/ascii8.py +9 -6
  129. visidata/themes/asciimono.py +14 -4
  130. visidata/threads.py +13 -3
  131. visidata/tuiwin.py +5 -1
  132. visidata/type_currency.py +1 -2
  133. visidata/type_date.py +6 -1
  134. visidata/undo.py +10 -5
  135. visidata/utils.py +9 -3
  136. visidata/vdobj.py +21 -1
  137. visidata/wrappers.py +9 -1
  138. {visidata-3.0.2.data → visidata-3.1.data}/data/share/applications/visidata.desktop +2 -2
  139. {visidata-3.0.2.data → visidata-3.1.data}/data/share/man/man1/vd.1 +72 -39
  140. {visidata-3.0.2.data → visidata-3.1.data}/data/share/man/man1/visidata.1 +72 -39
  141. {visidata-3.0.2.dist-info → visidata-3.1.dist-info}/METADATA +24 -6
  142. visidata-3.1.dist-info/RECORD +284 -0
  143. visidata-3.0.2.dist-info/RECORD +0 -258
  144. {visidata-3.0.2.data → visidata-3.1.data}/scripts/vd +0 -0
  145. {visidata-3.0.2.data → visidata-3.1.data}/scripts/vd2to3.vdx +0 -0
  146. {visidata-3.0.2.dist-info → visidata-3.1.dist-info}/LICENSE.gpl3 +0 -0
  147. {visidata-3.0.2.dist-info → visidata-3.1.dist-info}/WHEEL +0 -0
  148. {visidata-3.0.2.dist-info → visidata-3.1.dist-info}/entry_points.txt +0 -0
  149. {visidata-3.0.2.dist-info → visidata-3.1.dist-info}/top_level.txt +0 -0
visidata/metasheets.py CHANGED
@@ -29,10 +29,9 @@ Some new commands on this sheet operate on all selected columns on the source sh
29
29
  - {help.commands.type_int_selected}
30
30
  - or `g` with any standard type to set type of selected source columns to that type
31
31
 
32
- ## As usual
32
+ Other commands (not specific to Columns Sheet):
33
33
 
34
34
  - {help.commands.setcol_input}
35
- {sheet.help_columns}
36
35
  '''
37
36
 
38
37
  class ValueColumn(Column):
@@ -47,15 +46,15 @@ Some new commands on this sheet operate on all selected columns on the source sh
47
46
  ColumnAttr('name', help='rename the column on the source sheet'),
48
47
  ColumnAttr('keycol', type=int, width=0),
49
48
  ColumnAttr('width', type=int, help='set the column width (`0` to hide completely)'),
50
- ColumnAttr('height', type=int, max_help=1, help='set a maximum height for the row, if this column will fill it'),
49
+ ColumnAttr('height', type=int, disp_expert=1, help='set a maximum height for the row, if this column will fill it'),
51
50
  ColumnAttr('hoffset', type=int, width=0),
52
51
  ColumnAttr('voffset', type=int, width=0),
53
52
  ColumnAttr('type', 'typestr', help='convert all values to a specific type'),
54
53
  ColumnAttr('fmtstr', help='use a custom format string, either C-style (`%0.4f`) or Python-style (`{{:0.4f}}`)'),
55
- ColumnAttr('formatter', max_help=1, help='use a custom format function (**{col.help_formatters}**)'),
56
- ColumnAttr('displayer', max_help=1, help='use a custom display function (**{col.help_displayers}**)'),
54
+ ColumnAttr('formatter', disp_expert=1, help='use a custom format function (**{col.help_formatters}**)'),
55
+ ColumnAttr('displayer', disp_expert=1, help='use a custom display function (**{col.help_displayers}**)'),
57
56
  ValueColumn('value', help='change the value of this cell on the source sheet'),
58
- ColumnAttr('expr', max_help=1, help='change the main column parameter'),
57
+ ColumnAttr('expr', disp_expert=1, help='change the main column parameter'),
59
58
  ColumnAttr('ncalcs', type=int, width=0, cache=False),
60
59
  ColumnAttr('maxtime', type=float, width=0, cache=False),
61
60
  ColumnAttr('totaltime', type=float, width=0, cache=False),
visidata/modify.py CHANGED
@@ -65,7 +65,7 @@ def rowAdded(self, row):
65
65
  self._deferredAdds[self.rowid(row)] = row
66
66
  def _undoRowAdded(sheet, row):
67
67
  if sheet.rowid(row) not in sheet._deferredAdds:
68
- vd.warning('cannot undo to before commit')
68
+ vd.warning('cannot undo row addition after a commit')
69
69
  return
70
70
  del sheet._deferredAdds[sheet.rowid(row)]
71
71
  vd.addUndo(_undoRowAdded, self, row)
@@ -87,7 +87,7 @@ def cellChanged(col, row, val):
87
87
  if oldval == col.getSourceValue(row):
88
88
  # if we have reached the original value, remove from defermods entirely
89
89
  if col.sheet.rowid(row) not in col.sheet._deferredMods:
90
- vd.warning('cannot undo to before commit')
90
+ vd.warning('cannot undo cell change after a commit')
91
91
  return
92
92
  del col.sheet._deferredMods[col.sheet.rowid(row)]
93
93
  else:
@@ -105,7 +105,7 @@ def rowDeleted(self, row):
105
105
  self.unselectRow(row)
106
106
  def _undoRowDeleted(sheet, row):
107
107
  if sheet.rowid(row) not in sheet._deferredDels:
108
- vd.warning('cannot undo to before commit')
108
+ vd.warning('cannot undo row deletion after a commit')
109
109
  return
110
110
  del sheet._deferredDels[sheet.rowid(row)]
111
111
  vd.addUndo(_undoRowDeleted, self, row)
@@ -330,7 +330,7 @@ def new_rows(sheet, n):
330
330
  return [sheet.newRow() for i in range(n)]
331
331
 
332
332
  Sheet.addCommand('a', 'add-row', 'addRows([newRow()], index=cursorRowIndex); cursorDown(1)', 'append a blank row')
333
- Sheet.addCommand('ga', 'add-rows', 'n=int(input("add rows: ", value=1)); addRows(new_rows(n), index=cursorRowIndex); cursorDown(1)', 'append N blank rows')
333
+ Sheet.addCommand('ga', 'add-rows', 'n=int(input("add # rows: ", value=1)); addRows(new_rows(n), index=cursorRowIndex); cursorDown(1)', 'append N blank rows')
334
334
  Sheet.addCommand('za', 'addcol-new', 'addColumnAtCursor(SettableColumn(input("column name: ")))', 'append an empty column')
335
335
  Sheet.addCommand('gza', 'addcol-bulk', 'addColumnAtCursor(*(SettableColumn() for c in range(int(input("add columns: ")))))', 'append N empty columns')
336
336
 
visidata/mouse.py CHANGED
@@ -62,6 +62,8 @@ def parseMouse(vd, **kwargs):
62
62
  keystroke = clicktype + curses.mouseEvents.get(bstate, str(bstate))
63
63
  ret = AttrDict(keystroke=keystroke, y=y, x=x, found=[])
64
64
  for winname, winscr in kwargs.items():
65
+ if not winscr:
66
+ continue
65
67
  px, py = vd.getrootxy(winscr)
66
68
  mh, mw = winscr.getmaxyx()
67
69
  if py <= y < py+mh and px <= x < px+mw:
visidata/movement.py CHANGED
@@ -1,11 +1,10 @@
1
1
  import itertools
2
- import re
3
2
 
4
- from visidata import vd, VisiData, BaseSheet, Sheet, Column, Progress, ALT, asyncthread
3
+ from visidata import vd, BaseSheet, Sheet, Column, Progress, ALT, asyncthread
5
4
 
6
5
 
7
6
  def rotateRange(n, idx, reverse=False):
8
- 'Wraps an iter starting from idx. Yields indices from idx to n and then 0 to idx.'
7
+ 'Wraps an iter starting from idx. Yields indices from idx+1 to n and then 0 to idx, or from idx-1 to 0 and n-1 to idx.'
9
8
  if n == 0: return []
10
9
  if reverse:
11
10
  rng = range(idx-1, -1, -1)
@@ -81,18 +80,6 @@ def moveToNextRow(vs, func, reverse=False, msg='no different value up this colum
81
80
  vd.status(msg)
82
81
 
83
82
 
84
- @Sheet.api
85
- def nextColRegex(sheet, colregex):
86
- 'Go to first visible column after the cursor matching `colregex`.'
87
- pivot = sheet.cursorVisibleColIndex
88
- for i in itertools.chain(range(pivot+1, len(sheet.visibleCols)), range(0, pivot+1)):
89
- c = sheet.visibleCols[i]
90
- if re.search(colregex, c.name, sheet.regex_flags()):
91
- return i
92
-
93
- vd.fail('no column name matches /%s/' % colregex)
94
-
95
-
96
83
  @Column.property
97
84
  def visibleWidth(self):
98
85
  'Width of column as is displayed in terminal'
@@ -102,20 +89,18 @@ def visibleWidth(self):
102
89
  return self.sheet._visibleColLayout[vcolidx][1]
103
90
 
104
91
 
105
- Sheet.addCommand(None, 'go-left', 'cursorRight(-1)', 'go left')
106
- Sheet.addCommand(None, 'go-down', 'cursorDown(+1)', 'go down')
107
- Sheet.addCommand(None, 'go-up', 'cursorDown(-1)', 'go up')
108
- Sheet.addCommand(None, 'go-right', 'cursorRight(+1)', 'go right')
109
- Sheet.addCommand(None, 'go-pagedown', 'cursorDown(nScreenRows-1); sheet.topRowIndex = bottomRowIndex', 'scroll one page forward')
110
- Sheet.addCommand(None, 'go-pageup', 'cursorDown(-nScreenRows+1); sheet.bottomRowIndex = topRowIndex', 'scroll one page backward')
92
+ Sheet.addCommand(None, 'go-left', 'cursorRight(-1)', 'go left', replay=False)
93
+ Sheet.addCommand(None, 'go-down', 'cursorDown(+1)', 'go down', replay=False)
94
+ Sheet.addCommand(None, 'go-up', 'cursorDown(-1)', 'go up', replay=False)
95
+ Sheet.addCommand(None, 'go-right', 'cursorRight(+1)', 'go right', replay=False)
96
+ Sheet.addCommand(None, 'go-pagedown', 'cursorDown(nScreenRows-1); sheet.topRowIndex = bottomRowIndex', 'scroll one page forward', replay=False)
97
+ Sheet.addCommand(None, 'go-pageup', 'cursorDown(-nScreenRows+1); sheet.bottomRowIndex = topRowIndex', 'scroll one page backward', replay=False)
111
98
 
112
99
  Sheet.addCommand(None, 'go-leftmost', 'sheet.cursorVisibleColIndex = sheet.leftVisibleColIndex = 0', 'go all the way to the left of sheet')
113
100
  Sheet.addCommand(None, 'go-top', 'sheet.cursorRowIndex = sheet.topRowIndex = 0', 'go all the way to the top of sheet')
114
- Sheet.addCommand(None, 'go-bottom', 'sheet.cursorRowIndex = len(rows); sheet.topRowIndex = cursorRowIndex-nScreenRows', 'go all the way to the bottom of sheet')
101
+ Sheet.addCommand(None, 'go-bottom', 'sheet.cursorRowIndex = sheet.bottomRowIndex = len(rows)-1', 'go all the way to the bottom of sheet')
115
102
  Sheet.addCommand(None, 'go-rightmost', 'sheet.leftVisibleColIndex = len(visibleCols)-1; pageLeft(); sheet.cursorVisibleColIndex = len(visibleCols)-1', 'go all the way to the right of sheet')
116
103
 
117
- Sheet.addCommand('c', 'go-col-regex', 'sheet.cursorVisibleColIndex=nextColRegex(inputRegex("column name regex: ", type="regex-col", defaultLast=True))', 'go to next column with name matching regex')
118
- Sheet.addCommand('zc', 'go-col-number', 'sheet.cursorVisibleColIndex = int(input("move to column number: "))', 'go to given column number (0-based)')
119
104
  Sheet.addCommand('zr', 'go-row-number', 'sheet.cursorRowIndex = int(input("move to row number: "))', 'go to the given row number (0-based)')
120
105
 
121
106
 
@@ -130,6 +115,9 @@ Sheet.addCommand('z>', 'go-next-null', 'moveToNextRow(lambda row,col=cursorCol,i
130
115
  for i in range(1, 11):
131
116
  BaseSheet.addCommand(ALT+str(i)[-1], 'jump-sheet-'+str(i), f'vd.push(*(list(s for s in allSheets if s.shortcut==str({i})) or fail("no sheet")))', f'jump to sheet {i}')
132
117
 
118
+ for i in range(11, 21):
119
+ BaseSheet.addCommand('', 'jump-sheet-'+str(i), f'vd.push(*(list(s for s in allSheets if s.shortcut==str({i})) or fail("no sheet")))', f'jump to sheet {i}')
120
+
133
121
  BaseSheet.bindkey('KEY_LEFT', 'go-left')
134
122
  BaseSheet.bindkey('KEY_DOWN', 'go-down')
135
123
  BaseSheet.bindkey('KEY_UP', 'go-up')
@@ -150,8 +138,8 @@ Sheet.bindkey('BUTTON3_PRESSED', 'go-mouse')
150
138
  # vim-style scrolling with the 'z' prefix
151
139
  Sheet.addCommand('zz', 'scroll-middle', 'sheet.topRowIndex = cursorRowIndex-int(nScreenRows/2)', 'scroll current row to center of screen')
152
140
 
153
- Sheet.addCommand('kRIT5', 'go-right-page', 'sheet.cursorVisibleColIndex = sheet.leftVisibleColIndex = rightVisibleColIndex', 'scroll cursor one page right')
154
- Sheet.addCommand('kLFT5', 'go-left-page', 'pageLeft()', 'scroll cursor one page left')
141
+ Sheet.addCommand('kRIT5', 'go-right-page', 'sheet.cursorVisibleColIndex = sheet.leftVisibleColIndex = rightVisibleColIndex', 'scroll cursor one page right', replay=False)
142
+ Sheet.addCommand('kLFT5', 'go-left-page', 'pageLeft()', 'scroll cursor one page left', replay=False)
155
143
  Sheet.addCommand(None, 'scroll-left', 'sheet.cursorVisibleColIndex -= options.scroll_incr', 'scroll one column left')
156
144
  Sheet.addCommand(None, 'scroll-right', 'sheet.cursorVisibleColIndex += options.scroll_incr', 'scroll one column right')
157
145
  Sheet.addCommand(None, 'scroll-leftmost', 'sheet.leftVisibleColIndex = cursorVisibleColIndex', 'scroll sheet to leftmost column')
@@ -207,8 +195,6 @@ vd.addGlobals({'rotateRange': rotateRange})
207
195
  vd.addMenuItems('''
208
196
  View > Other sheet > previous sheet > jump-prev
209
197
  View > Other sheet > first sheet > jump-first
210
- Column > Goto > by number > go-col-number
211
- Column > Goto > by name > go-col-regex
212
198
  Row > Goto > top > go-top
213
199
  Row > Goto > bottom > go-bottom
214
200
  Row > Goto > previous > page > go-pageup
visidata/optionssheet.py CHANGED
@@ -17,14 +17,14 @@ class OptionsSheet(Sheet):
17
17
  precious = False
18
18
  columns = (
19
19
  Column('option', getter=lambda col,row: row.name),
20
- Column('module', getter=lambda col,row: row.module, max_help=1),
20
+ Column('module', getter=lambda col,row: row.module, disp_expert=1),
21
21
  Column('value',
22
22
  getter=lambda col,row: col.sheet.diffOption(row.name),
23
- setter=lambda col,row,val: col.sheet.source.options.set(row.name, val)
23
+ setter=lambda col,row,val: vd.options.set(row.name, val, col.sheet.source)
24
24
  ),
25
25
  Column('default', getter=lambda col,row: vd.options.getdefault(row.name)),
26
26
  Column('description', width=40, getter=lambda col,row: vd.options._get(row.name, 'default').helpstr),
27
- AttrColumn('replayable', max_help=1),
27
+ AttrColumn('replayable', disp_expert=1),
28
28
  )
29
29
  colorizers = [
30
30
  CellColorizer(3, None, lambda s,c,r,v: v.value if r and c in s.columns[2:4] and r.name.startswith('color_') else None),
@@ -73,8 +73,6 @@ class OptionsSheet(Sheet):
73
73
  def iterload(self):
74
74
  for k in vd.options.keys():
75
75
  v = vd.options._get(k)
76
- # if vd.options.disp_help > v.max_help:
77
- # continue
78
76
  if v.sheettype in [None, BaseSheet]:
79
77
  yield v
80
78
  elif self.source != 'global' and v.sheettype in self.source.superclasses():
visidata/path.py CHANGED
@@ -111,6 +111,7 @@ class FileProgress:
111
111
 
112
112
  # track Progress on original fp
113
113
  self.fp_orig_read = self.fp.read
114
+ self.fp_orig_readline = self.fp.readline
114
115
  self.fp_orig_close = self.fp.close
115
116
 
116
117
  self.fp.read = self.read
@@ -132,6 +133,12 @@ class FileProgress:
132
133
  self.prog.addProgress(len(r))
133
134
  return r
134
135
 
136
+ def readline(self, size=-1):
137
+ r = self.fp_orig_readline(size)
138
+ if self.prog:
139
+ self.prog.addProgress(len(r))
140
+ return r
141
+
135
142
  def __getattr__(self, k):
136
143
  return getattr(self.fp, k)
137
144
 
@@ -172,10 +179,6 @@ class Path(os.PathLike):
172
179
  'Filename without any extensions. Not the same as pathlib.Path.'
173
180
  return self.base_stem
174
181
 
175
- @lru_cache()
176
- def stat(self, force=False):
177
- return self._path.stat()
178
-
179
182
  @property
180
183
  def given(self):
181
184
  'The path as given to the constructor.'
@@ -222,7 +225,7 @@ class Path(os.PathLike):
222
225
  return self._path.__fspath__()
223
226
 
224
227
  def __lt__(self, a):
225
- if isinstance(a, visidata.Path):
228
+ if isinstance(a, Path):
226
229
  return self._path.__lt__(a._path)
227
230
  return self._path.__lt__(a)
228
231
 
@@ -234,27 +237,50 @@ class Path(os.PathLike):
234
237
  return bool(self.fp or self.fptext)
235
238
 
236
239
  def open(self, mode='rt', encoding=None, encoding_errors=None, newline=None):
237
- 'Open path in text or binary mode, using options.encoding and options.encoding_errors. Return open file-pointer or file-pointer-like.'
240
+ if 'b' in mode:
241
+ return self.open_bytes(mode)
242
+
243
+ return self.open_text(mode=mode, encoding=encoding, encoding_errors=encoding_errors, newline=newline)
244
+
245
+ def open_bytes(self, mode='rb'):
246
+ 'Open the file pointed by this path and return a file object in binary mode.'
247
+ if self.rfile:
248
+ raise ValueError('a RepeatFile holds text and cannot be reopened in binary mode')
249
+
250
+ if 'b' not in mode:
251
+ mode += 'b'
252
+
253
+ if self.given == '-':
254
+ if 'r' in mode:
255
+ return os.fdopen(vd._stdin.fileno(), 'rb')
256
+ elif 'w' in mode or 'a' in mode:
257
+ # convert 'a' to 'w' for stdout: https://bugs.python.org/issue27805
258
+ return os.dup(vd._stdout.fileno())
259
+ else:
260
+ vd.error('invalid mode "%s" for Path.open()' % mode)
261
+ return sys.stderr
262
+
263
+ return self._open(mode=mode)
264
+
265
+ def open_text(self, mode='rt', encoding=None, encoding_errors=None, newline=None):
266
+ 'Open path in text mode, using options.encoding and options.encoding_errors. Return open file-pointer or file-pointer-like.'
238
267
  # rfile makes a single-access fp reusable
239
268
 
269
+ if 't' not in mode:
270
+ mode += 't'
271
+
240
272
  if self.rfile:
241
- if 'b' in mode:
242
- raise ValueError('a RepeatFile holds text and cannot be reopened in binary mode')
243
273
  return self.rfile.reopen()
244
274
 
245
- if self.fp:
246
- if 'b' not in mode:
247
- self.fptext = codecs.iterdecode(self.fp,
248
- encoding=encoding or vd.options.encoding,
249
- errors=encoding_errors or vd.options.encoding_errors)
275
+ if self.fp and not self.fptext:
276
+ self.fptext = codecs.iterdecode(self.fp,
277
+ encoding=encoding or vd.options.encoding,
278
+ errors=encoding_errors or vd.options.encoding_errors)
250
279
 
251
280
  if self.fptext:
252
281
  self.rfile = RepeatFile(self.fptext)
253
282
  return self.rfile
254
283
 
255
- if 't' not in mode and 'b' not in mode:
256
- mode += 't'
257
-
258
284
  if self.given == '-':
259
285
  if 'r' in mode:
260
286
  return vd._stdin
@@ -265,10 +291,7 @@ class Path(os.PathLike):
265
291
  vd.error('invalid mode "%s" for Path.open()' % mode)
266
292
  return sys.stderr
267
293
 
268
- if 'b' in mode:
269
- return self._open(mode=mode)
270
- else:
271
- return self._open(mode=mode, encoding=encoding or vd.options.encoding, errors=vd.options.encoding_errors, newline=newline)
294
+ return self._open(mode=mode, encoding=encoding or vd.options.encoding, errors=vd.options.encoding_errors, newline=newline)
272
295
 
273
296
  @wraps(pathlib.Path.read_text)
274
297
  def read_text(self, *args, **kwargs):
@@ -282,6 +305,8 @@ class Path(os.PathLike):
282
305
  return RepeatFile(self.lines).read()
283
306
  elif self.fp:
284
307
  return self.fp.read()
308
+ elif self.fptext:
309
+ return self.fptext.read()
285
310
  else:
286
311
  return self._path.read_text(*args, **kwargs)
287
312
 
@@ -324,12 +349,6 @@ class Path(os.PathLike):
324
349
  prog.addProgress(len(line))
325
350
  yield line.rstrip('\n')
326
351
 
327
- def open_bytes(self, mode='rb'):
328
- 'Open the file pointed by this path and return a file object in binary mode.'
329
- if 'b' not in mode:
330
- mode += 'b'
331
- return self.open(mode=mode) #1880
332
-
333
352
  def read_bytes(self):
334
353
  'Return the entire binary contents of the pointed-to file as a bytes object.'
335
354
  with self.open(mode='rb') as fp:
@@ -337,12 +356,12 @@ class Path(os.PathLike):
337
356
 
338
357
  @wraps(pathlib.Path.is_fifo)
339
358
  def is_fifo(self):
340
- 'Return True if the path is a file.'
359
+ 'Return True if the path is a fifo.'
341
360
  return self._path.is_fifo()
342
361
 
343
362
  def is_local(self):
344
363
  'Return True if self.filename refers to a file on the local disk.'
345
- return not bool(self.fp) and not bool(self.fptext)
364
+ return not bool(self.is_url()) and not bool(self.fp) and not bool(self.fptext)
346
365
 
347
366
  def is_url(self):
348
367
  'Return True if the given path appears to be a URL.'
@@ -406,21 +425,24 @@ class RepeatFile:
406
425
  return RepeatFile(self.iter_lines, lines=self.lines)
407
426
 
408
427
  def read(self, n=None):
409
- r = None
428
+ '''Returns a string or bytes object. Unlike the standard read() function, when *n* is given, more than *n* characters/bytes can be returned, and often will.'''
410
429
  if n is None:
411
430
  n = 10**12 # some too huge number
412
- while r is None or len(r) < n:
431
+ r = []
432
+ size = 0
433
+ output_type = str; eol = '\n'; joiner = ''
434
+ while not r or size < n:
413
435
  try:
414
436
  s = next(self.iter)
415
- if r is None:
416
- r = '' if isinstance(s, str) else b''
417
- else:
418
- assert isinstance(r, type(s)), (r, type(s))
419
-
420
- r += s + '\n' if isinstance(s, str) else b'\n'
437
+ if not r and isinstance(s, bytes):
438
+ output_type = bytes; eol = b'\n'; joiner = b''
439
+ assert isinstance(s, output_type), (s, output_type)
440
+ r.append(s)
441
+ r.append(eol)
442
+ size += len(s) + len(eol)
421
443
  except StopIteration:
422
444
  break # end of file
423
- return r or ''
445
+ return joiner.join(r)
424
446
 
425
447
  def write(self, s):
426
448
  return self.iter_lines.write(s)
visidata/pivot.py CHANGED
@@ -53,7 +53,7 @@ class AggrColumn(Column):
53
53
  def calcValue(col, row):
54
54
  if col.sheet.loading:
55
55
  return visidata.INPROGRESS
56
- return col.aggregator(col.origCol, row.sourcerows)
56
+ return col.aggregator.aggregate(col.origCol, row.sourcerows)
57
57
 
58
58
 
59
59
  def makeAggrColumn(aggcol, aggregator):
@@ -175,13 +175,13 @@ class PivotSheet(Sheet):
175
175
  c = Column(colname,
176
176
  type=aggregator.type or aggcol.type,
177
177
  aggvalue=value,
178
- getter=lambda col,row,aggcol=aggcol,agg=aggregator: agg(aggcol, row.pivotrows.get(col.aggvalue, [])))
178
+ getter=lambda col,row,aggcol=aggcol,agg=aggregator: agg.aggregate(aggcol, row.pivotrows.get(col.aggvalue, [])))
179
179
  self.addColumn(c)
180
180
 
181
181
  # if aggregator.name != 'count': # already have count above
182
182
  # c = Column('Total_' + aggcol.name,
183
183
  # type=aggregator.type or aggcol.type,
184
- # getter=lambda col,row,aggcol=aggcol,agg=aggregator: agg(aggcol, row.sourcerows))
184
+ # getter=lambda col,row,aggcol=aggcol,agg=aggregator: agg.aggregate(aggcol, row.sourcerows))
185
185
  # self.addColumn(c)
186
186
 
187
187
  @asyncthread
@@ -291,8 +291,11 @@ class PivotSheet(Sheet):
291
291
  @PivotSheet.api
292
292
  def addcol_aggr(sheet, col):
293
293
  hasattr(col, 'origCol') or vd.fail('not an aggregation column')
294
- for agg in vd.chooseAggregators():
295
- sheet.addColumnAtCursor(makeAggrColumn(col.origCol, vd.aggregators[agg]))
294
+ for agg_choice in vd.chooseAggregators():
295
+ agg_or_list = vd.aggregators[agg_choice]
296
+ aggs = agg_or_list if isinstance(agg_or_list, list) else [agg_or_list]
297
+ for agg in aggs:
298
+ sheet.addColumnAtCursor(makeAggrColumn(col.origCol, vd.aggregators[agg]))
296
299
 
297
300
 
298
301
  Sheet.addCommand('W', 'pivot', 'vd.push(makePivot(sheet, keyCols, [cursorCol]))', 'open Pivot Table: group rows by key column and summarize current column')
visidata/pyobj.py CHANGED
@@ -1,9 +1,10 @@
1
1
  from typing import Mapping
2
2
  import inspect
3
3
  import math
4
+ import numbers
4
5
 
5
6
  from visidata import vd, asyncthread, ENTER, deduceType
6
- from visidata import Sheet, Column, VisiData, ColumnItem, TableSheet, BaseSheet, Progress, ColumnAttr, SuspendCurses, TextSheet
7
+ from visidata import Sheet, Column, VisiData, ColumnItem, TableSheet, BaseSheet, Progress, ColumnAttr, SuspendCurses, TextSheet, setitem
7
8
  import visidata
8
9
 
9
10
  vd.option('visibility', 0, 'visibility level')
@@ -14,8 +15,30 @@ vd.option('fmt_expand_list', '%s[%s]', 'format str to use for names of columns e
14
15
 
15
16
  class PythonSheet(Sheet):
16
17
  def openRow(self, row):
17
- return PyobjSheet("%s[%s]" % (self.name, self.keystr(row)), source=row)
18
+ return PyobjSheet("%s[%s]" % (self.name, self.rowname(row)), source=row)
18
19
 
20
+ class PythonAtomSheet(PythonSheet):
21
+ '''a sheet to display one Python object that does not offer deeper inspection,
22
+ like None, a bool, or an int/float'''
23
+ rowtype = 'object' #singular, because it should only ever hold one
24
+ columns = [
25
+ Column('value', getter=lambda col,row: row,
26
+ setter=lambda c,r,v: None)
27
+ ]
28
+ def loader(self):
29
+ self.rows = [self.source]
30
+ self.column('value').type = deduceType(self.source)
31
+
32
+ def openRow(self, row):
33
+ vd.fail('cannot dive deeper on this object')
34
+ def openCell(self, col, row, rowidx=None):
35
+ vd.fail('cannot dive deeper on this object')
36
+ def openRowPyobj(self, rowidx):
37
+ vd.fail('cannot dive deeper on this object')
38
+ def openCellPyobj(self, col, rowidx):
39
+ vd.fail('cannot dive deeper on this object')
40
+ def newRow(self):
41
+ vd.fail('adding rows to this sheet is not supported')
19
42
 
20
43
  #### generic list/dict/object browsing
21
44
  @VisiData.global_api
@@ -152,17 +175,23 @@ class PyobjSheet(PythonSheet):
152
175
  def __new__(cls, *names, **kwargs):
153
176
  'Return Sheet object of appropriate type for given sources in `args`.'
154
177
  pyobj=kwargs.get('source', object())
155
- if isinstance(pyobj, list) or isinstance(pyobj, tuple):
178
+ if pyobj is None:
179
+ return None
180
+ elif isinstance(pyobj, numbers.Number):
181
+ return PythonAtomSheet(*names, source=pyobj)
182
+ elif isinstance(pyobj, (list, tuple)):
156
183
  if getattr(pyobj, '_fields', None): # list of namedtuple
157
184
  return SheetNamedTuple(*names, **kwargs)
158
185
  else:
159
186
  return SheetList(*names, **kwargs)
187
+ elif isinstance(pyobj, set):
188
+ return ListOfPyobjSheet(*names, source=list(pyobj))
160
189
  elif isinstance(pyobj, Mapping):
161
190
  return SheetDict(*names, **kwargs)
162
191
  elif isinstance(pyobj, str):
163
192
  return TextSheet(*names, source=pyobj.splitlines())
164
193
  elif isinstance(pyobj, bytes):
165
- return TextSheet(*names, source=pyobj.decode(options.encoding).splitlines())
194
+ return TextSheet(*names, source=pyobj.decode(cls.options.encoding).splitlines())
166
195
  elif isinstance(pyobj, object):
167
196
  obj = super().__new__(cls) #, *names, **kwargs)
168
197
  return obj
@@ -173,6 +202,10 @@ class PyobjSheet(PythonSheet):
173
202
  self.rows = []
174
203
  vislevel = self.options.visibility
175
204
  for r in dir(self.source):
205
+ # reading these attributes can cause distracting fail() messages
206
+ if r in ('onlySelectedRows', 'someSelectedRows'):
207
+ vd.warning(f'skipping attribute: {r}')
208
+ continue
176
209
  try:
177
210
  if vislevel <= 2 and r.startswith('__'): continue
178
211
  if vislevel <= 1 and r.startswith('_'): continue
@@ -191,8 +224,9 @@ class PyobjSheet(PythonSheet):
191
224
  @TableSheet.api
192
225
  def openRow(sheet, row, rowidx=None):
193
226
  'Return Sheet diving into *row*.'
227
+ if row is None or sheet.nRows == 0: vd.fail('no row to dive into')
194
228
  if rowidx is None:
195
- k = sheet.keystr(row) or str(sheet.cursorRowIndex)
229
+ k = sheet.rowname(row) or str(sheet.cursorRowIndex)
196
230
  else:
197
231
  k = rowidx
198
232
 
@@ -209,21 +243,40 @@ def openRow(sheet, row, rowidx=None):
209
243
  @TableSheet.api
210
244
  def openCell(sheet, col, row, rowidx=None):
211
245
  'Return Sheet diving into cell at *row* in *col*.'
246
+ if col is None or row is None or sheet.nRows == 0: vd.fail('no cell to dive into')
212
247
  if rowidx is None:
213
- k = sheet.keystr(row) or str(sheet.cursorRowIndex)
248
+ k = sheet.rowname(row) or str(sheet.cursorRowIndex)
214
249
  else:
215
250
  k = rowidx
216
251
  name = f'{sheet.name}[{k}].{col.name}'
217
252
  return PyobjSheet(name, source=col.getTypedValue(row))
218
253
 
254
+ @Sheet.api
255
+ @asyncthread
256
+ def openRows(sheet, rows):
257
+ for r in Progress(rows):
258
+ vd.push(sheet.openRow(r))
259
+
260
+ vd.sync()
261
+
262
+ @Sheet.api
263
+ @asyncthread
264
+ def openCells(sheet, col, rows):
265
+ for r in Progress(rows):
266
+ vd.push(openCell(col, r))
267
+
268
+ vd.sync()
269
+
219
270
  @TableSheet.api
220
271
  def openRowPyobj(sheet, rowidx):
221
272
  'Return Sheet of raw Python object of row.'
273
+ if sheet.nRows == 0: vd.fail('no row to dive into')
222
274
  return PyobjSheet("%s[%s]" % (sheet.name, rowidx), source=sheet.rows[rowidx])
223
275
 
224
276
  @TableSheet.api
225
277
  def openCellPyobj(sheet, col, rowidx):
226
278
  'Return Sheet of raw Python object of cell.'
279
+ if col is None or sheet.nRows == 0: vd.fail('no cell to dive into')
227
280
  name = f'{sheet.name}[{rowidx}].{col.name}'
228
281
  return PyobjSheet(name, source=col.getValue(sheet.rows[rowidx]))
229
282
 
@@ -249,10 +302,11 @@ Sheet.addCommand('z^Y', 'pyobj-cell', 'status(type(cursorValue).__name__); vd.pu
249
302
  BaseSheet.addCommand('g^Y', 'pyobj-sheet', 'status(type(sheet).__name__); vd.push(PyobjSheet(sheet.name+"_sheet", source=sheet))', 'open current sheet as Python object')
250
303
 
251
304
  Sheet.addCommand('', 'open-row-basic', 'vd.push(TableSheet.openRow(sheet, cursorRow))', 'dive into current row as basic table (ignoring subsheet dive)')
252
- Sheet.addCommand(ENTER, 'open-row', 'vd.push(openRow(cursorRow))', 'open current row with sheet-specific dive')
305
+ Sheet.addCommand(ENTER, 'open-row', 'vd.push(openRow(cursorRow)) if cursorRow else vd.fail("no row to open")', 'open current row with sheet-specific dive')
253
306
  Sheet.addCommand('z'+ENTER, 'open-cell', 'vd.push(openCell(cursorCol, cursorRow))', 'open sheet with copies of rows referenced in current cell')
254
- Sheet.addCommand('g'+ENTER, 'dive-selected', 'for r in selectedRows: vd.push(openRow(r))', 'open sheet with copies of rows referenced in selected rows')
255
- Sheet.addCommand('gz'+ENTER, 'dive-selected-cells', 'for r in selectedRows: vd.push(openCell(cursorCol, r))', 'open sheet with copies of rows referenced in selected rows')
307
+ openRows
308
+ Sheet.addCommand('g'+ENTER, 'dive-selected', 'openRows(selectedRows)', 'open all selected rows')
309
+ Sheet.addCommand('gz'+ENTER, 'dive-selected-cells', 'openCells(cursorCol, selectedRows)', 'open all selected cells')
256
310
 
257
311
  PyobjSheet.addCommand('v', 'visibility', 'sheet.options.visibility = 0 if sheet.options.visibility else 2; reload()', 'toggle show/hide for methods and hidden properties')
258
312
  PyobjSheet.addCommand('gv', 'show-hidden', 'sheet.options.visibility = 2; reload()', 'show methods and hidden properties')
visidata/save.py CHANGED
@@ -3,7 +3,7 @@ import os
3
3
  from copy import copy
4
4
 
5
5
  from visidata import vd
6
- from visidata import Sheet, BaseSheet, VisiData, IndexSheet, Path, Progress, TypedExceptionWrapper
6
+ from visidata import Sheet, BaseSheet, VisiData, IndexSheet, Path, Progress, TypedExceptionWrapper, TypedWrapper, UNLOADED
7
7
 
8
8
  vd.option('safe_error', '#ERR', 'error string to use while saving', replay=True)
9
9
  vd.option('save_encoding', 'utf-8', 'encoding passed to codecs.open when saving a file', replay=True, help=vd.help_encoding)
@@ -13,12 +13,15 @@ def safe_trdict(vs):
13
13
  'returns string.translate dictionary for replacing tabs and newlines'
14
14
  if vs.options.safety_first:
15
15
  delim = vs.options.delimiter
16
- return {
16
+ trdict = {
17
17
  0: '', # strip NUL completely
18
- ord(delim): vs.options.tsv_safe_tab, # \t
19
18
  10: vs.options.tsv_safe_newline, # \n
20
19
  13: vs.options.tsv_safe_newline, # \r
21
20
  }
21
+ if not delim or ord(delim) in trdict:
22
+ vd.fail(f'cannot use delimiter {repr(delim)} with safety_first')
23
+ trdict[ord(delim)] = vs.options.tsv_safe_tab # \t
24
+ return trdict
22
25
  return {}
23
26
 
24
27
 
@@ -29,12 +32,12 @@ def iterdispvals(sheet, *cols, format=False):
29
32
  cols = sheet.visibleCols
30
33
 
31
34
  transformers = collections.OrderedDict() # list of transformers for each column in order
35
+ trdict = sheet.safe_trdict()
32
36
  for col in cols:
33
37
  transformers[col] = [ col.type ]
34
38
  if format:
35
39
  formatMaker = getattr(col, 'formatter_'+(col.formatter or sheet.options.disp_formatter))
36
40
  transformers[col].append(formatMaker(col._formatdict))
37
- trdict = sheet.safe_trdict()
38
41
  if trdict:
39
42
  transformers[col].append(lambda v,trdict=trdict: v.translate(trdict))
40
43
 
@@ -46,7 +49,6 @@ def iterdispvals(sheet, *cols, format=False):
46
49
  dispval = col.getValue(r)
47
50
 
48
51
  except Exception as e:
49
- vd.exceptionCaught(e)
50
52
  dispval = options_safe_error or str(e)
51
53
 
52
54
  try:
@@ -56,6 +58,9 @@ def iterdispvals(sheet, *cols, format=False):
56
58
  elif isinstance(dispval, TypedExceptionWrapper):
57
59
  dispval = options_safe_error or str(dispval)
58
60
  break
61
+ elif isinstance(dispval, TypedWrapper):
62
+ dispval = ''
63
+ break
59
64
  else:
60
65
  dispval = t(dispval)
61
66
 
@@ -89,7 +94,7 @@ def getDefaultSaveName(sheet):
89
94
 
90
95
 
91
96
  @VisiData.api
92
- def save_cols(vd, cols):
97
+ def saveCols(vd, cols):
93
98
  sheet = cols[0].sheet
94
99
  vs = copy(sheet)
95
100
  vs.columns = list(cols)
@@ -109,8 +114,10 @@ def saveSheets(vd, givenpath, *vsheets, confirm_overwrite=True):
109
114
  if not vsheets: # blank tuple
110
115
  vd.warning('no sheets to save')
111
116
  return
117
+ unloaded = [ vs for vs in vsheets if vs.rows is UNLOADED ]
118
+ vd.sync(*vd.ensureLoaded(unloaded))
112
119
 
113
- filetypes = [givenpath.ext, vd.options.save_filetype]
120
+ filetypes = [givenpath.ext.lower(), vd.options.save_filetype.lower()]
114
121
 
115
122
  vd.clearCaches()
116
123
 
@@ -203,8 +210,8 @@ BaseSheet.addCommand('', 'save-sheet-really', 'vd.saveSheets(Path(getDefaultSave
203
210
  BaseSheet.addCommand('', 'save-source', 'vd.saveSheets(rootSheet().source, rootSheet())', 'save root sheet to its source')
204
211
  BaseSheet.addCommand('g^S', 'save-all', 'vd.saveSheets(inputPath("save all sheets to: "), *vd.stackedSheets)', 'save all sheets to given file or directory)')
205
212
  IndexSheet.addCommand('g^S', 'save-selected', 'vd.saveSheets(inputPath("save %d sheets to: " % nSelectedRows, value="_".join(getattr(vs, "name", None) or "blank" for vs in selectedRows)), *selectedRows)', 'save all selected sheets to given file or directory')
206
- Sheet.addCommand('', 'save-col', 'save_cols([cursorCol])', 'save current column only to filename in format determined by extension (default .tsv)')
207
- Sheet.addCommand('', 'save-col-keys', 'save_cols(keyCols + [cursorCol])', 'save key columns and current column to filename in format determined by extension (default .tsv)')
213
+ Sheet.addCommand('', 'save-col', 'saveCols([cursorCol])', 'save current column only to filename in format determined by extension (default .tsv)')
214
+ Sheet.addCommand('', 'save-col-keys', 'saveCols(keyCols + [cursorCol])', 'save key columns and current column to filename in format determined by extension (default .tsv)')
208
215
 
209
216
  vd.addMenuItems('''
210
217
  File > Save > current sheet > save-sheet