Repository: voxel51/fiftyone

Issue:
Don't re-create other view stages when modifying/creating other stages
Current behavior:

- Create a `Take(1, seed=None)` view stage in the App. An image appears
- Add another stage, say, `Limit(1)` in the view bar. Now a different image appears, because creating the new stage causes the existing `Take` stage to be re-instantiated

Note that editing a stage, eg clicking into the `seed` parameter of an existing `Take(1, seed=None)` stage and then submitting the form, should cause the view stage to be recreated. This would be a way to intentionally regenerate a randomized view, for example.

The desired behavior is that existing view stages are not re-instantiated when other view stages are created, modified, re-organized, etc. The issue is that stages can have default parameters, and those default parameters can be randomly generated (e.g., `_randint` for the `Take()` and `Shuffle()` stages), so stages can't be re-instantiated.

For reference, the backend exhibits the desired behavior:

v = dataset.take(1)
v2 = v.limit(1)  # contains same sample

It seems that the best way to achieve this may be to let the backend own the definition of the view and have the App report changes that it would like to be made to the view.


Discussion:

Resolving patch:
diff --git a/fiftyone/core/stages.py b/fiftyone/core/stages.py
--- a/fiftyone/core/stages.py
+++ b/fiftyone/core/stages.py
@@ -7,6 +7,7 @@
 """
 import random
 import reprlib
+import uuid
 
 from bson import ObjectId
 from pymongo import ASCENDING, DESCENDING
@@ -30,6 +31,8 @@ class ViewStage(object):
     view stages are represented by a :class:`fiftyone.core.view.DatasetView`.
     """
 
+    _uuid = None
+
     def __str__(self):
         return repr(self)
 
@@ -98,9 +101,12 @@ def _serialize(self):
         Returns:
             a JSON dict
         """
+        if self._uuid is None:
+            self._uuid = str(uuid.uuid4())
         return {
             "kwargs": self._kwargs(),
             "_cls": etau.get_class_name(self),
+            "_uuid": self._uuid,
         }
 
     def _kwargs(self):
@@ -134,7 +140,10 @@ def _from_dict(cls, d):
             a :class:`ViewStage`
         """
         view_stage_cls = etau.get_class(d["_cls"])
-        return view_stage_cls(**{k: v for (k, v) in d["kwargs"]})
+        uuid = d.pop("_uuid", None)
+        stage = view_stage_cls(**{k: v for (k, v) in d["kwargs"]})
+        stage._uuid = uuid
+        return stage
 
 
 class ViewStageError(Exception):
@@ -1159,8 +1168,9 @@ def _params(self):
                 "name": "seed",
                 "type": "float|NoneType",
                 "default": "None",
-                "placeholder": "seed (default=None",
-            }
+                "placeholder": "seed (default=None)",
+            },
+            {"name": "_randint", "type": "int|NoneType", "default": "None"},
         ]
 
 
@@ -1397,8 +1407,9 @@ def _params(cls):
                 "name": "seed",
                 "type": "float|NoneType",
                 "default": "None",
-                "placeholder": "reverse (default=False)",
+                "placeholder": "seed (default=None)",
             },
+            {"name": "_randint", "type": "int|NoneType", "default": "None"},
         ]
 
 


Test patch:
diff --git a/tests/unittests.py b/tests/unittests.py
--- a/tests/unittests.py
+++ b/tests/unittests.py
@@ -30,6 +30,7 @@
 import fiftyone.core.odm as foo
 from fiftyone.core.odm.sample import default_sample_fields
 import fiftyone.core.sample as fos
+import fiftyone.core.stages as fosg
 from fiftyone import ViewField as F
 
 
@@ -2164,6 +2165,14 @@ def test_take(self):
         result = list(self.dataset.take(1))
         self.assertIs(len(result), 1)
 
+    def test_uuids(self):
+        stage = fosg.Take(1)
+        stage_dict = stage._serialize()
+        self.assertEqual(stage._uuid, stage_dict["_uuid"])
+        self.assertEqual(
+            stage_dict["_uuid"], fosg.ViewStage._from_dict(stage_dict)._uuid
+        )
+
 
 if __name__ == "__main__":
     fo.config.show_progress_bars = False