Repository: voxel51/fiftyone
Issue: Visualizing views obtained through expressions

I have a detection dataset and got a view using 

```
faster_rcnn_75 = dataset.view().list_filter("faster_rcnn.detections", F("confidence")>0.75)
```

When I tried to load this view into my `session` I got the following error:

```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-45-5df364ebf323> in <module>
----> 1 session.view = filt_view

~/work/fiftyone/fiftyone/fiftyone/core/client.py in __setattr__(self, name, value)
    140             self._hc_client.update(value)
    141         else:
--> 142             super().__setattr__(name, value)
    143 
    144     @property

~/work/fiftyone/fiftyone/fiftyone/core/session.py in wrapper(self, *args, **kwargs)
     81     def wrapper(self, *args, **kwargs):
     82         result = func(self, *args, **kwargs)
---> 83         self._update_state()
     84         return result
     85 

~/work/fiftyone/fiftyone/fiftyone/core/session.py in _update_state(self)
    244             dataset=self._dataset,
    245             view=self._view,
--> 246             selected=self.state.selected,
    247         )
    248 

~/work/fiftyone/fiftyone/fiftyone/core/client.py in __setattr__(self, name, value)
    138                     % (self._HC_ATTR_TYPE, type(value))
    139                 )
--> 140             self._hc_client.update(value)
    141         else:
    142             super().__setattr__(name, value)

~/work/fiftyone/fiftyone/fiftyone/core/client.py in update(self, data)
     85         """
     86         self.data = data
---> 87         self.emit("update", data.serialize())
     88 
     89 

~/work/fiftyone/fiftyone/eta/eta/core/serial.py in serialize(self, reflective)
    345         d = self._prepare_serial_dict(reflective)
    346         for a in self.attributes():
--> 347             d[a] = _recurse(getattr(self, a), reflective)
    348         return d
    349 

~/work/fiftyone/fiftyone/eta/eta/core/serial.py in _recurse(v, reflective)
    488 
    489     if hasattr(v, "serialize") and callable(v.serialize):
--> 490         return v.serialize()
    491 
    492     if hasattr(v, "to_dict") and callable(v.to_dict):

~/work/fiftyone/fiftyone/fiftyone/core/view.py in serialize(self)
    483         return {
    484             "dataset": self._dataset.serialize(),
--> 485             "view": json_util.dumps([s._serialize() for s in self._stages]),
    486         }
    487 

~/venvs/fo/lib/python3.6/site-packages/bson/json_util.py in dumps(obj, *args, **kwargs)
    381     """
    382     json_options = kwargs.pop("json_options", DEFAULT_JSON_OPTIONS)
--> 383     return json.dumps(_json_convert(obj, json_options), *args, **kwargs)
    384 
    385 

/usr/lib/python3.6/json/__init__.py in dumps(obj, skipkeys, ensure_ascii, check_circular, allow_nan, cls, indent, separators, default, sort_keys, **kw)
    229         cls is None and indent is None and separators is None and
    230         default is None and not sort_keys and not kw):
--> 231         return _default_encoder.encode(obj)
    232     if cls is None:
    233         cls = JSONEncoder

/usr/lib/python3.6/json/encoder.py in encode(self, o)
    197         # exceptions aren't as detailed.  The list call should be roughly
    198         # equivalent to the PySequence_Fast that ''.join() would do.
--> 199         chunks = self.iterencode(o, _one_shot=True)
    200         if not isinstance(chunks, (list, tuple)):
    201             chunks = list(chunks)

/usr/lib/python3.6/json/encoder.py in iterencode(self, o, _one_shot)
    255                 self.key_separator, self.item_separator, self.sort_keys,
    256                 self.skipkeys, _one_shot)
--> 257         return _iterencode(o, 0)
    258 
    259 def _make_iterencode(markers, _default, _encoder, _indent, _floatstr,

/usr/lib/python3.6/json/encoder.py in default(self, o)
    178         """
    179         raise TypeError("Object of type '%s' is not JSON serializable" %
--> 180                         o.__class__.__name__)
    181 
    182     def encode(self, o):

TypeError: Object of type 'ViewExpression' is not JSON serializable
```

Changed files:
fiftyone/core/stages.py

diff --git a/fiftyone/core/stages.py b/fiftyone/core/stages.py
index c29a71941..aaf6e439f 100644
--- a/fiftyone/core/stages.py
+++ b/fiftyone/core/stages.py
@@ -67,17 +67,38 @@ class ViewStage(object):
         raise NotImplementedError("subclasses must implement `to_mongo()`")
 
     def _serialize(self):
+        """Returns a JSON dict representation of the :class:`ViewStage`.
+
+        Returns:
+            a JSON dict
+        """
         return {
             "kwargs": self._kwargs(),
             "_cls": etau.get_class_name(self),
         }
 
     def _kwargs(self):
+        """Returns a JSON dict describing the keyword arguments that define the
+        ViewStage.
+
+        Returns:
+            a JSON dict
+        """
         raise NotImplementedError("subclasses must implement `_kwargs()`")
 
     @classmethod
     def _from_dict(cls, d):
-        return etau.get_class(d["_cls"])(**d["kwargs"])
+        """Creates a :class:`ViewStage` instance from a serialized JSON dict
+        representation of it.
+
+        Args:
+            d: a JSON dict
+
+        Returns:
+            a :class:`ViewStage`
+        """
+        view_stage_cls = etau.get_class(d["_cls"])
+        return view_stage_cls(**d["kwargs"])
 
 
 class ViewStageError(Exception):
