Repository: saulpw/visidata

Issue:
visidata.view_pandas(None) raises UnboundLocalError: local variable 'df' referenced before assignment
Hi @saulpw,

I'm aware that's an odd usage...

```python
In [3]: import visidata

In [4]: visidata.view_pandas(None)
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
<ipython-input-4-5d0ae9b435d5> in <module>
----> 1 visidata.view_pandas(None)

~/opt/anaconda3/lib/python3.7/site-packages/visidata/loaders/_pandas.py in view_pandas(df)
    318
    319 def view_pandas(df):
--> 320     run(PandasSheet('', source=df))
    321
    322

~/opt/anaconda3/lib/python3.7/site-packages/visidata/mainloop.py in run(*sheetlist)
    223     curses.use_env(True)
    224
--> 225     ret = wrapper(cursesMain, sheetlist)
    226
    227     if ret:

~/opt/anaconda3/lib/python3.7/site-packages/visidata/mainloop.py in wrapper(f, *args)
    213
    214 def wrapper(f, *args):
--> 215     return curses.wrapper(setupcolors, f, *args)
    216
    217 def run(*sheetlist):

~/opt/anaconda3/lib/python3.7/curses/__init__.py in wrapper(*args, **kwds)
    100             pass
    101
--> 102         return func(stdscr, *args, **kwds)
    103     finally:
    104         # Set everything back to normal

~/opt/anaconda3/lib/python3.7/site-packages/visidata/mainloop.py in setupcolors(stdscr, f, *args)
    210             curses.mouseEvents[getattr(curses, k)] = k
    211
--> 212     return f(stdscr, *args)
    213
    214 def wrapper(f, *args):

~/opt/anaconda3/lib/python3.7/site-packages/visidata/mainloop.py in cursesMain(_scr, sheetlist)
    234
    235     for vs in sheetlist:
--> 236         vd.push(vs)
    237
    238     vd.status('Ctrl+H opens help')

~/opt/anaconda3/lib/python3.7/site-packages/visidata/sheets.py in push(vd, vs)
    980         vd.allSheets.append(vs)
    981
--> 982     vs.ensureLoaded()
    983
    984

~/opt/anaconda3/lib/python3.7/site-packages/visidata/basesheet.py in ensureLoaded(self)
    187         if self.rows is UNLOADED:
    188             self.rows = []  # prevent auto-reload from running twice
--> 189             return self.reload()   # likely launches new thread
    190
    191     def reload(self):

~/opt/anaconda3/lib/python3.7/site-packages/visidata/loaders/_pandas.py in reload(self)
    118
    119         # reset the index here
--> 120         if type(df.index) is not pd.RangeIndex:
    121             df = df.reset_index()
    122

UnboundLocalError: local variable 'df' referenced before assignment
```

but I think it shouldn't fail like this.

I see 2 possibilities.

1) consider that `self.source` can act like as a Pandas DataFrame (ie duck typing)
2) raises exception for unsupported type

I personally think the first approach is more "Pythonesque".

Any opinion about this ?

Kind regards


Discussion:
Hey @scls19fr - interesting issue. I think the fact that you're using `None` makes it look like a fluke, but the same issue comes up in other cases too. Currently the loader considers two possibilities for `self.source`:

1. It's already a DataFrame - great, use it.
2. It's a file path - try to read it with pandas.

We _could_ attempt something like `pd.DataFrame(self.source)` if neither of those cases holds. That'd let you pass DataFrameable structures into `view_pandas` directly like you suggested in your "possibility 1", which seems like a reasonably common/useful pattern if you're working from a REPL. That'd give an empty VisiData sheet/DataFrame for your test case, but also allow things like this to "just work":

```python
visidata.view_pandas([{'col1': 5, 'col2': 10}, {'col1': 25, 'col2': 50}])
```

In that case we could still try to gracefully fail if the source is something that can't be converted into a DataFrame. And in any case we get to avoid those unbound local errors. Seem reasonable (to you and @saulpw both)?

Resolving patch:
diff --git a/visidata/loaders/_pandas.py b/visidata/loaders/_pandas.py
--- a/visidata/loaders/_pandas.py
+++ b/visidata/loaders/_pandas.py
@@ -117,11 +117,20 @@ def reload(self):
             else:
                 readfunc = getattr(pd, 'read_'+filetype) or vd.error('no pandas.read_'+filetype)
             df = readfunc(str(self.source), **options.getall('pandas_'+filetype+'_'))
+        else:
+            try:
+                df = pd.DataFrame(self.source)
+            except ValueError as err:
+                vd.fail('error building pandas DataFrame from source data: %s' % err)
 
         # reset the index here
         if type(df.index) is not pd.RangeIndex:
             df = df.reset_index()
 
+        # VisiData assumes string column names but pandas does not. Forcing string
+        # columns at load-time avoids various errors later.
+        df.columns = df.columns.astype(str)
+
         self.columns = []
         for col in (c for c in df.columns if not c.startswith("__vd_")):
             self.addColumn(Column(
diff --git a/visidata/loaders/pandas_freqtbl.py b/visidata/loaders/pandas_freqtbl.py
--- a/visidata/loaders/pandas_freqtbl.py
+++ b/visidata/loaders/pandas_freqtbl.py
@@ -97,7 +97,7 @@ def reload(self):
         # that operates similarly to pd.cut.
         super().initCols()
 
-        df = self.source.rows.df
+        df = self.source.df.copy()
 
         # Implementation (special case): for one row, this degenerates
         # to .value_counts(); however this does not order in a stable manner.


Test patch: