Repository: pyout/pyout

Issue:
gets stuck or pukes error if there is "interval" but value is a string
torturing pyout via dandi

I have
```
        "errors": dict(
            color=dict(
                interval=[
                    [0, 1, "green"],
                    [1, None, "red"],
                ]
            ),
            aggregate=lambda x: "{} with errors".format(sum(map(bool, x))),
        ),
```
and if I yield an empty string as a value - usually stalls, but once actually crashed and gave me error

```
$> dandi upload -i local -c upload2 -t /tmp --validation skip /tmp/nwb_test_data/v2.0.0/test_*nwb
PATH                                 SIZE    ERRORS        UPLOAD STATUS MESSAGE
Traceback (most recent call last):
  File "/home/yoh/proj/dandi/dandi-cli/venvs/dev3/bin/dandi", line 11, in <module>
    load_entry_point('dandi', 'console_scripts', 'dandi')()
  File "/home/yoh/proj/misc/click/click/core.py", line 828, in __call__
    return self.main(*args, **kwargs)
  File "/home/yoh/proj/misc/click/click/core.py", line 781, in main
    rv = self.invoke(ctx)
  File "/home/yoh/proj/misc/click/click/core.py", line 1227, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/home/yoh/proj/misc/click/click/core.py", line 1046, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/home/yoh/proj/misc/click/click/core.py", line 590, in invoke
    return callback(*args, **kwargs)
  File "/home/yoh/proj/dandi/dandi-cli/dandi/cli/command.py", line 462, in upload
    out(rec)
  File "/home/yoh/proj/repronim/pyout/pyout/interface.py", line 384, in __call__
    self._write(row, style)
  File "/home/yoh/proj/repronim/pyout/pyout/interface.py", line 236, in _write
    self._write_fn(row, style)
  File "/home/yoh/proj/repronim/pyout/pyout/interface.py", line 249, in _write_update
    content, status, summary = self._content.update(row, style)
  File "/home/yoh/proj/repronim/pyout/pyout/common.py", line 701, in update
    content, status = super(ContentWithSummary, self).update(row, style)
  File "/home/yoh/proj/repronim/pyout/pyout/common.py", line 672, in update
    return six.text_type(self), "repaint"
  File "/home/yoh/proj/repronim/pyout/pyout/common.py", line 611, in __str__
    return "".join(self._render(self.rows))
  File "/home/yoh/proj/repronim/pyout/pyout/common.py", line 601, in _render
    line, adj = self.fields.render(row, **kwds)
  File "/home/yoh/proj/repronim/pyout/pyout/common.py", line 523, in render
    for c in self.columns if not hidden[c]]
  File "/home/yoh/proj/repronim/pyout/pyout/common.py", line 523, in <listcomp>
    for c in self.columns if not hidden[c]]
  File "/home/yoh/proj/repronim/pyout/pyout/field.py", line 158, in __call__
    result = fn(value, result)
  File "/home/yoh/proj/repronim/pyout/pyout/field.py", line 381, in proc
    value = float(value)
ValueError: could not convert string to float: 
(dev3) 1 37143 ->1 [2].....................................:Fri 18 Oct 2019 12:31:28 PM EDT:.
(git)hopa:~/proj/dandi/dandi-cli[nf-upload]git
$> dandi upload -i local -c upload2 -t /tmp --validation skip /tmp/nwb_test_data/v2.0.0/test_*nwb
PATH                                     SIZE    ERRORS        UPLOAD STATUS MESSAGE
nwb_test_data/v2.0.0/test_append.nwb     43.1 kB                                    
^[[a^C
Aborted!
```

from traceback above seems like might relate to use of `interval` since otherwise there should be no reason for float conversion

Comments:

Patch:
diff --git a/pyout/field.py b/pyout/field.py
index e9d49e8..9935f7b 100644
--- a/pyout/field.py
+++ b/pyout/field.py
@@ -195,6 +195,14 @@ class Nothing(object):
         return self._text.__format__(format_spec)
 
 
+def _pass_nothing_through(proc):
+    """Make processor function `proc` skip Nothing objects.
+    """
+    def wrapped(value, result):
+        return result if isinstance(value, Nothing) else proc(value, result)
+    return wrapped
+
+
 class StyleFunctionError(Exception):
     """Signal that a style function failed.
     """
@@ -241,9 +249,6 @@ class StyleProcessors(object):
         """Return a processor for a style's "transform" function.
         """
         def transform_fn(_, result):
-            if isinstance(result, Nothing):
-                return result
-
             lgr.debug("Transforming %r with %r", result, function)
             try:
                 return function(result)
@@ -309,10 +314,13 @@ class StyleProcessors(object):
         def proc(value, result):
             try:
                 lookup_value = mapping[value]
-            except (KeyError, TypeError):
-                # ^ TypeError is included in case the user passes non-hashable
-                # values.
-                return result
+            except KeyError:
+                lgr.debug("by_lookup: Key %r not found in mapping %s",
+                          value, mapping)
+                lookup_value = None
+            except TypeError:
+                lgr.debug("by_lookup: Key %r not hashable", value)
+                lookup_value = None
 
             if not lookup_value:
                 return result
@@ -345,6 +353,8 @@ class StyleProcessors(object):
 
         def proc(value, result):
             if not isinstance(value, six.string_types):
+                lgr.debug("by_re_lookup: Skipping non-string value %r",
+                          value)
                 return result
             for r, lookup_value in regexps:
                 if r.search(value):
@@ -379,7 +389,8 @@ class StyleProcessors(object):
         def proc(value, result):
             try:
                 value = float(value)
-            except TypeError:
+            except Exception as exc:
+                lgr.debug("by_interval_lookup: Skipping %r: %s", value, exc)
                 return result
 
             for start, end, lookup_value in intervals:
@@ -409,7 +420,8 @@ class StyleProcessors(object):
         A generator object.
         """
         if "transform" in column_style:
-            yield self.transform(column_style["transform"])
+            yield _pass_nothing_through(
+                self.transform(column_style["transform"]))
 
     def post_from_style(self, column_style):
         """Yield post-format processors based on `column_style`.
@@ -442,7 +454,7 @@ class StyleProcessors(object):
             if vtype == "re_lookup":
                 args.append(sum(getattr(re, f)
                                 for f in column_style.get("re_flags", [])))
-            yield fn(*args)
+            yield _pass_nothing_through(fn(*args))
 
         yield flanks.join_flanks
 


Test patch:
diff --git a/pyout/tests/test_tabular.py b/pyout/tests/test_tabular.py
index a29e01e..550a96e 100644
--- a/pyout/tests/test_tabular.py
+++ b/pyout/tests/test_tabular.py
@@ -468,6 +468,23 @@ def test_tabular_write_re_lookup_bold():
     assert_eq_repr(out.stdout, expected)
 
 
+def test_tabular_write_intervals_wrong_type():
+    out = Tabular(style={"name": {"width": 3},
+                         "percent": {"color": {"interval":
+                                               [[0, 50, "red"],
+                                                [50, 80, "yellow"],
+                                                [80, 100, "green"]]},
+                                     "width": 8}})
+    out(OrderedDict([("name", "foo"),
+                     ("percent", 88)]))
+    out(OrderedDict([("name", "bar"),
+                     ("percent", "notfloat")]))
+
+    expected = ["foo " + capres("green", "88") + "      ",
+                "bar notfloat"]
+    assert_contains_nc(out.stdout.splitlines(), *expected)
+
+
 def test_tabular_write_intervals_color():
     out = Tabular(style={"name": {"width": 3},
                          "percent": {"color": {"interval":