Repository: voxel51/fiftyone

Issue:
ds = Dataset.from_dir(...., max_samples=N) errors out when embeddings exist in "brain" sub_folder [BUG]

### System information

-   **OS Platform and Distribution**= Linux Ubuntu 20.04
-   **Python version** (`python --version`): 3.8
-   **FiftyOne version** (`fiftyone --version`): 0.19.1
-   **FiftyOne installed from** (pip or source): pip

### Commands to reproduce

*** Have a directory with multiple json files, and/or subdirectories with json files (e.g. evaluations, brain, etc).  Even just samples.json and metadata.json causes issues ***

```
dataset = fo.Dataset.from_dir('<dir_path>', dataset_type=fo.types.FiftyOneDataset, max_samples=10)
```

### Describe the problem

It appears when using from_dir that using max_samples will cause an error to be thrown.  It can vary what kind of error.

### Other info/logs

ValueError: The number of embeddings (19998) in these results no longer matches the number of samples (10) in the collection. You must regenerate the results

**** after removing the "brain" directory I get the following:  TypeError: 'ObjectId' object does not support item assignment ****

### What areas of FiftyOne does this bug affect?

-   [ ] `Core`: Core Python library issue


### Willingness to contribute

The FiftyOne Community encourages bug fix contributions. Would you or another member of your organization be willing to contribute a fix for this bug to the FiftyOne codebase?

-   [ ] No. I cannot contribute a bug fix at this time



Discussion:

Resolving patch:
diff --git a/fiftyone/utils/data/exporters.py b/fiftyone/utils/data/exporters.py
--- a/fiftyone/utils/data/exporters.py
+++ b/fiftyone/utils/data/exporters.py
@@ -1689,6 +1689,10 @@ class LegacyFiftyOneDatasetExporter(GenericSampleDatasetExporter):
             necessary) via :func:`fiftyone.core.utils.normalize_path`
         abs_paths (False): whether to store absolute paths to the media in the
             exported labels