@@ -94,11 +115,11 @@ class Exclude(ViewStage):
     """
 
     def __init__(self, sample_ids):
-        self._sample_ids = sample_ids
+        self._sample_ids = list(sample_ids)
 
     @property
     def sample_ids(self):
-        """The iterable of sample IDs to exclude."""
+        """The list of sample IDs to exclude."""
         return self._sample_ids
 
     def to_mongo(self):
@@ -128,7 +149,7 @@ class Exists(ViewStage):
 
     @property
     def field(self):
-        """The field to check if exists."""
+        """The field to check for existence."""
         return self._field
 
     def to_mongo(self):
@@ -148,7 +169,7 @@ class Limit(ViewStage):
     """Limits the view to the given number of samples.
 
     Args:
-        num: the maximum number of samples to return. If a non-positive
+        limit: the maximum number of samples to return. If a non-positive
             number is provided, an empty view is returned
     """
 
@@ -167,6 +188,9 @@ class Limit(ViewStage):
         Returns:
             a MongoDB aggregation pipeline (list of dicts)
         """
+        if self._limit <= 0:
+            return Match({"_id": None}).to_mongo()
+
         return [{"$limit": self._limit}]
 
     def _kwargs(self):
@@ -177,7 +201,7 @@ class ListFilter(ViewStage):
     """Filters the list elements in the samples in the stage.
 
     Args:
-        field: the field to filter, which must be a list
+        field: the field to filter, which must be a list field
         filter: a :class:`fiftyone.core.expressions.ViewExpression` or
             `MongoDB expression <https://docs.mongodb.com/manual/meta/aggregation-quick-reference/#aggregation-expressions>`_
             that returns a boolean describing the filter to apply
@@ -205,22 +229,27 @@ class ListFilter(ViewStage):
         Returns:
             a MongoDB aggregation pipeline (list of dicts)
         """
-        cond = self._filter
-        if isinstance(cond, ViewExpression):
-            cond = cond.to_mongo(in_list=True)
-
         return [
             {
                 "$addFields": {
                     self._field: {
-                        "$filter": {"input": "$" + self._field, "cond": cond}
+                        "$filter": {
+                            "input": "$" + self._field,
+                            "cond": self._get_mongo_filter(),
+                        }
                     }
                 }
             }
         ]
 
+    def _get_mongo_filter(self):
+        if isinstance(self._filter, ViewExpression):
+            return self._filter.to_mongo(in_list=True)
+
+        return self._filter
+
     def _kwargs(self):
-        return {"field": self._field, "filter": self._filter}
+        return {"field": self._field, "filter": self._get_mongo_filter()}
 
     def _validate(self):
         if not isinstance(self._filter, (ViewExpression, dict)):
@@ -255,14 +284,16 @@ class Match(ViewStage):
         Returns:
             a MongoDB aggregation pipeline (list of dicts)
         """
-        filt = self._filter
-        if isinstance(filt, ViewExpression):
-            filt = {"$expr": filt.to_mongo()}
+        return [{"$match": self._get_mongo_filter()}]
 
-        return [{"$match": filt}]
+    def _get_mongo_filter(self):
+        if isinstance(self._filter, ViewExpression):
+            return {"$expr": self._filter.to_mongo()}
+
+        return self._filter
 
     def _kwargs(self):
-        return {"filter": self._filter}
+        return {"filter": self._get_mongo_filter()}
 
     def _validate(self):
         if not isinstance(self._filter, (ViewExpression, dict)):
@@ -304,18 +335,18 @@ class MatchTags(ViewStage):
     """Returns a view containing the samples that have any of the given
     tags.
 
-    To match samples that contain a single, use :class:`MatchTag`
+    To match samples that contain a single, use :class:`MatchTag`.
 
     Args:
         tags: an iterable of tags
     """
 
     def __init__(self, tags):
-        self._tags = tags
+        self._tags = list(tags)
 
     @property
     def tags(self):
-        """The iterable of tags to match."""
+        """The list of tags to match."""
         return self._tags
 
     def to_mongo(self):
@@ -370,11 +401,11 @@ class Select(ViewStage):
     """
 
     def __init__(self, sample_ids):
-        self._sample_ids = sample_ids
+        self._sample_ids = list(sample_ids)
 
     @property
     def sample_ids(self):
-        """The iterable of sample IDs to select."""
+        """The list of sample IDs to select."""
         return self._sample_ids
 
     def to_mongo(self):
@@ -427,22 +458,28 @@ class SortBy(ViewStage):
         """
         order = DESCENDING if self._reverse else ASCENDING
 
-        if not isinstance(self._field_or_expr, (ViewExpression, dict)):
-            return [{"$sort": {self._field_or_expr: order}}]
+        field_or_expr = self._get_mongo_field_or_expr()
 
-        if isinstance(self._field_or_expr, ViewExpression):
-            expr = self._field_or_expr.to_mongo()
-        else:
-            expr = self._field_or_expr
+        if not isinstance(field_or_expr, dict):
+            return [{"$sort": {field_or_expr: order}}]
 
         return [
-            {"$addFields": {"_sort_field": expr}},
+            {"$addFields": {"_sort_field": field_or_expr}},
             {"$sort": {"_sort_field": order}},
             {"$unset": "_sort_field"},
         ]
 
+    def _get_mongo_field_or_expr(self):
+        if isinstance(self._field_or_expr, ViewExpression):
+            return self._field_or_expr.to_mongo()
+
+        return self._field_or_expr
+
     def _kwargs(self):
-        return {"field_or_expr": self._field_or_expr, "reverse": self._reverse}
+        return {
+            "field_or_expr": self._get_mongo_field_or_expr(),
+            "reverse": self._reverse,
+        }
 
 
 class Skip(ViewStage):