prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  Bug: cannot read .vds with expression column
  **Small description**
  
  Visidata cannot read back sheet(s) it saved as `.vds` if they contain an
  expression column.
  
  "KeyError: 'ExprColumn'" shows as error, resulting in a partial read.
  
  
  **Expected result**
  
  It should be able to read those files.
  
  
  **Actual result with ~~screenshot~~ stacktrace**
  
  ```
  Traceback (most recent call last):
    File "/nix/store/z4xjb4j8i73894r2wqjvlnps9j60rjr0-visidata-2.11/lib/python3.10/site-packages/visidata/threads.py", line 198, in _toplevelTryFunc
      t.status = func(*args, **kwargs)
    File "/nix/store/z4xjb4j8i73894r2wqjvlnps9j60rjr0-visidata-2.11/lib/python3.10/site-packages/visidata/pyobj.py", line 26, in reload
      for r in self.iterload():
    File "/nix/store/z4xjb4j8i73894r2wqjvlnps9j60rjr0-visidata-2.11/lib/python3.10/site-packages/visidata/loaders/vds.py", line 76, in iterload
      c = globals()[classname](d.pop('name'), sheet=self)
  KeyError: 'ExprColumn'
  ```
  
  
  **Steps to reproduce with sample data and a .vd**
  
  Create and save some test sheet with an expr column with this `cmdlog.vdj`:
  
  ```
  #!vd -p
  {"col": "", "row": "", "longname": "open-new", "input": "", "keystrokes": "Shift+A", "comment": "Open new empty sheet"}
  {"sheet": "unnamed", "col": "A", "row": "", "longname": "type-int", "input": "", "keystrokes": "#", "comment": "set type of current column to int"}
  {"sheet": "unnamed", "col": "", "row": "", "longname": "add-row", "input": "", "keystrokes": "a", "comment": "append a blank row"}
  {"sheet": "unnamed", "col": "A", "row": "0", "longname": "edit-cell", "input": "2", "keystrokes": "e", "comment": "edit contents of current cell"}
  {"sheet": "unnamed", "col": "A", "row": "", "longname": "addcol-expr", "input": "A*2", "keystrokes": "=", "comment": "create new column from Python expression, with column names as variables"}
  {"sheet": "unnamed", "col": "", "row": "", "longname": "save-sheet", "input": "sheet.vds", "keystrokes": "Ctrl+S", "comment": "save current sheet to filename in format determined by extension (default .tsv)"}
  ```
  
  This produces `sheet.vds` as follows, which seems valid:
  
  ```
  #{"name": "unnamed"}
  #{"name": "A", "width": 4, "height": 1, "expr": null, "keycol": 0, "formatter": "", "fmtstr": "", "voffset": 0, "hoffset": 0, "aggstr": "", "type": "int", "col": "Column"}
  #{"name": "A*2", "width": 5, "height": 1, "expr": "A*2", "keycol": 0, "formatter": "", "fmtstr": "", "voffset": 0, "hoffset": 0, "aggstr": "", "type": "", "col": "ExprColumn"}
  {"A": 2, "A*2": 4}
  ```
  
  Quit visidata and open that file again with `vd sheet.vds`,
  and observe the loading error.
  
  
  **Additional context**
  
  - visidata v2.11
  - python 3.10.12
  
  
  --- END ISSUE ---
  
  Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
  --- BEGIN FILES ---
  Path: `visidata/loaders/vds.py`
  Content:
  ```
  1 'Custom VisiData save format'
  2 
  3 import json
  4 
  5 from visidata import VisiData, JsonSheet, Progress, IndexSheet, SettableColumn, ItemColumn
  6 
  7 
  8 NL='\n'
  9 
  10 @VisiData.api
  11 def open_vds(vd, p):
  12     return VdsIndexSheet(p.name, source=p)
  13 
  14 
  15 @VisiData.api
  16 def save_vds(vd, p, *sheets):
  17     'Save in custom VisiData format, preserving columns and their attributes.'
  18 
  19     with p.open(mode='w', encoding='utf-8') as fp:
  20         for vs in sheets:
  21             # class and attrs for vs
  22             d = { 'name': vs.name, }
  23             fp.write('#'+json.dumps(d)+NL)
  24 
  25             # class and attrs for each column in vs
  26             for col in vs.visibleCols:
  27                 d = col.__getstate__()
  28                 if isinstance(col, SettableColumn):
  29                     d['col'] = 'Column'
  30                 else:
  31                     d['col'] = type(col).__name__
  32                 fp.write('#'+json.dumps(d)+NL)
  33 
  34             with Progress(gerund='saving'):
  35                 for row in vs.iterdispvals(*vs.visibleCols, format=False):
  36                     d = {col.name:val for col, val in row.items()}
  37                     fp.write(json.dumps(d, default=str)+NL)
  38 
  39 
  40 class VdsIndexSheet(IndexSheet):
  41     def iterload(self):
  42         vs = None
  43         with self.source.open(encoding='utf-8') as fp:
  44             line = fp.readline()
  45             while line:
  46                 if line.startswith('#{'):
  47                     d = json.loads(line[1:])
  48                     if 'col' not in d:
  49                         vs = VdsSheet(d.pop('name'), columns=[], source=self.source, source_fpos=fp.tell())
  50                         yield vs
  51                 line = fp.readline()
  52 
  53 
  54 class VdsSheet(JsonSheet):
  55     def newRow(self):
  56         return {}   # rowdef: dict
  57 
  58     def iterload(self):
  59         self.colnames = {}
  60         self.columns = []
  61 
  62         with self.source.open(encoding='utf-8') as fp:
  63             fp.seek(self.source_fpos)
  64 
  65             # consume all metadata, create columns
  66             line = fp.readline()
  67             while line and line.startswith('#{'):
  68                 d = json.loads(line[1:])
  69                 if 'col' not in d:
  70                     raise Exception(d)
  71                 classname = d.pop('col')
  72                 if classname == 'Column':
  73                     classname = 'ItemColumn'
  74                     d['expr'] = d['name']
  75 
  76                 c = globals()[classname](d.pop('name'), sheet=self)
  77                 self.addColumn(c)
  78                 self.colnames[c.name] = c
  79                 for k, v in d.items():
  80                     setattr(c, k, v)
  81 
  82                 line = fp.readline()
  83 
  84             while line and not line.startswith('#{'):
  85                 d = json.loads(line)
  86                 yield d
  87                 line = fp.readline()
  88
  ```
  --- END FILES ---
  
  Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.
  
  Here is an example:
  
  ```diff
  diff --git a/examples/server_async.py b/examples/server_async.py
  --- a/examples/server_async.py
  +++ b/examples/server_async.py
  @@ -313,4 +313,4 @@
   
   
   if __name__ == "__main__":
  -    asyncio.run(run_async_server("."), debug=True)
  +    asyncio.run(run_async_server(), debug=True)
  diff --git a/examples/server_sync.py b/examples/server_sync.py
  --- a/examples/server_sync.py
  +++ b/examples/server_sync.py
  @@ -313,5 +313,5 @@
   
   
   if __name__ == "__main__":
  -    server = run_sync_server(".")
  +    server = run_sync_server()
       server.shutdown()
  
  ```
  