+        export_saved_views (True): whether to include saved views in the export.
+            Only applicable when exporting full datasets
+        export_runs (True): whether to include annotation/brain/evaluation
+            runs in the export. Only applicable when exporting full datasets
         pretty_print (False): whether to render the JSON in human readable
             format with newlines and indentations
     """
@@ -1699,6 +1703,8 @@ def __init__(
         export_media=None,
         rel_dir=None,
         abs_paths=False,
+        export_saved_views=True,
+        export_runs=True,
         pretty_print=False,
     ):
         if export_media is None:
@@ -1709,6 +1715,8 @@ def __init__(
         self.export_media = export_media
         self.rel_dir = rel_dir
         self.abs_paths = abs_paths
+        self.export_saved_views = export_saved_views
+        self.export_runs = export_runs
         self.pretty_print = pretty_print
 
         self._data_dir = None
@@ -1807,26 +1815,26 @@ def log_collection(self, sample_collection):
         # Exporting the information below only makes sense when exporting an
         # entire dataset
 
-        if dataset.has_saved_views:
+        if dataset.has_saved_views and self.export_saved_views:
             self._metadata["saved_views"] = [
                 json_util.dumps(v.to_dict()) for v in dataset._doc.saved_views
             ]
 
-        if dataset.has_annotation_runs:
+        if dataset.has_annotation_runs and self.export_runs:
             self._metadata["annotation_runs"] = {
                 k: json_util.dumps(v.to_dict())
                 for k, v in dataset._doc.annotation_runs.items()
             }
             _export_annotation_results(dataset, self._anno_dir)
 
-        if dataset.has_brain_runs:
+        if dataset.has_brain_runs and self.export_runs:
             self._metadata["brain_methods"] = {
                 k: json_util.dumps(v.to_dict())
                 for k, v in dataset._doc.brain_methods.items()
             }
             _export_brain_results(dataset, self._brain_dir)
 
-        if dataset.has_evaluations:
+        if dataset.has_evaluations and self.export_runs:
             self._metadata["evaluations"] = {
                 k: json_util.dumps(v.to_dict())
                 for k, v in dataset._doc.evaluations.items()
diff --git a/fiftyone/utils/data/importers.py b/fiftyone/utils/data/importers.py
--- a/fiftyone/utils/data/importers.py
+++ b/fiftyone/utils/data/importers.py
@@ -1438,6 +1438,10 @@ class LegacyFiftyOneDatasetImporter(GenericSampleDatasetImporter):
         rel_dir (None): a relative directory to prepend to each filepath if it
             is not absolute. This path is converted to an absolute path (if
             necessary) via :func:`fiftyone.core.utils.normalize_path`
+        import_saved_views (True): whether to include saved views in the
+            import. Only applicable when importing full datasets
+        import_runs (True): whether to include annotation/brain/evaluation
+            runs in the import. Only applicable when importing full datasets
         shuffle (False): whether to randomly shuffle the order in which the
             samples are imported
         seed (None): a random seed to use when shuffling
@@ -1449,6 +1453,8 @@ def __init__(
         self,
         dataset_dir,
         rel_dir=None,
+        import_saved_views=True,
+        import_runs=True,
         shuffle=False,
         seed=None,
         max_samples=None,
@@ -1461,6 +1467,8 @@ def __init__(
         )
 
         self.rel_dir = rel_dir
+        self.import_saved_views = import_saved_views
+        self.import_runs = import_runs
 
         self._metadata = None
         self._rel_dir = None
@@ -1561,12 +1569,16 @@ def import_extras(self, sample_collection):
 
         # Import saved views
         saved_views = self._metadata.get("saved_views", None)
-        if saved_views:
+        if (
+            saved_views
+            and self.import_saved_views
+            and self.max_samples is None
+        ):
             _import_saved_views(dataset, saved_views)
 
         # Import annotation runs
         annotation_runs = self._metadata.get("annotation_runs", None)
-        if annotation_runs:
+        if annotation_runs and self.import_runs and self.max_samples is None:
             for anno_key in annotation_runs.keys():
                 if dataset.has_annotation_run(anno_key):
                     logger.warning(
@@ -1583,7 +1595,7 @@ def import_extras(self, sample_collection):
 
         # Import brain method runs
         brain_methods = self._metadata.get("brain_methods", None)
-        if brain_methods:
+        if brain_methods and self.import_runs and self.max_samples is None:
             for brain_key in brain_methods.keys():
                 if dataset.has_brain_run(brain_key):
                     logger.warning(
@@ -1600,7 +1612,7 @@ def import_extras(self, sample_collection):
 
         # Import evaluation runs
         evaluations = self._metadata.get("evaluations", None)
-        if evaluations:
+        if evaluations and self.import_runs and self.max_samples is None:
             for eval_key in evaluations.keys():
                 if dataset.has_evaluation(eval_key):
                     logger.warning(
@@ -1656,6 +1668,10 @@ class FiftyOneDatasetImporter(BatchDatasetImporter):
             each sample if the filepath is not absolute. This path is converted
             to an absolute path (if necessary) via
             :func:`fiftyone.core.utils.normalize_path`
+        import_saved_views (True): whether to include saved views in the
+            import. Only applicable when importing full datasets
+        import_runs (True): whether to include annotation/brain/evaluation
+            runs in the import. Only applicable when importing full datasets
         ordered (True): whether to preserve document order when importing
         shuffle (False): whether to randomly shuffle the order in which the
             samples are imported
@@ -1668,6 +1684,8 @@ def __init__(
         self,
         dataset_dir,
         rel_dir=None,
+        import_saved_views=True,
+        import_runs=True,
         ordered=True,
         shuffle=False,
         seed=None,
@@ -1681,6 +1699,8 @@ def __init__(
         )
 
         self.rel_dir = rel_dir
+        self.import_saved_views = import_saved_views
+        self.import_runs = import_runs
         self.ordered = ordered
 
         self._data_dir = None
@@ -1873,14 +1893,18 @@ def _parse_frame(fd):
         # Import saved views
         #
 
-        if empty_import:
+        if (
+            empty_import
+            and self.import_saved_views
+            and self.max_samples is None
+        ):
             _import_saved_views(dataset, views)
 
         #
         # Import runs
         #
 
-        if empty_import:
+        if empty_import and self.import_runs and self.max_samples is None:
             _import_runs(
                 dataset,
                 annotations,


Test patch: