Repository: voxel51/fiftyone

Issue:
dataset.delete_sample_fields() is not deleting all values
When running `dataset.delete_sample_fields("field")`, the field will be deleted. However, if a sample then recieves a new value for that field, all other samples are reverted to the value they had before the field was deleted. 

This can result in two samples having different value types for the same field. 
Recreate with the code:
```
dataset = fo.Dataset("example")

s1 = fo.Sample("s1.png")
s2 = fo.Sample("s2.png")

ids = dataset.add_samples([s1, s2])

s1["new_field"] = 1
dataset.delete_sample_fields("new_field")
s2["new_field"] = "str"

print(dataset[ids[0]].new_field)
Out:  1  ### This is an integer

print(dataset[ids[1]].new_field)
Out: "str" ### This is a string
```


Discussion:

Resolving patch:
diff --git a/fiftyone/core/dataset.py b/fiftyone/core/dataset.py
--- a/fiftyone/core/dataset.py
+++ b/fiftyone/core/dataset.py
@@ -316,6 +316,7 @@ def delete_sample_field(self, field_name):
             AttributeError: if the field does not exist
         """
         self._sample_doc_cls.delete_field(field_name)
+        fos.Sample._purge_field(self.name, field_name)
 
     def get_tags(self):
         """Returns the set of tags in the dataset.
diff --git a/fiftyone/core/odm/document.py b/fiftyone/core/odm/document.py
--- a/fiftyone/core/odm/document.py
+++ b/fiftyone/core/odm/document.py
@@ -63,8 +63,6 @@ def _to_repr_dict(self):
                 continue
 
             value = getattr(self, f)
-            if value is None:
-                continue
 
             if isinstance(value, ObjectId):
                 d[f] = str(value)
diff --git a/fiftyone/core/sample.py b/fiftyone/core/sample.py
--- a/fiftyone/core/sample.py
+++ b/fiftyone/core/sample.py
@@ -21,7 +21,6 @@
 
 from collections import defaultdict
 from copy import deepcopy
-import json
 import os
 import weakref
 
@@ -333,6 +332,17 @@ def _reload_dataset_samples(cls, dataset_name):
         for sample in cls._instances[dataset_name].values():
             sample.reload()
 
+    @classmethod
+    def _purge_field(cls, dataset_name, field_name):
+        """Remove any field values from samples that exist in memory.
+
+        Args:
+            dataset_name: the name of the dataset to reload.
+            field_name: the name of the field to purge.
+        """
+        for sample in cls._instances[dataset_name].values():
+            sample._doc._data.pop(field_name, None)
+
     def _delete(self):
         """Deletes the document from the database."""
         self._doc.delete()


Test patch:
diff --git a/tests/unittests.py b/tests/unittests.py
--- a/tests/unittests.py
+++ b/tests/unittests.py
@@ -1062,6 +1062,26 @@ def test_copy_sample(self):
         self.assertIsNone(sample_copy.id)
         self.assertIsNone(sample_copy.dataset_name)
 
+    @drop_datasets
+    def test_in_memory_sample_fields(self):
+        """Ensure that any in memory samples have the field values purged when
+        a field is deleted.
+        """
+        dataset_name = self.test_in_memory_sample_fields.__name__
+        dataset = fo.Dataset(dataset_name)
+
+        s1 = fo.Sample("s1.png")
+        s2 = fo.Sample("s2.png")
+
+        dataset.add_samples([s1, s2])
+
+        s1["new_field"] = 51
+        dataset.delete_sample_field("new_field")
+        s2["new_field"] = "fiftyone"
+
+        self.assertIsNone(s1.new_field)
+        self.assertEqual(s2.new_field, "fiftyone")
+
 
 class LabelsTest(unittest.TestCase):
     @drop_datasets