golden_diff: diff --git a/visidata/loaders/vds.py b/visidata/loaders/vds.py
  --- a/visidata/loaders/vds.py
  +++ b/visidata/loaders/vds.py
  @@ -2,7 +2,7 @@
   
   import json
   
  -from visidata import VisiData, JsonSheet, Progress, IndexSheet, SettableColumn, ItemColumn
  +from visidata import VisiData, JsonSheet, Progress, IndexSheet, SettableColumn, ItemColumn, ExprColumn
   
   
   NL='\n'
  
verification_info: {"golden_diff": "diff --git a/visidata/loaders/vds.py b/visidata/loaders/vds.py\n--- a/visidata/loaders/vds.py\n+++ b/visidata/loaders/vds.py\n@@ -2,7 +2,7 @@\n \n import json\n \n-from visidata import VisiData, JsonSheet, Progress, IndexSheet, SettableColumn, ItemColumn\n+from visidata import VisiData, JsonSheet, Progress, IndexSheet, SettableColumn, ItemColumn, ExprColumn\n \n \n NL='\\n'\n", "issue": "Bug: cannot read .vds with expression column\n**Small description**\n\nVisidata cannot read back sheet(s) it saved as `.vds` if they contain an\nexpression column.\n\n\"KeyError: 'ExprColumn'\" shows as error, resulting in a partial read.\n\n\n**Expected result**\n\nIt should be able to read those files.\n\n\n**Actual result with ~~screenshot~~ stacktrace**\n\n```\nTraceback (most recent call last):\n  File \"/nix/store/z4xjb4j8i73894r2wqjvlnps9j60rjr0-visidata-2.11/lib/python3.10/site-packages/visidata/threads.py\", line 198, in _toplevelTryFunc\n    t.status = func(*args, **kwargs)\n  File \"/nix/store/z4xjb4j8i73894r2wqjvlnps9j60rjr0-visidata-2.11/lib/python3.10/site-packages/visidata/pyobj.py\", line 26, in reload\n    for r in self.iterload():\n  File \"/nix/store/z4xjb4j8i73894r2wqjvlnps9j60rjr0-visidata-2.11/lib/python3.10/site-packages/visidata/loaders/vds.py\", line 76, in iterload\n    c = globals()[classname](d.pop('name'), sheet=self)\nKeyError: 'ExprColumn'\n```\n\n\n**Steps to reproduce with sample data and a .vd**\n\nCreate and save some test sheet with an expr column with this `cmdlog.vdj`:\n\n```\n#!vd -p\n{\"col\": \"\", \"row\": \"\", \"longname\": \"open-new\", \"input\": \"\", \"keystrokes\": \"Shift+A\", \"comment\": \"Open new empty sheet\"}\n{\"sheet\": \"unnamed\", \"col\": \"A\", \"row\": \"\", \"longname\": \"type-int\", \"input\": \"\", \"keystrokes\": \"#\", \"comment\": \"set type of current column to int\"}\n{\"sheet\": \"unnamed\", \"col\": \"\", \"row\": \"\", \"longname\": \"add-row\", \"input\": \"\", \"keystrokes\": \"a\", \"comment\": \"append a blank row\"}\n{\"sheet\": \"unnamed\", \"col\": \"A\", \"row\": \"0\", \"longname\": \"edit-cell\", \"input\": \"2\", \"keystrokes\": \"e\", \"comment\": \"edit contents of current cell\"}\n{\"sheet\": \"unnamed\", \"col\": \"A\", \"row\": \"\", \"longname\": \"addcol-expr\", \"input\": \"A*2\", \"keystrokes\": \"=\", \"comment\": \"create new column from Python expression, with column names as variables\"}\n{\"sheet\": \"unnamed\", \"col\": \"\", \"row\": \"\", \"longname\": \"save-sheet\", \"input\": \"sheet.vds\", \"keystrokes\": \"Ctrl+S\", \"comment\": \"save current sheet to filename in format determined by extension (default .tsv)\"}\n```\n\nThis produces `sheet.vds` as follows, which seems valid:\n\n```\n#{\"name\": \"unnamed\"}\n#{\"name\": \"A\", \"width\": 4, \"height\": 1, \"expr\": null, \"keycol\": 0, \"formatter\": \"\", \"fmtstr\": \"\", \"voffset\": 0, \"hoffset\": 0, \"aggstr\": \"\", \"type\": \"int\", \"col\": \"Column\"}\n#{\"name\": \"A*2\", \"width\": 5, \"height\": 1, \"expr\": \"A*2\", \"keycol\": 0, \"formatter\": \"\", \"fmtstr\": \"\", \"voffset\": 0, \"hoffset\": 0, \"aggstr\": \"\", \"type\": \"\", \"col\": \"ExprColumn\"}\n{\"A\": 2, \"A*2\": 4}\n```\n\nQuit visidata and open that file again with `vd sheet.vds`,\nand observe the loading error.\n\n\n**Additional context**\n\n- visidata v2.11\n- python 3.10.12\n\n", "before_files": [{"content": "'Custom VisiData save format'\n\nimport json\n\nfrom visidata import VisiData, JsonSheet, Progress, IndexSheet, SettableColumn, ItemColumn\n\n\nNL='\\n'\n\n@VisiData.api\ndef open_vds(vd, p):\n    return VdsIndexSheet(p.name, source=p)\n\n\n@VisiData.api\ndef save_vds(vd, p, *sheets):\n    'Save in custom VisiData format, preserving columns and their attributes.'\n\n    with p.open(mode='w', encoding='utf-8') as fp:\n        for vs in sheets:\n            # class and attrs for vs\n            d = { 'name': vs.name, }\n            fp.write('#'+json.dumps(d)+NL)\n\n            # class and attrs for each column in vs\n            for col in vs.visibleCols:\n                d = col.__getstate__()\n                if isinstance(col, SettableColumn):\n                    d['col'] = 'Column'\n                else:\n                    d['col'] = type(col).__name__\n                fp.write('#'+json.dumps(d)+NL)\n\n            with Progress(gerund='saving'):\n                for row in vs.iterdispvals(*vs.visibleCols, format=False):\n                    d = {col.name:val for col, val in row.items()}\n                    fp.write(json.dumps(d, default=str)+NL)\n\n\nclass VdsIndexSheet(IndexSheet):\n    def iterload(self):\n        vs = None\n        with self.source.open(encoding='utf-8') as fp:\n            line = fp.readline()\n            while line:\n                if line.startswith('#{'):\n                    d = json.loads(line[1:])\n                    if 'col' not in d:\n                        vs = VdsSheet(d.pop('name'), columns=[], source=self.source, source_fpos=fp.tell())\n                        yield vs\n                line = fp.readline()\n\n\nclass VdsSheet(JsonSheet):\n    def newRow(self):\n        return {}   # rowdef: dict\n\n    def iterload(self):\n        self.colnames = {}\n        self.columns = []\n\n        with self.source.open(encoding='utf-8') as fp:\n            fp.seek(self.source_fpos)\n\n            # consume all metadata, create columns\n            line = fp.readline()\n            while line and line.startswith('#{'):\n                d = json.loads(line[1:])\n                if 'col' not in d:\n                    raise Exception(d)\n                classname = d.pop('col')\n                if classname == 'Column':\n                    classname = 'ItemColumn'\n                    d['expr'] = d['name']\n\n                c = globals()[classname](d.pop('name'), sheet=self)\n                self.addColumn(c)\n                self.colnames[c.name] = c\n                for k, v in d.items():\n                    setattr(c, k, v)\n\n                line = fp.readline()\n\n            while line and not line.startswith('#{'):\n                d = json.loads(line)\n                yield d\n                line = fp.readline()\n", "path": "visidata/loaders/vds.py"}], "after_files": [{"content": "'Custom VisiData save format'\n\nimport json\n\nfrom visidata import VisiData, JsonSheet, Progress, IndexSheet, SettableColumn, ItemColumn, ExprColumn\n\n\nNL='\\n'\n\n@VisiData.api\ndef open_vds(vd, p):\n    return VdsIndexSheet(p.name, source=p)\n\n\n@VisiData.api\ndef save_vds(vd, p, *sheets):\n    'Save in custom VisiData format, preserving columns and their attributes.'\n\n    with p.open(mode='w', encoding='utf-8') as fp:\n        for vs in sheets:\n            # class and attrs for vs\n            d = { 'name': vs.name, }\n            fp.write('#'+json.dumps(d)+NL)\n\n            # class and attrs for each column in vs\n            for col in vs.visibleCols:\n                d = col.__getstate__()\n                if isinstance(col, SettableColumn):\n                    d['col'] = 'Column'\n                else:\n                    d['col'] = type(col).__name__\n                fp.write('#'+json.dumps(d)+NL)\n\n            with Progress(gerund='saving'):\n                for row in vs.iterdispvals(*vs.visibleCols, format=False):\n                    d = {col.name:val for col, val in row.items()}\n                    fp.write(json.dumps(d, default=str)+NL)\n\n\nclass VdsIndexSheet(IndexSheet):\n    def iterload(self):\n        vs = None\n        with self.source.open(encoding='utf-8') as fp:\n            line = fp.readline()\n            while line:\n                if line.startswith('#{'):\n                    d = json.loads(line[1:])\n                    if 'col' not in d:\n                        vs = VdsSheet(d.pop('name'), columns=[], source=self.source, source_fpos=fp.tell())\n                        yield vs\n                line = fp.readline()\n\n\nclass VdsSheet(JsonSheet):\n    def newRow(self):\n        return {}   # rowdef: dict\n\n    def iterload(self):\n        self.colnames = {}\n        self.columns = []\n\n        with self.source.open(encoding='utf-8') as fp:\n            fp.seek(self.source_fpos)\n\n            # consume all metadata, create columns\n            line = fp.readline()\n            while line and line.startswith('#{'):\n                d = json.loads(line[1:])\n                if 'col' not in d:\n                    raise Exception(d)\n                classname = d.pop('col')\n                if classname == 'Column':\n                    classname = 'ItemColumn'\n                    d['expr'] = d['name']\n\n                c = globals()[classname](d.pop('name'), sheet=self)\n                self.addColumn(c)\n                self.colnames[c.name] = c\n                for k, v in d.items():\n                    setattr(c, k, v)\n\n                line = fp.readline()\n\n            while line and not line.startswith('#{'):\n                d = json.loads(line)\n                yield d\n                line = fp.readline()\n", "path": "visidata/loaders/vds.py"}]}